diff --git a/.config/nextest.toml b/.config/nextest.toml index f3630d3ca2..f6d597b64d 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -6,3 +6,52 @@ failure-output = "immediate-final" final-status-level = "slow" slow-timeout = { period = "60s", terminate-after = 1 } status-level = "slow" + +# `cargo nextest archive` captures the test binaries + the `elephc` bin, but NOT +# the bridge staticlibs — the codegen test runner links compiled PHP programs +# against libelephc_*.a at run time (via `ld`), so nextest can't discover them. +# Include them explicitly so archived shards can run on a machine that never +# rebuilt the workspace. Built into target/debug by `cargo build $BRIDGE_CRATES` +# in the archive job before archiving. +[[profile.ci.archive.include]] +path = "debug/libelephc_tls.a" +relative-to = "target" +[[profile.ci.archive.include]] +path = "debug/libelephc_pdo.a" +relative-to = "target" +[[profile.ci.archive.include]] +path = "debug/libelephc_crypto.a" +relative-to = "target" +[[profile.ci.archive.include]] +path = "debug/libelephc_phar.a" +relative-to = "target" +[[profile.ci.archive.include]] +path = "debug/libelephc_tz.a" +relative-to = "target" +[[profile.ci.archive.include]] +path = "debug/libelephc_image.a" +relative-to = "target" +[[profile.ci.archive.include]] +path = "debug/libelephc_web.a" +relative-to = "target" + +# Emit a machine-readable JUnit report for every `--profile ci` run. This is +# purely additive (it writes `target/nextest/ci/junit.xml` and changes no test +# outcome or console output), so the native codegen/non-codegen jobs are +# unaffected. The Windows codegen no-regression gate parses this file per shard +# to recover the exact set of failing test names (see the `windows-codegen-parity` +# job in `.github/workflows/ci.yml` and `scripts/windows_codegen_gate_check.py`). +[profile.ci.junit] +path = "junit.xml" + +# The `ir_backend_parity` first-class-callable case bundles 15 legacy-vs-EIR +# parity programs (several link the PCRE staticlib), each compiled and run twice. +# It legitimately runs ~65-70s, just over the global 60s cap, so it needs a +# longer slow-timeout to avoid spurious termination on loaded runners. +[[profile.default.overrides]] +filter = 'test(parity_function_first_class_callable_dispatch)' +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 } diff --git a/.github/traffic/clones-badge.svg b/.github/traffic/clones-badge.svg index cd129e5bdd..797ff53762 100644 --- a/.github/traffic/clones-badge.svg +++ b/.github/traffic/clones-badge.svg @@ -1 +1 @@ -cloners: 14051cloners14051 \ No newline at end of file +cloners: 14246cloners14246 \ No newline at end of file diff --git a/.github/traffic/clones.json b/.github/traffic/clones.json index 7fbe899c6f..6c089d3ef5 100644 --- a/.github/traffic/clones.json +++ b/.github/traffic/clones.json @@ -1,5 +1,5 @@ { - "total_uniques": 14051, + "total_uniques": 14246, "daily": { "2026-03-16": 0, "2026-03-17": 0, @@ -103,6 +103,15 @@ "2026-06-23": 48, "2026-06-24": 31, "2026-06-25": 39, - "2026-06-26": 43 + "2026-06-26": 43, + "2026-06-27": 24, + "2026-06-28": 36, + "2026-06-29": 25, + "2026-06-30": 18, + "2026-07-01": 13, + "2026-07-02": 18, + "2026-07-03": 17, + "2026-07-04": 22, + "2026-07-05": 22 } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94a63038ea..705d414c2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,12 @@ on: pull_request: branches: [main] +# Cancel superseded runs for the same ref (e.g. when a branch is force-pushed or +# rebased) so stale jobs stop consuming runners the moment a newer commit lands. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always BRIDGE_CRATES: >- @@ -18,23 +24,13 @@ env: -p elephc-web jobs: - non-codegen-tests: - name: Build & Non-Codegen Tests (${{ matrix.platform.name }}) - runs-on: ${{ matrix.platform.runner }} + # Compile each platform once, then let that platform's tests start as soon as + # its archive is uploaded. This avoids a matrix-wide barrier where Linux tests + # wait for a queued or slower macOS archive job. + build-archive-macos-aarch64: + name: Build & Archive Tests (macos-aarch64) + runs-on: macos-14 timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - platform: - - name: macos-aarch64 - runner: macos-14 - nextest_jobs: 1 - - name: linux-x86_64 - runner: ubuntu-24.04 - nextest_jobs: 1 - - name: linux-aarch64 - runner: ubuntu-24.04-arm - nextest_jobs: 1 steps: - uses: actions/checkout@v4 @@ -48,19 +44,74 @@ jobs: ~/.cargo/git ~/.cargo/registry target - key: rust-${{ matrix.platform.name }}-${{ hashFiles('Cargo.lock') }} + key: rust-macos-aarch64-${{ hashFiles('Cargo.lock') }} restore-keys: | - rust-${{ matrix.platform.name }}- + rust-macos-aarch64- - name: Install cargo-nextest uses: taiki-e/install-action@nextest - - name: Install native test dependencies (macOS) - if: runner.os == 'macOS' + - name: Install native test dependencies run: brew install pcre2 - - name: Install native test dependencies (Linux) - if: runner.os == 'Linux' + - name: Check (no warnings) + shell: bash + run: | + set -o pipefail + cargo build 2>&1 | tee "$RUNNER_TEMP/cargo-build.log" + ! grep -i warning "$RUNNER_TEMP/cargo-build.log" + + - name: Build bridge/native support crates + # The codegen test runner links compiled programs against bridge + # 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 + # 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 + + - name: Archive test binaries + run: cargo nextest archive --profile ci --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" + + - name: Run doctests + # Doctests cannot run from a nextest archive, so run them here where the + # workspace is already built. + run: cargo test --workspace --doc + + - name: Upload test archive + uses: actions/upload-artifact@v4 + with: + name: nextest-archive-macos-aarch64 + path: ${{ runner.temp }}/nextest-archive.tar.zst + retention-days: 1 + if-no-files-found: error + + build-archive-linux-x86_64: + name: Build & Archive Tests (linux-x86_64) + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build state + uses: actions/cache@v4 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target + key: rust-linux-x86_64-${{ hashFiles('Cargo.lock') }} + restore-keys: | + rust-linux-x86_64- + + - 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 \ @@ -86,35 +137,31 @@ 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 all of them up front so every CI platform validates native support - # crates before integration tests need them. + # (GD / Exif / Imagick / Gmagick / Cairo), 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 - - name: Run non-codegen tests - run: cargo nextest run --profile ci --workspace -E 'not binary(codegen_tests)' --no-fail-fast --retries 1 --flaky-result pass -j ${{ matrix.platform.nextest_jobs }} + - name: Archive test binaries + run: cargo nextest archive --profile ci --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" - name: Run doctests + # Doctests cannot run from a nextest archive, so run them here where the + # workspace is already built. run: cargo test --workspace --doc - codegen-tests: - name: Codegen Tests (${{ matrix.platform.name }} ${{ matrix.shard }}/16) - runs-on: ${{ matrix.platform.runner }} - timeout-minutes: 75 - strategy: - fail-fast: false - matrix: - platform: - - name: macos-aarch64 - runner: macos-14 - nextest_jobs: 1 - - name: linux-x86_64 - runner: ubuntu-24.04 - nextest_jobs: 1 - - name: linux-aarch64 - runner: ubuntu-24.04-arm - nextest_jobs: 1 - shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + - name: Upload test archive + uses: actions/upload-artifact@v4 + with: + name: nextest-archive-linux-x86_64 + path: ${{ runner.temp }}/nextest-archive.tar.zst + retention-days: 1 + if-no-files-found: error + + build-archive-linux-aarch64: + name: Build & Archive Tests (linux-aarch64) + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 steps: - uses: actions/checkout@v4 @@ -128,19 +175,14 @@ jobs: ~/.cargo/git ~/.cargo/registry target - key: rust-${{ matrix.platform.name }}-${{ hashFiles('Cargo.lock') }} + key: rust-linux-aarch64-${{ hashFiles('Cargo.lock') }} restore-keys: | - rust-${{ matrix.platform.name }}- + rust-linux-aarch64- - name: Install cargo-nextest uses: taiki-e/install-action@nextest - - name: Install native test dependencies (macOS) - if: runner.os == 'macOS' - run: brew install pcre2 - - - name: Install native test dependencies (Linux) - if: runner.os == 'Linux' + - name: Install native test dependencies run: | sudo apt-get update sudo apt-get install -y \ @@ -154,18 +196,271 @@ jobs: tzdata \ zlib1g-dev + - name: Check (no warnings) + shell: bash + run: | + set -o pipefail + cargo build 2>&1 | tee "$RUNNER_TEMP/cargo-build.log" + ! grep -i warning "$RUNNER_TEMP/cargo-build.log" + - name: Build bridge/native support crates # The codegen test runner links compiled programs against bridge # 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 all of them up front so every CI platform validates native support - # crates before integration tests need them. + # (GD / Exif / Imagick / Gmagick / Cairo), 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 + - name: Archive test binaries + run: cargo nextest archive --profile ci --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" + + - name: Run doctests + # Doctests cannot run from a nextest archive, so run them here where the + # workspace is already built. + run: cargo test --workspace --doc + + - name: Upload test archive + uses: actions/upload-artifact@v4 + with: + name: nextest-archive-linux-aarch64 + path: ${{ runner.temp }}/nextest-archive.tar.zst + retention-days: 1 + if-no-files-found: error + + non-codegen-tests-macos-aarch64: + name: Non-Codegen Tests (macos-aarch64) + needs: build-archive-macos-aarch64 + runs-on: macos-14 + timeout-minutes: 30 + 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 non-codegen tests + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'not binary(codegen_tests)' \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 1 + + non-codegen-tests-linux-x86_64: + name: Non-Codegen Tests (linux-x86_64) + needs: build-archive-linux-x86_64 + runs-on: ubuntu-24.04 + timeout-minutes: 30 + 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 non-codegen tests + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'not binary(codegen_tests)' \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 1 + + non-codegen-tests-linux-aarch64: + name: Non-Codegen Tests (linux-aarch64) + needs: build-archive-linux-aarch64 + runs-on: ubuntu-24.04-arm + timeout-minutes: 30 + 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 non-codegen tests + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'not binary(codegen_tests)' \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 1 + + codegen-tests-macos-aarch64: + name: 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 codegen test shard - run: cargo nextest run --profile ci --test codegen_tests --partition hash:${{ matrix.shard }}/16 --no-fail-fast --retries 1 --flaky-result pass -j ${{ matrix.platform.nextest_jobs }} + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'binary(codegen_tests)' \ + --partition hash:${{ matrix.shard }}/16 \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 1 + + codegen-tests-linux-x86_64: + name: 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 codegen test shard + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'binary(codegen_tests)' \ + --partition hash:${{ matrix.shard }}/16 \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 1 + + codegen-tests-linux-aarch64: + name: 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 codegen test shard + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'binary(codegen_tests)' \ + --partition hash:${{ matrix.shard }}/16 \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 1 image-api-sync: name: Image API stubs in sync @@ -195,6 +490,26 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build state + uses: actions/cache@v4 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target + key: rust-builtins-docs-${{ hashFiles('Cargo.lock') }} + restore-keys: | + 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 + - name: Regenerate builtins documentation # Re-run the generator; the committed Markdown pages and JSON registry # must come back byte-identical. A diff means someone changed a builtin @@ -221,22 +536,43 @@ jobs: name: Build & Test runs-on: ubuntu-latest needs: - - non-codegen-tests - - codegen-tests + - build-archive-macos-aarch64 + - build-archive-linux-x86_64 + - build-archive-linux-aarch64 + - non-codegen-tests-macos-aarch64 + - non-codegen-tests-linux-x86_64 + - non-codegen-tests-linux-aarch64 + - codegen-tests-macos-aarch64 + - codegen-tests-linux-x86_64 + - codegen-tests-linux-aarch64 - image-api-sync - builtins-docs-sync + - windows-pe-cross-compile + - windows-codegen-gate if: always() steps: - name: Verify test jobs run: | - test "${{ needs.non-codegen-tests.result }}" = "success" - test "${{ needs.codegen-tests.result }}" = "success" + test "${{ needs.build-archive-macos-aarch64.result }}" = "success" + test "${{ needs.build-archive-linux-x86_64.result }}" = "success" + test "${{ needs.build-archive-linux-aarch64.result }}" = "success" + test "${{ needs.non-codegen-tests-macos-aarch64.result }}" = "success" + test "${{ needs.non-codegen-tests-linux-x86_64.result }}" = "success" + test "${{ needs.non-codegen-tests-linux-aarch64.result }}" = "success" + test "${{ needs.codegen-tests-macos-aarch64.result }}" = "success" + test "${{ needs.codegen-tests-linux-x86_64.result }}" = "success" + test "${{ needs.codegen-tests-linux-aarch64.result }}" = "success" test "${{ needs.image-api-sync.result }}" = "success" test "${{ needs.builtins-docs-sync.result }}" = "success" + test "${{ needs.windows-pe-cross-compile.result }}" = "success" + test "${{ needs.windows-codegen-gate.result }}" = "success" benchmark: name: Benchmark Suite runs-on: macos-14 # Apple Silicon runner + # The benchmark suite does a full release build and only tracks trends, so it + # runs on pushes to main rather than on every pull request. + if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 @@ -262,3 +598,248 @@ jobs: path: | benchmark-results.json benchmark-results.md + + windows-pe-cross-compile: + name: Windows PE Cross-Compile Tests + runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + # Silence Wine's diagnostic chatter so it never pollutes captured stdout/stderr. + WINEDEBUG: -all + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build state + uses: actions/cache@v4 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target + key: rust-windows-pe-${{ hashFiles('Cargo.lock') }} + restore-keys: | + rust-windows-pe- + + - name: Install MinGW-w64 cross-compiler + run: | + sudo apt-get update + sudo apt-get install -y \ + binutils-mingw-w64-x86-64 \ + gcc-mingw-w64-x86-64 \ + file + + - name: Install Wine + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends wine64 wine + # On ubuntu-24.04 the wine64 package only ships the internal + # /usr/lib/wine/wine64 loader; the callable binary on PATH is `wine` + # (dispatches to the 64-bit loader since wine32/i386 is not installed + # and is not needed for x86_64-only PE binaries). + command -v wine64 >/dev/null 2>&1 && wine64 --version || wine --version + + - name: Initialize Wine prefix + run: | + wineboot --init || true + wineserver --wait || true + + - name: Check (no warnings) + shell: bash + run: | + set -o pipefail + cargo build 2>&1 | tee "$RUNNER_TEMP/cargo-build.log" + ! grep -i warning "$RUNNER_TEMP/cargo-build.log" + + - name: Run Windows PE tests + run: cargo test --test codegen_tests -- windows_pe --nocapture + + - name: Cross-compile hello-world + run: | + echo ' /tmp/hello.php + cargo run -- --target windows-x86_64 /tmp/hello.php + file /tmp/hello.exe + # Verify it is a valid PE32+ executable + file /tmp/hello.exe | grep -q "PE32+ executable (console) x86-64" + # Verify imports include kernel32 + x86_64-w64-mingw32-objdump -x /tmp/hello.exe | grep -q "KERNEL32.dll" + + - name: Run hello-world under Wine + run: | + WINE_BIN=wine64 + command -v wine64 >/dev/null 2>&1 || WINE_BIN=wine + "$WINE_BIN" /tmp/hello.exe > /tmp/hello.out + cat /tmp/hello.out + grep -q "Hello from Windows!" /tmp/hello.out + + - name: Cross-compile arithmetic test + run: | + echo ' /tmp/arith.php + cargo run -- --target windows-x86_64 /tmp/arith.php + file /tmp/arith.exe | grep -q "PE32+ executable" + + - name: Cross-compile function call test + run: | + echo ' /tmp/func.php + cargo run -- --target windows-x86_64 /tmp/func.php + file /tmp/func.exe | grep -q "PE32+ executable" + + - name: Cross-compile loop test + run: | + echo ' /tmp/loop.php + cargo run -- --target windows-x86_64 /tmp/loop.php + file /tmp/loop.exe | grep -q "PE32+ executable" + + - name: Cross-compile string concatenation test + run: | + echo ' /tmp/concat.php + cargo run -- --target windows-x86_64 /tmp/concat.php + file /tmp/concat.exe | grep -q "PE32+ executable" + + # Windows codegen parity MEASUREMENT + no-regression GATE (unified, sharded 16x). + # Runs the full codegen suite cross-compiled to windows-x86_64 and executed under + # Wine, then does two things from that single run: + # 1. MEASURE (informational): emits passed / failed / parity% for the shard to + # the job summary, so the overall Windows parity picture stays visible. + # 2. GATE (blocking): fails the shard iff any test in the curated allow-list + # (`tests/codegen/support/windows_codegen_allowlist.txt`, the tests that + # currently PASS on Windows) failed. Tests NOT in the allow-list -- the known + # failures AND any brand-new / native-only fixtures -- never fail the gate, so + # Windows parity can only improve, never regress. + # The nextest run step is `continue-on-error: true` so ordinary (non-allow-listed) + # failures do not fail the job; only the post-run gate step decides pass/fail via + # `actual_failures ∩ allow_list`. The aggregating `windows-codegen-gate` job (which + # needs all 16 shards) is the single entry wired into the `test` gate. + # Interpreting a "pass": a real Windows pass OR a graceful skip (harness skip when + # MinGW/Wine are missing, or a raw-asm exit-harness fixture that cannot target + # Windows). A crashing `.exe` cannot stall a shard: the `ci` profile's 60s + # slow-timeout kills and reports a hung binary as a failure. Refresh the allow-list + # as parity grows (see docs/compiling/targets.md, "Windows codegen parity gate"). + windows-codegen-parity: + name: Windows Codegen Parity + Gate (${{ matrix.shard }}/16) + runs-on: ubuntu-24.04 + timeout-minutes: 75 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + env: + # Cross-compile + run every codegen fixture as windows-x86_64 (via Wine). + ELEPHC_TEST_TARGET: windows-x86_64 + # Silence Wine's diagnostic chatter so it never pollutes captured stdout/stderr. + WINEDEBUG: -all + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build state + uses: actions/cache@v4 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target + key: rust-windows-parity-${{ hashFiles('Cargo.lock') }} + restore-keys: | + rust-windows-parity- + + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + + - name: Install native test dependencies (Linux) + 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: Install MinGW-w64 cross-compiler + run: | + sudo apt-get update + sudo apt-get install -y \ + binutils-mingw-w64-x86-64 \ + gcc-mingw-w64-x86-64 \ + file + + - name: Install Wine + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends wine64 wine + # On ubuntu-24.04 the wine64 package only ships the internal + # /usr/lib/wine/wine64 loader; the callable binary on PATH is `wine` + # (dispatches to the 64-bit loader since wine32/i386 is not installed + # and is not needed for x86_64-only PE binaries). + command -v wine64 >/dev/null 2>&1 && wine64 --version || wine --version + + - name: Initialize Wine prefix + run: | + wineboot --init || true + wineserver --wait || true + + - name: Build bridge/native support crates + # Match the native codegen-tests job so bridge-linking fixtures build their + # host staticlibs up front instead of triggering serialized on-demand builds + # mid-shard. + run: cargo build $BRIDGE_CRATES + + - name: Run codegen test shard under windows-x86_64 + # `--no-fail-fast` so the shard measures every fixture. `continue-on-error` + # is on the STEP (not the job): ordinary test failures -- including the + # ~1874 known Windows failures -- must not fail the job here, because the + # gate step below is the sole arbiter of pass/fail. nextest still writes its + # JUnit report (`target/nextest/ci/junit.xml`, configured in + # `.config/nextest.toml`) even when tests fail, which the gate step parses. + id: nextest + continue-on-error: true + run: cargo nextest run --profile ci --test codegen_tests --partition hash:${{ matrix.shard }}/16 --no-fail-fast --retries 1 --flaky-result pass + + - name: Windows codegen no-regression gate + parity summary + # Compute regressions = actual_failures ∩ allow_list from THIS shard's JUnit + # report. Emits passed / failed / parity% to the job summary (measurement), + # then exits non-zero -- failing the shard -- iff any allow-listed test + # regressed. Non-allow-listed failures (known failures + brand-new tests) + # are ignored, so this never blocks native-only fixtures. + run: | + python3 scripts/gen_windows_codegen_allowlist.py gate \ + --allowlist tests/codegen/support/windows_codegen_allowlist.txt \ + --junit target/nextest/ci/junit.xml \ + --shard "${{ matrix.shard }}/16" + + - name: Upload shard JUnit report + # Retained so a maintainer can refresh the allow-list from a real parity run + # by feeding these 16 reports to `gen_windows_codegen_allowlist.py generate`. + if: always() + uses: actions/upload-artifact@v4 + with: + name: windows-codegen-junit-${{ matrix.shard }} + path: target/nextest/ci/junit.xml + if-no-files-found: warn + + # Aggregating gate: a single job that is green iff every sharded + # `windows-codegen-parity` job was green (i.e. no allow-listed Windows codegen + # test regressed on any shard). A matrix job's `result` in `needs` is `success` + # only when ALL of its shards succeeded, so this collapses the 16 shards into the + # one entry wired into the `test` gate's `needs` below. Kept separate from the + # measurement/gate shards so the `test` gate lists one dependency, not sixteen. + windows-codegen-gate: + name: Windows Codegen No-Regression Gate + runs-on: ubuntu-latest + needs: + - windows-codegen-parity + if: always() + steps: + - name: Verify no allow-listed Windows codegen regressions + run: | + echo "windows-codegen-parity result: ${{ needs.windows-codegen-parity.result }}" + test "${{ needs.windows-codegen-parity.result }}" = "success" diff --git a/.gitignore b/.gitignore index 22450ffd17..0285daba46 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,14 @@ __pycache__/ .DS_Store # Internal planning docs (specs/plans) — kept locally, not tracked docs/superpowers/ +# Subagent-driven-development scratch (briefs, reports, review diffs, ledger) — local only +.superpowers/ +# Generated archives written to the repo root when the phar-write example +# is run from here instead of its own directory (root-anchored so the +# tracked examples/phar-reader/app.phar fixture is unaffected) +/hello.phar +/note.phar +/oop.phar +/bundle.tar +/bundle.tar.gz +/bundle.tar.bz2 diff --git a/.plans/DESIGN_eir_codegen_bugs.md b/.plans/DESIGN_eir_codegen_bugs.md new file mode 100644 index 0000000000..f923cd7ad8 --- /dev/null +++ b/.plans/DESIGN_eir_codegen_bugs.md @@ -0,0 +1,835 @@ +# Design: EIR Lowering / Codegen PHP-Compatibility Bug Fixes (Issues #384, #340, #377, #381, #360) + +Status: **Design only — no implementation.** All designs are grounded in the current +`main` source. Each issue was reproduced locally (see "Reproduction" notes) except +#340 and #377, whose original symptom has already been fixed on `main` by commits +`9b5913988` (interpolation) and `00aecd00f` (release overwritten locals by storage +type); for those two the design targets the **residual gap** that still lets +related inputs go wrong, plus hardening tests so the regression cannot return. + +Active-backend constraints respected throughout: +- No edits to `src/codegen/expr/`, `src/codegen/stmt/`, `src/codegen/builtins/` + (frozen legacy). New semantics go through `src/ir_lower/` and + `src/codegen_ir/`. +- ARM64 **and** x86_64 lowerings for every new codegen path. +- Every new `emitter.instruction(...)` carries a `//` comment at column 81. +- New/edited Rust files get a `//!` module preamble and `///` docblocks on every + function. Leaf files stay under the 500-LOC cohesion guideline. +- Runtime cache (`~/.cache/elephc/*.o`) must be invalidated after any runtime + emitter change; the design notes this where relevant. + +--- + +## Issue #384 — `match` subject call loses by-reference write-back + +### Reproduction +``` +function bump(&$i) { $i++; return $i; } +$i = 0; +echo match (bump($i)) { 1 => 'one', default => 'other' } . '|' . $i; +``` +PHP prints `one|1`. elephc prints `one|0`. + +### Root cause (verified) +The bug is **not match-specific**. The identical program with +`echo bump($i) . '|' . $i;` also prints `1|0`, and `switch (bump($i))` prints +`one|1`. The difference is statement vs. expression scope. + +`--emit-ir` (with and without `--no-ir-opt`) for both the bare call and the +`match` form shows the second read of `$i` lowered as +`v6: I64 = const_i64 0` — i.e. **the AST-level constant propagator already +folded `$i` to `0` before EIR lowering**. The generated assembly +(`/tmp/t384b.s:4073`) correctly passes `sub x0, x29, #120` (the address of +`$i`'s slot) to `_fn_bump`, and `_fn_bump` correctly does +`ldr x0, [x9]` / `str x0, [x9]` through that pointer — so the *runtime* +write-back works. The folded `0` for the later read simply never observes it. + +The folding happens in `src/optimize/propagate/expr.rs:52`: + +```rust +ExprKind::BinaryOp { left, op, right } => ExprKind::BinaryOp { + left: Box::new(propagate_expr(*left, env)), + op, + right: Box::new(propagate_expr(*right, env)), // <-- same env +}, +``` + +The right operand is propagated with the **same `env`** as the left. A +`FunctionCall` left operand (`bump($i)`) returns `None` from +`expr_local_writes` ("may write anything"), but that write-set is consulted +only at **statement** boundaries (`stmt_local_writes`). Inside one expression +the propagator never invalidates variables that the left subexpression may +have mutated. `ExprKind::Match` (`propagate_expr` at line 111) propagates the +subject and each arm with the same `env` too, so the arms see the pre-subject +value of `$i`. + +`switch` works only because the `echo '|' . $i;` is a *separate statement* +after the switch: `stmt_local_writes(switch_stmt)` returns `None` (the +subject contains a call), so the next statement starts with a fully +invalidated env. + +### Spec +- **PHP behavior**: By-reference parameter mutation is a visible side effect + on the caller's variable. Any subsequent read of that variable — in the + same expression or a later statement — must observe the mutated value. + Evaluation order is left-to-right for `BinaryOp`, subject-before-arms for + `match`, condition-before-body for `??`/`?:`, callee-before-args for calls. +- **elephc current**: Constant propagation within a single expression reuses + one `env` across all subexpressions, so a by-ref call followed by a read of + the mutated variable in the same expression folds to the stale constant. +- **Fix target**: Intra-expression propagation must invalidate every variable + that a *may-write* subexpression can mutate, *before* propagating the next + subexpression in source/evaluation order. This is the general fix; `match` + is one beneficiary. + +### Architecture + +**Primary file**: `src/optimize/propagate/expr.rs` (rewrite of +`propagate_expr` to thread a *write-invalidate* accumulator through +evaluation order). + +New helper in the same file (or a small new `src/optimize/propagate/order.rs` +if `expr.rs` would exceed the soft 500-LOC limit after the change — current +`expr.rs` is 439 LOC, so an in-place edit is preferred): + +```rust +/// Returns the set of locals a subexpression may write, or `None` when it +/// may write anything (conservative). Mirrors `expr_local_writes` but is +/// kept in sync with the order in which `propagate_expr` visits children. +fn expr_may_write_locals(expr: &Expr) -> Option> +``` + +`propagate_expr` gains an internal recursive form that threads a mutable +`ConstantEnv`: + +```rust +fn propagate_expr_ordered(expr: Expr, env: &mut ConstantEnv) -> Expr +``` + +For each composite variant, after propagating the left/first child with the +current `env`, compute `expr_may_write_locals(child)`: +- `Some(set)` → remove every name in `set` from `env` before propagating the + next child. +- `None` → clear `env` entirely (unknown writes) before propagating the next + child. + +Variants that must invalidate between children, in evaluation order: +- `BinaryOp { left, op, right }` — invalidate after `left`, before `right`. + (PHP evaluates `left` then `right` for all binary ops including `&&`/`||`, + which already short-circuit in `propagate_expr` via their own arms — those + keep their existing handling and just additionally invalidate.) +- `Match { subject, arms, default }` — invalidate after `subject`; each arm + body is propagated in its own fresh env (arms are mutually exclusive, so a + write in arm 1 must not be visible to arm 2). +- `Assignment { value, result_target, prelude, conditional_value_temp, .. }` + — the `prelude` (by-ref setup) and `value` already mutate the target; keep + existing handling but invalidate the target name after the value is + propagated, before any `result_target` read. +- `NullCoalesce { value, default }`, `Ternary { .. }`, + `ShortTernary { .. }` — invalidate after the condition/value, before the + alternative (the alternative only runs when the first didn't, but + conservatively the first may have written before deciding). +- `Pipe { value, callable }` — invalidate after `value`. +- `PreIncrement`/`PostIncrement`/`PreDecrement`/`PostDecrement` (as + expression operands) — invalidate the named variable after the operand, + before any surrounding continuation. (These already propagate as their own + variant; the change is that a *parent* binary/match invalidates after them.) +- Function/method/constructor call variants (`FunctionCall`, `MethodCall`, + `NullsafeMethodCall`, `StaticMethodCall`, `ExprCall`, `ClosureCall`, + `NewObject`, `NewDynamic`, `NewDynamicObject`, `NewScopedObject`) — these + already return `None` from `expr_local_writes`; the ordered propagator + treats them as `None` writers and clears the env for any following sibling. + +**Optimizer effect modeling** (`src/optimize/effects.rs` and +`src/optimize/effects/calls.rs`): no change required — `function_call_effect` +already sets `with_side_effects()`, which is what makes +`expr_local_writes` return `None`. The fix only changes *propagation*, not +*effect modeling*. + +**Tests**: `src/optimize/tests/propagate/straight_line.rs` and +`src/optimize/tests/propagate/loops/foreach_loops.rs` get new unit tests +asserting the AST after propagation keeps the second read as a `Variable` +(not a literal) when a preceding call may write it. Codegen regression test +in `tests/codegen/callables/language_features.rs` (or a new +`tests/codegen/by_ref/intra_expr_writeback.rs`) with the issue's exact +program plus the bare-call and switch variants. + +### Test plan +| # | PHP source | Expected | Covers | +|---|---|---|---| +| 1 | `function bump(&$i){$i++;return $i;} $i=0; echo match(bump($i)){1=>'one',default=>'other'}.'|'.$i;` | `one\|1` | Issue #384 exact | +| 2 | `function bump(&$i){$i++;return $i;} $i=0; echo bump($i).'|'.$i;` | `1\|1` | Bare call (same root cause) | +| 3 | `function bump(&$i){$i++;return $i;} $i=0; switch(bump($i)){case 1: echo 'one'; break; default: echo 'other';} echo '|'.$i;` | `one\|1` | Switch (must keep working) | +| 4 | `function add(&$a,$v){$a[]=$v;return count($a);} $a=[]; echo add($a,1).$a[0];` | `11` | Array append by-ref + read in same expr | +| 5 | `$i=0; echo ($i++).'|'.$i;` | `0\|1` | Post-increment in same expression | +| 6 | `function b(&$x){$x=5;return $x;} $i=0; $r = b($i) + $i; echo $r;` | `10` | Binary op + by-ref | +| 7 | `function b(&$x){$x=5;return 1;} $i=0; echo match(b($i)){1=>$i,default=>0};` | `5` | Match arm reads mutated var | +| 8 | `$i=5; echo $i . ($i=0) . $i;` | `500` | Assignment mid-expression (PHP eval order) | + +Edge: `echo bump($i) . bump($i);` with `$i=0` → PHP: `12` (first call makes +`$i=1` returns `1`; second makes `$i=2` returns `2`). Validates that repeated +calls in the same expression each invalidate. + +### Risk assessment +- **High-value risk**: over-invalidation could suppress *legitimate* folding + and regress optimized output size/speed. Mitigation: the invalidation only + fires for `None`/non-empty write-sets; pure subexpressions (literals, + variable reads, pure builtins) return `Some(empty)` and don't invalidate. +- **Short-circuit ops**: `&&`/`||`/`??` only evaluate the right side + conditionally; invalidating before the right side is still safe (the right + side runs only when the left didn't write, so clearing is conservative, not + wrong). Verify with `$i=1; $r = ($i=0) && ($i=2); echo $i;` → PHP `0`. +- **Regression watch**: existing propagation tests in + `src/optimize/tests/propagate/**` and `tests/codegen/optimizer/**` must stay + green. The `--no-ir-opt` IR for the issue program must show the second `$i` + read as `load_local`, not `const_i64`. + +--- + +## Issue #340 — String interpolation with array access emits corrupted output + +### Reproduction +The issue's exact input `echo "{$a['x']}";` now compiles and prints `ok` on +`main` (fixed by `9b5913988 fix(lexer): support complex and simple string +interpolation forms`). Residual gaps found while grounding this design: + +1. **Deprecated `${...}` form** (PHP 8.2 deprecation, still functional): + `echo "${a['x']}";` → PHP prints `ok` (with deprecation notice); elephc + emits the literal text `${a['x']}` (the `${` form is never recognized — + `src/lexer/literals/strings.rs:140` only matches `{$`). +2. **Heredoc with a `}` inside a quoted key**: works for `"{$a['x']}"` but + the brace/quote scanner in `capture_braced_expr` (line 289) is hand-rolled; + it does not understand PHP string *escapes* inside the embedded string + (only raw `\\` and matching quote). A key like `"{$a['x\'']}"` would + confuse the depth counter. PHP rejects this as a parse error, so elephc + should too — but it currently mis-captures rather than erroring cleanly. + +The original `{{['x']}` symptom was the `{$a['x']}` form before +`9b5913988`; the design below locks in the fix and covers the residual +`${...}` and escape-balancing gaps. + +### Spec +- **PHP behavior**: Inside a double-quoted string or heredoc body: + - `{$expr}` — complex interpolation; `$expr` is any PHP expression that + *starts with `$`* (a variable, `$this`, `${...}`, or a callable/array + access chain on one). The braces close at the matching `}`, with + balanced nested braces and string literals skipped verbatim. + - `${name}` (deprecated since PHP 8.2) — simple variable lookup of + `$name`; PHP emits `E_DEPRECATED` and treats it as `$name` (not an + arbitrary expression; `${a['x']}` is special: PHP reads it as + `$a['x']` for legacy compatibility — see PHP's `ZEND_COMPILE_*` + `${...}` rule). +- **elephc current**: `{$expr}` is handled by `capture_braced_expr` + + `tokenize_fragment` (re-lexes the captured text as `prop}`. + `${name}` is not recognized at all (emitted literally). +- **Fix target**: + 1. Keep `{$expr}` handling as-is (regression-protected by new tests). + 2. Add `${name}` (deprecated) handling: recognize `${` at the start of an + interpolation, emit a deprecation warning through the existing warning + channel (`src/types/warnings/`), and parse the contents as a variable + name with optional single-level `[offset]` (matching PHP's legacy + `${a['x']}` semantics, which is *not* a full expression — only + `$var` or `$var[offset]`). + 3. Harden `capture_braced_expr` to reject unbalanced/unterminated inputs + with the existing `"Unterminated complex interpolation '{$...}'"` error + rather than silently mis-capturing. + +### Architecture + +**Primary file**: `src/lexer/literals/strings.rs`. + +- In `interpolate` (line 112), add a new arm before the `Some('$') =>` arm: + + ```rust + Some('$') if input.peek_nth(1) == Some('{') => { + input.advance_escape(); // consume '$' + input.advance_escape(); // consume '{' + let inner = capture_dollar_brace_var(input, span)?; + has_interpolation = true; + // emit a deprecation warning via the lexer's warning sink + // ... build token stream for `$var` or `$var[offset]` ... + push_interp_part(&mut tokens, &mut current, part, span); + } + ``` + +- New function `capture_dollar_brace_var` in the same file: parses only + `$name`, `$name[offset]` (with `$name`, integer, or bareword offset — + reusing `append_simple_offset_key`), and the closing `}`. Anything more + complex (e.g. `${a->prop}`, `${func()}`) is a PHP parse error; emit a + `CompileError` with a clear message rather than mis-capturing. This keeps + the new code under the 500-LOC guideline (the file is currently ~722 LOC + and a *single cohesive feature* — string scanning — so it already lives + above the soft limit legitimately; adding ~40 LOC stays within the + "cohesive leaf" exception). + +- Deprecation warning: the lexer does not currently emit warnings; the + existing `CompileError` is error-only. Wire a `DiagnosticSink`-style + warning through the lexer's return path, or defer the warning to the type + checker by tagging the token with a `DeprecatedInterpolation` marker. + **Preferred minimal approach**: emit the warning during + `src/types/warnings/expr_reads.rs` (which already walks interpolation + expression nodes) by detecting a new `ExprKind::DeprecatedDollarBrace` + wrapper. Add that wrapper in `src/parser/ast/expr.rs`, parse it in + `src/parser/expr/prefix_complex.rs`'s interpolation-token consumer, and + lower it in `src/ir_lower/expr/mod.rs` as the underlying `$var[offset]`. + +- `capture_braced_expr` hardening: after the capture loop, if `depth != 0` + is already caught (EOF → error). Add an additional check: if the captured + `inner` re-lex fails (`tokenize_fragment` returns an error), propagate the + error instead of pushing a malformed part. + +**Parser**: `src/parser/expr/prefix_complex.rs` — the interpolation token +stream consumer already handles `Variable` + `ArrayAccess`; the new +`DeprecatedDollarBrace` node is a single new variant consumed there. + +**EIR lowering**: `src/ir_lower/expr/mod.rs:90` dispatch — add +`ExprKind::DeprecatedDollarBrace { .. } => lower_deprecated_dollar_brace(...)` +that simply delegates to `lower_array_access` / `lower_expr(Variable)`. +No new EIR op needed. + +**No codegen change** — interpolation lowers to normal `Op::StrInterpolate` +plus the inner expression's ops, which already work. + +### Test plan +| # | PHP source | Expected | Covers | +|---|---|---|---| +| 1 | `$a=['x'=>'ok']; echo "{$a['x']}";` | `ok` | Original issue (regression lock) | +| 2 | `$a=['x'=>'ok']; echo "{$a["x"]}";` | `ok` | Double-quoted key inside braces | +| 3 | `$a=[['x'=>'deep']]; echo "{$a[0]['x']}";` | `deep` | Nested array access | +| 4 | `$a=['x'=>'ok']; echo "${a['x']}";` | `ok` (+ deprecation warning) | `${...}` legacy form | +| 5 | `$a=['k'=>1]; echo "${a['k']}";` | `1` | `${...}` int value | +| 6 | `$a=['x'=>'ok']; echo "pre {$a['x']} post";` | `pre ok post` | Interpolation with surrounding text | +| 7 | `$a=['x'=>'ok']; echo "{$a['x']}{$a['x']}";` | `okok` | Two interpolations in one string | +| 8 | `echo "{$a['x']}";` with `$a` undefined | PHP: empty + notice | Undefined-key interpolation (elephc may fatal; document) | +| 9 | Lexer error test: `echo "{$a['x']";` (unbalanced) | `Unterminated complex interpolation` | Hardened error path | + +Error tests go in `tests/error_tests/` (new `interpolation.rs`); codegen +tests extend `tests/codegen/strings/interpolation_and_hashes.rs`. + +### Risk assessment +- **Deprecation warning plumbing** is the most invasive part. If wiring a + warning through the lexer is too costly, the minimal alternative is to + *accept* `${name}` silently (matching the *value* PHP produces) and defer + the deprecation notice to a follow-up. This still fixes the corruption + (literal `${a['x']}` output) without the warning sink. +- **Parse-error parity**: PHP's `${...}` accepts only a narrow subset; any + deviation must produce a clean error, not silent mis-capture. New error + tests (#9) guard this. +- **Regression watch**: existing interpolation tests in + `tests/codegen/strings/interpolation_and_hashes.rs` and + `tests/parser_tests/**` must stay green. + +--- + +## Issue #377 — Numeric loop variable that becomes float leaks heap + +### Reproduction +On current `main` the exact issue program (`$i = 0; $i = $i + 1.0` for 2M +iterations) runs to completion with the correct result `1999999000000` in +both debug and release (verified locally). Commit `00aecd00f fix(ir): release +overwritten locals by storage type` closed the leak for the common path. + +**Residual gap verified during grounding**: the slot-widening logic in +`src/ir/builder.rs:385 widened_local_storage_type` promotes +`Int → Float` storage to `Mixed` (boxed). The cleanup load in +`src/ir_lower/context.rs:594 release_stored_local_value` then loads the slot +as `Heap(Mixed)` and calls `Op::Release` even on the very first reassignment, +when the slot still holds a *raw unboxed int* (the `store_local v1 slot[1]` +in the loop preheader stores an `I64` into the not-yet-widened slot). The +runtime `__rt_decref_mixed` (in +`src/codegen/runtime/arrays/decref_mixed.rs:42`) treats the raw int bits as +a "pointer" and skips the decref because `0`/small ints are below +`_heap_buf`. This is *safe* (no crash) but means a raw-int slot can be +released as Mixed without effect, while a subsequently-stored boxed float +*is* released correctly on iteration 2+. The net leak on `main` is at most +one cell per widening transition — small, but it generalizes to other +type-widening transitions (`Bool → Float`, `Int → Str`, etc.) where the +first reassignment releases a non-boxed old value as if it were boxed. + +### Spec +- **PHP behavior**: Reassigning a variable that changes its runtime type + (int → float, int → string, etc.) must free the previous value's storage + exactly once. No leak across long loops. +- **elephc current**: `release_stored_local_value` uses the slot's + *widened storage type* (`local_php_type(slot)`) to load and release the + previous occupant. When the previous occupant was stored before the + slot was widened (raw int/bool), the release is a no-op for that + transition; for transitions into refcounted storage it can leak the first + old value. +- **Fix target**: Release the previous occupant using the *type the slot + held when the previous store happened*, not the post-widening storage + type. Concretely: track the previous logical type (`previous_type` + captured at `store_local:617`) and use it to decide whether the old value + is refcounted *before* the widening. If `previous_type` is non-refcounted + (`Int`/`Bool`/`Float`/`Void`), skip the release entirely — there is no + heap cell to free. + +### Architecture + +**Primary file**: `src/ir_lower/context.rs`. + +Change `release_stored_local_value` (line 594) to take the *previous logical +type* as an argument, and skip the release when that type does not need +lifetime tracking: + +```rust +fn release_stored_local_value_with_previous_type( + &mut self, + name: &str, + slot: LocalSlotId, + previous_type: PhpType, + span: Option, +) { + if !Ownership::php_type_needs_lifetime_tracking(&previous_type) { + return; // old occupant was a raw scalar; nothing to free + } + let previous = self.load_local_storage(name, slot, previous_type, span); + crate::ir_lower::ownership::release_if_owned(self, previous, span); +} +``` + +Update the three call sites in `store_local` (lines 633, 642, 656) and the +epilogue path (line 948) to pass `previous_type` (or `local_php_type(slot)` +for the epilogue, which is correct there because the epilogue runs after all +stores have widened the slot to its final type and boxed every occupant). + +**No EIR-op change**, no runtime change, no codegen change. This is purely a +lowering-side type-accuracy fix. + +**Tests**: `src/ir_lower/tests/ownership.rs` — add a unit test that builds a +function with an `Int`-then-`Float` store to the same slot and asserts the +EIR contains exactly one `release` (for the boxed float on iteration 2+), +not a release of the raw int. End-to-end test in +`tests/codegen/runtime_gc/` (new `type_widening_loop.rs`) running the issue +program at 2M iterations with an assertion on the result and a *memory-debug* +run (`ELEPHC_HEAP_DEBUG=1`) asserting no leak is reported. + +### Test plan +| # | PHP source | Expected | Covers | +|---|---|---|---| +| 1 | `$acc=0.0; for($i=0;$i<2000000;$i=$i+1.0){$acc=$acc+$i;} echo $acc;` | `1999999000000` | Issue #377 exact (2M, no OOM) | +| 2 | `$acc=0; for($i=0;$i<2000000;$i=$i+1){$acc+=$i;} echo $acc;` | `1999999000000` | Pure int loop (no widening, must not regress) | +| 3 | `$s=''; for($i=0;$i<100000;$i=$i+1){$s=$s.'x';} echo strlen($s);` | `100000` | Int→string reassignment, no leak | +| 4 | `for($i=0;$i<100000;$i++){ $x = ($i%2==0) ? 1 : 1.5; } echo $x;` | `1.5` | Alternating int/float reassignment | +| 5 | `$x=1; $x=2.0; $x=3; echo $x;` | `3` | Single int→float→int transition | +| 6 | `ELEPHC_HEAP_DEBUG=1` run of #1 | no leak report | Heap-debug instrumentation | + +### Risk assessment +- **Risk**: skipping the release for a non-refcounted `previous_type` could + *under*-free if a previous store actually boxed the value (e.g. a prior + path stored a Mixed into the slot before this straight-line store). The + `previous_type` captured at line 617 is the *logical* type from the last + `set_local_type`, which is updated on every store, so it tracks what was + actually stored. The epilogue path keeps the conservative storage-type + release as a backstop. +- **Regression watch**: `tests/codegen/runtime_gc/**`, `tests/codegen/arrays/**`, + and the existing ownership unit tests must stay green. Run with + `ELEPHC_HEAP_DEBUG=1` locally on tests #1–#4. + +--- + +## Issue #381 — `foreach` over arrays visits appended elements during value iteration + +### Reproduction +``` +$a = [1, 2]; +foreach ($a as $v) { echo $v; if ($v === 1) $a[] = 3; } +echo '|' . count($a); +``` +PHP: `12|3`. elephc: `123|3`. + +### Root cause (verified) +`src/codegen_ir/lower_inst/iterators.rs:1183 lower_indexed_iter_next_aarch64` +(and the x86_64 twin at line 1202) re-reads the array length **every +iteration** from the live array header: + +``` +load_at_offset_scratch array_reg, [offset-0], "x9" ; reload source pointer +load_at_offset index_reg, [offset-8] ; cursor +add index_reg, index_reg, #1 +emit_load_from_address len_reg, array_reg, 0 ; <-- fresh length each iter +cmp index_reg, len_reg +``` + +PHP snapshots the array's length (and, for associative arrays, the entry +count) at the moment `foreach` begins iterating; appends during the loop +body do not extend the iteration. The current lowering treats the array as +live and re-reads its current length, so an append inside the body is +visible to the next `IterNext`. + +The hash path (`__rt_hash_iter_next`, line 1225) is a runtime helper that +already advances a cursor against a snapshot taken at `IterStart` (the hash +iterator stores its own end pointer), so the hash case does **not** have this +bug — only the indexed-array fast path does. + +### Spec +- **PHP behavior**: `foreach ($arr as $v)` captures the array's element count + at loop entry. Appends (`$arr[] = …`), `$arr[count($arr)] = …`, and + `array_push` inside the body do not add new elements to the iteration. + Modifications to *existing* elements that change values are visible (PHP's + semantics are nuanced, but the *count* is frozen). Unset of the current + element is handled separately (PHP keeps a copy of the current value). +- **elephc current**: indexed-array `IterNext` re-reads the length each + iteration → appends are visited. +- **Fix target**: `IterStart` for an indexed array must snapshot the array's + initial length into the iterator state. `IterNext` compares the cursor + against the **snapshotted** length, not the live array header. + +### Architecture + +**Primary file**: `src/codegen_ir/lower_inst/iterators.rs`. + +1. **Add a length slot to the iterator state**: + - In `src/codegen_ir/value_placement.rs`, bump + `ITERATOR_STATE_BYTES` from `64` to `72` (one extra 8-byte word). + - In `src/codegen_ir/lower_inst/iterators.rs`, add + `const ITER_LENGTH_OFFSET_DELTA: usize = 64;` after + `ITER_VALUE_ADDR_OFFSET_DELTA` (line 30). This slot is only used by + the indexed-array path; the hash path keeps using + `__rt_hash_iter_next` and ignores it. + +2. **`lower_iter_start`** (line 48): for `IteratorSourceKind::Indexed { .. }`, + after storing the source pointer and the initial cursor (`-1`), also load + the array's current length (`emit_load_from_address(emitter, result_reg, + source_reg, 0)`) and store it at `offset - ITER_LENGTH_OFFSET_DELTA`. + This is the snapshot. ARM64 and x86_64 paths each get a 2-instruction + addition with column-81 comments. + +3. **`lower_indexed_iter_next_aarch64`** (line 1183) and + `lower_indexed_iter_next_x86_64` (line 1202): replace the fresh length + load with a load from the snapshot slot: + - Remove: `emit_load_from_address(len_reg, array_reg, 0)` + - Add: `load_at_offset(emitter, len_reg, offset - ITER_LENGTH_OFFSET_DELTA)` + The `array_reg` load is still needed because the value loader + (`load_current_array_value_*`) reads the element from `[array_reg, + element_offset]`. Keep that load. + +4. **Dynamic iterable/mixed indexed paths** + (`initialize_dynamic_iterable_iterator`, + `initialize_dynamic_mixed_iterator`): these already store the cursor; + add the same length snapshot for the indexed branch. The hash and object + branches leave the length slot uninitialized (the indexed `IterNext` is + the only reader, and the dynamic dispatch routes hash/object to their own + `IterNext` lowering, so an uninitialized length slot is never read on + those paths). + +5. **By-reference foreach**: PHP's `foreach ($a as &$v)` does **not** use the + by-value length snapshot for indexed arrays; appended elements remain + visible to the live iteration. Preserve that by making `IterNext` consult + the `IterStart` by-ref flag and read the live array length on indexed by-ref + paths. Verify with a regression test. + +**No EIR-op change**, no `src/ir_lower/stmt/mod.rs` change. The EIR +`Op::IterStart`/`Op::IterNext` keep their existing operand lists; the +length snapshot is a codegen-internal detail of the indexed-array lowering. + +**Runtime cache**: the iterator state size changes from 64 to 72 bytes. This +is a frame-layout change internal to each function; no runtime `.o` is +affected, so `~/.cache/elephc/*.o` does **not** need clearing. (Document +this in the PR description anyway.) + +### Test plan +| # | PHP source | Expected | Covers | +|---|---|---|---| +| 1 | `$a=[1,2]; foreach($a as $v){echo $v; if($v===1)$a[]=3;} echo '|'.count($a);` | `12\|3` | Issue #381 exact | +| 2 | `$a=[1,2,3]; foreach($a as $v){echo $v; $a[]=9;} echo '|'.count($a);` | `123\|6` | Multiple appends | +| 3 | `$a=[1,2]; foreach($a as &$v){echo $v; if($v===1)$a[]=3;} echo '|'.count($a);` | `123\|3` | By-ref foreach live length | +| 4 | `$a=range(1,1000); $c=0; foreach($a as $v){$c++; $a[]=$v;} echo $c;` | `1000` | Large loop with appends, exact count | +| 5 | `$a=[1,2,3]; foreach($a as $k=>$v){echo "$k=$v,"; $a[]=$k;} echo count($a);` | `0=1,1=2,2=3,6` | Key+value foreach with appends | +| 6 | `$a=['a'=>1,'b'=>2]; foreach($a as $k=>$v){echo "$k=$v,"; $a['c']=3;} echo count($a);` | `a=1,b=2,3` | Hash foreach (already correct, lock in) | +| 7 | `$a=[1,2]; foreach($a as $v){echo $v; unset($a[0]);}` | PHP: `12` | Unset during foreach (current behavior, document) | + +Codegen tests in `tests/codegen/arrays/foreach_snapshot.rs` (new file, with +module preamble). + +### Risk assessment +- **Frame-size change** (64→72 bytes per iterator): the + `allocates_iter_start_value_as_iterator_state` unit test in + `src/codegen_ir/value_placement.rs` hard-codes the slot offset; update it. + Any other test asserting iterator state size must be updated. +- **Uninitialized length slot** on hash/object paths: confirm via the + validator that no `IterNext` on those paths reads + `ITER_LENGTH_OFFSET_DELTA`. Add a debug assertion in + `lower_indexed_iter_next_*` that the source kind is `Indexed` (already + guaranteed by the dispatch in `lower_iter_next` line 124). +- **Regression watch**: all existing `tests/codegen/arrays/foreach*.rs` and + `src/ir_lower/tests/arrays.rs` tests. Run the foreach key-write tests + specifically (`tests/codegen/arrays/foreach_key_write.rs`) since they + exercise the same iterator state layout. + +--- + +## Issue #360 — Array elements cannot be passed to by-reference parameters + +### Reproduction +``` +function bump(&$x) { $x++; } +$a = [5]; +bump($a[0]); +echo $a[0]; +``` +PHP: `6`. elephc: `parameter $x must be passed a variable` (compile error). + +### Root cause (verified) +Two distinct gaps: + +1. **Checker rejects valid lvalues**: + - `src/types/checker/functions/call_validation.rs:332-347` rejects any + by-ref argument that is not `ExprKind::Variable(_)`: + ```rust + if sig.ref_params.get(param_idx).copied().unwrap_or(false) + && !matches!(arg.kind, ExprKind::Variable(_)) + { return Err("... must be passed a variable"); } + ``` + - The same check is duplicated in + `src/types/checker/functions/resolution/mod.rs:293-308` and `:527-542` + for already-resolved callees. + - PHP allows as by-ref targets: `$var`, `$a[idx]`, `$a[]` (append), + `$a[$i]`, `$o->prop`, `$$var`, and `$a[idx][idx2]` (nested). PHP 8 + also accepts `$a[]` (append) as a write target for by-ref. + +2. **EIR lowering has no by-ref array-element path**: + - `src/ir_lower/expr/mod.rs:4315 lower_by_ref_array_arg_with_signature` + only handles the *array-widening* case (converting `Array(Int)` to + `Array(Mixed)` before a Mixed ref param). It does **not** pass the + element's address. + - For a plain `$a[0]` argument to a by-ref param, `lower_arg_with_signature` + (line 4240) falls through to `lower_expr(arg)`, which emits an + `Op::ArrayGet` (a *read*) and passes the value. The codegen + `plan_ref_arg_writebacks` (`src/codegen_ir/lower_inst.rs:5194`) only + plans writebacks for **Mixed-typed parameters** with scalar sources, so + an `Int`-typed element passed to an `Int` by-ref param has no writeback + and no address passing — even if the checker accepted it, the mutation + would be lost. + +### Spec +- **PHP behavior**: A by-reference parameter accepts any lvalue: a + variable, an array element (`$a[0]`, `$a['k']`, `$a[$i]`, `$a[]`), a + nested array element, or an object property. The callee mutates the + *caller's storage*. For a packed array with unboxed scalar elements, the + callee mutates the element slot in place; for a Mixed/assoc array, the + callee mutates the boxed cell. `$a[]` as a by-ref target appends a new + element (default `null`) and passes its address. +- **elephc current**: Checker rejects non-variable lvalues; lowering has no + element-address path. +- **Fix target**: + 1. Checker accepts `ExprKind::ArrayAccess { array: Variable, .. }` (and + nested) and `ExprKind::PropertyAccess { .. }` as by-ref targets, with + the existing "must be passed a variable" error retained only for + non-lvalue expressions (literals, calls, binary ops, constants). + 2. EIR lowering passes the *address* of the array element (or property + slot) to the callee, and the codegen writeback path copies any boxed + cell back into the array element after the call. + +### Architecture + +This is the largest of the five fixes. It splits into the checker change +(small, unblocks the error) and the lowering/codegen change (substantial, +actually makes the mutation work). + +**Phase A — Checker (unblock the error)** + +Files: `src/types/checker/functions/call_validation.rs`, +`src/types/checker/functions/resolution/mod.rs`. + +Introduce a shared predicate so the three duplicated checks stay in lockstep +(per the AGENTS.md "do not maintain parallel tables" rule): + +```rust +// in src/types/checker/functions/mod.rs or a new lvalues.rs +pub(super) fn is_by_ref_lvalue(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Variable(_) => true, + ExprKind::ArrayAccess { array, .. } => is_by_ref_lvalue_base(array), + ExprKind::PropertyAccess { object, .. } => is_by_ref_lvalue_base(object), + ExprKind::DynamicPropertyAccess { object, .. } => is_by_ref_lvalue_base(object), + _ => false, + } +} +fn is_by_ref_lvalue_base(expr: &Expr) -> bool { + matches!(expr.kind, + ExprKind::Variable(_) + | ExprKind::ArrayAccess { .. } + | ExprKind::PropertyAccess { .. } + | ExprKind::DynamicPropertyAccess { .. }) +} +``` + +Replace the three `!matches!(arg.kind, ExprKind::Variable(_))` checks with +`!is_by_ref_lvalue(arg)`. The error message can stay "must be passed a +variable" (PHP's own message) or be widened to "must be passed a variable or +array element" — match PHP's wording in tests. + +**Phase B — EIR lowering (make it work)** + +The hard part: passing the address of `$a[0]`. Packed arrays in elephc store +unboxed scalar elements inline (`Array(Int)` → contiguous 8-byte slots); +Mixed/assoc arrays store boxed `Mixed` cells. The by-ref mechanism already +has two paths in `src/codegen_ir/lower_inst.rs`: +- `materialize_local_ref_arg_address` (line 5388): emits the *frame slot + address* of a local variable. This is what makes `bump($i)` work for + scalar by-ref params. +- `materialize_temporary_ref_arg_cell` (line 5278): allocates a heap + ref-cell, copies the value in, passes the cell pointer, and + `emit_ref_arg_writebacks` copies back. + +For `$a[0]`: +- **Packed scalar array**: the element lives at + `array_header + 16 + index * 8` (header is 16 bytes: length + capacity). + The address is `array_ptr + 16 + index*8`. We can compute this and pass + it directly — the callee reads/writes through that pointer exactly like + it does for a local slot. No writeback needed because the write goes + directly into the array storage. +- **Mixed/assoc array**: the element is a boxed `Mixed` cell allocated on + the heap. The element's address is the cell pointer (read from the hash + bucket). Pass that pointer; the callee mutates the cell in place. Again + no writeback needed because the cell is shared. + +So the design is: compute the *element address* and pass it, mirroring +`materialize_local_ref_arg_address` but with the address derived from the +array pointer + index instead of a frame slot. + +New EIR op (so the codegen can emit target-specific address arithmetic +without the lowering having to know element byte sizes): + +```rust +// src/ir/instr.rs +ArrayElementAddr, // operands: [array_ptr, index]; result: I64 pointer +PropSlotAddr, // operands: [object_ptr, prop_offset]; for Phase B2 +``` + +`ArrayElementAddr` is `Heap(Array) + Int → I64`. It emits: +- ARM64: load array ptr, load index, compute `ptr + 16 + index*8` (packed) + or call `__rt_hash_element_addr(ptr, key)` for assoc. +- x86_64: same arithmetic with the x86_64 register conventions. + +The lowering (`src/ir_lower/expr/mod.rs`) gets a new by-ref arg path: + +```rust +fn lower_by_ref_element_arg( + ctx: &mut LoweringContext<'_, '_>, + array: &Expr, + index: &Expr, + span: Span, +) -> ValueId { + // For a packed array with statically-known element type, emit + // Op::ArrayElementAddr(array_ptr, index). For a Mixed/assoc array, + // emit the hash-element-address runtime helper. Append targets + // ($a[]) lower to "address of the next slot after auto-grow". +} +``` + +`lower_arg_with_signature` (line 4240) gains, before the `lower_expr(arg)` +fallthrough: + +```rust +if sig.ref_params.get(index).copied().unwrap_or(false) { + if let ExprKind::ArrayAccess { array, index: idx } = &arg.kind { + return lower_by_ref_element_arg(ctx, array, idx, arg.span); + } + if let ExprKind::PropertyAccess { .. } = &arg.kind { + return lower_by_ref_property_arg(ctx, arg); // Phase B2 + } +} +``` + +**Codegen**: `src/codegen_ir/lower_inst.rs` — add `Op::ArrayElementAddr =>` +to the dispatch (line ~155) routing to a new +`src/codegen_ir/lower_inst/arrays.rs::lower_array_element_addr` (or reuse +`arrays.rs` if it has a natural home). This emits the address arithmetic +per target, with column-81 comments on every instruction. + +**Append target `$a[]`**: this is a by-ref write target in PHP. Lower it as +"ensure capacity for one more, return address of the new slot." Reuse the +existing array-grow runtime helpers (`__rt_array_push_*`). The new element +is initialized to `null`/`0` before the callee runs. + +**Writeback**: for the direct-address path, **no writeback** is needed — the +callee writes through the pointer into the array storage. The existing +`plan_ref_arg_writebacks` Mixed-cell writeback path is bypassed for these +args (the source value is an `ArrayElementAddr` result, not a `LoadLocal`, +so `local_ref_arg_source` fails and `materialize_temporary_ref_arg_cell` is +skipped via a new guard that recognizes the element-address opcode). + +**Phase B2 — Object properties** (`$o->prop` by-ref): out of scope for this +fix unless trivial; the issue only mentions array elements. The +`is_by_ref_lvalue` predicate accepts properties so the checker doesn't +reject them, but the lowering emits a clear `unsupported` diagnostic for +property by-ref until a follow-up. Document this in `docs/php/`. + +**Tests**: +- `tests/error_tests/callables.rs`: update the existing tests at lines 232, + 244, 257 that assert "must be passed a variable" — those pass non-lvalue + args (literals/calls) and must *still* error. Add new tests asserting + `$a[0]`, `$a['k']`, `$a[]`, `$a[$i]` are *accepted*. +- `tests/codegen/callables/by_ref_array_element.rs` (new): the issue program + plus nested arrays, append targets, and Mixed-element arrays. + +### Test plan +| # | PHP source | Expected | Covers | +|---|---|---|---| +| 1 | `function bump(&$x){$x++;} $a=[5]; bump($a[0]); echo $a[0];` | `6` | Issue #360 exact | +| 2 | `function bump(&$x){$x++;} $a=['k'=>5]; bump($a['k']); echo $a['k'];` | `6` | String key | +| 3 | `function bump(&$x){$x++;} $a=[1,2,3]; bump($a[1]); echo implode(',',$a);` | `1,3,3` | Mid-array element | +| 4 | `function bump(&$x){$x+=10;} $a=[[1],[2]]; bump($a[0][0]); echo $a[0][0];` | `11` | Nested array element | +| 5 | `function bump(&$x){$x++;} $a=[]; bump($a[]); echo $a[0];` | `1` | Append target (`$a[]`) | +| 6 | `function s(&$x){$x='changed';} $a=['k'=>'orig']; s($a['k']); echo $a['k'];` | `changed` | String value by-ref | +| 7 | `function add(&$arr,$v){$arr[]=$v;} $a=[1]; add($a,2); echo implode(',',$a);` | `1,2` | Array by-ref + append (existing path, lock in) | +| 8 | `function b(&$x){$x++;} b(5);` | error "must be passed a variable" | Non-lvalue still rejected | +| 9 | `function b(&$x){$x++;} b(foo());` | error | Call result rejected | +| 10 | `function b(&$x){$x++;} $a=[1]; b($a[0]+1);` | error | Binary op rejected | + +### Risk assessment +- **Highest risk** of the five issues: introduces a new EIR op and a new + codegen path for element-address arithmetic on two targets. Element byte + sizes (8 for scalars, 16 for TaggedScalar, cell-pointer indirection for + Mixed) must be exactly right per `PhpType::stack_size` and the array + header layout in `src/codegen/runtime/arrays/`. +- **Array grow during by-ref**: if the callee appends to the same array it + received an element address for, the array may reallocate and invalidate + the passed pointer. PHP does not guarantee stability here either (a by-ref + array param that grows can move the element), so matching PHP's "best + effort" is acceptable. Document. +- **Mixed-element address**: for `Array(Mixed)`, the element is a boxed + cell; passing the cell pointer is correct. For `AssocArray`, the hash + bucket's value cell address is needed; a runtime helper + `__rt_hash_element_addr(hash, key)` is the clean approach (mirror + `__rt_hash_iter_next`'s lookup). +- **Regression watch**: every existing by-ref test in + `tests/codegen/callables/**`, `tests/ir_backend_parity/cases.rs` (the + `parity_*_by_ref_*` tests at lines 1518, 1540, 1560), and + `tests/error_tests/callables.rs`. The frozen legacy backend must *not* be + touched; its by-ref path stays as-is (it may already support array + elements — verify parity but do not change it). +- **Runtime cache**: if a new runtime helper (`__rt_hash_element_addr`) is + added, `~/.cache/elephc/*.o` must be cleared before the next compile + (`rm -rf ~/.cache/elephc`). Note this in the PR. + +--- + +## Cross-cutting notes + +- **Order of implementation**: #340 and #377 are small, low-risk, and can + land first (regression locks + residual fixes). #384 is medium (optimizer + propagation change). #381 is small-medium (iterator state slot). #360 is + large (new EIR op + codegen path); land it last and consider splitting + into "Phase A checker accept" and "Phase B lowering" PRs so PHP programs + that only need the checker to stop erroring can progress sooner (Phase A + alone makes `bump($a[0])` *compile* but the mutation is still lost — + document this clearly). +- **Test policy**: each issue's tests span the required four surfaces + (lexer/parser/codegen/error) where applicable, plus an `examples/` entry + per AGENTS.md. Suggested examples: + - `examples/by_ref/main.php` exercising #384 and #360, + - `examples/foreach_snapshot/main.php` for #381, + - `examples/interpolation/main.php` for #340, + - `examples/type_widening_loop/main.php` for #377. +- **Docs**: update `docs/php/functions.md` (by-ref parameters), the + interpolation section of `docs/php/strings.md`, the foreach section of + `docs/php/control-structures.md`, and the optimizer section of + `docs/internals/the-optimizer.md` to describe intra-expression + write-invalidation. +- **CI**: rely on the sharded codegen matrix for the full ARM64/x86_64 + coverage; locally run only the focused filters named in each issue's test + plan during implementation. diff --git a/.plans/eir-00-overview.md b/.plans/eir-00-overview.md deleted file mode 100644 index 617260585d..0000000000 --- a/.plans/eir-00-overview.md +++ /dev/null @@ -1,162 +0,0 @@ -# elephc IR — Overview and Vision - -**Document version:** 2026-05-12 -**Author:** Architecture proposal for elephc -**Target series:** v0.24.x (introduction), v0.25.x+ (optimization passes) - ---- - -## Goal - -Introduce a **domain-specific intermediate representation** (called **EIR — elephc IR**) between the AST-level optimizer and the assembly emitter, so that: - -1. A real **register allocator** can see the entire function and avoid the per-expression spill/reload pattern that currently caps throughput. -2. **Instruction scheduling**, **CSE**, **LICM**, **peephole over wider windows**, and **inlining** become possible without retrofitting them onto an AST walker that has no notion of basic blocks or value identity. -3. The codegen pipeline gains a **clean boundary** between *semantic* lowering (AST → IR, preserves PHP semantics) and *physical* lowering (IR → ASM, preserves performance). -4. The educational character of the project is preserved: ASM is still hand-emitted, every instruction still commented at column 81, every emitter file still readable line-by-line. - -## Non-goals - -- **Do not** replace the hand-written ASM backend with a third-party crate (Cranelift, LLVM, etc.). EIR is ours. -- **Do not** redesign `PhpType`, the type checker, the parser, or the runtime. EIR consumes the existing semantic model. -- **Do not** rewrite the AST-level optimizer (`src/optimize/`) up front. AST-level folding, propagation, and DCE remain; IR-level optimizations are added on top, not in place. -- **Do not** ship register allocation in the first PR. The first deliverable is a 1:1 lowering with **zero behavior change**. -- **Do not** introduce a generic IR (SSA-CFG with abstract value semantics like LLVM/CLIF). EIR is **PHP-specific**: it has `MixedBox`, `ArrayCowEnsureUnique`, `Fatal`, ownership state — operations LLVM and Cranelift would never have. - -## Why a custom IR over Cranelift - -Decided already in the design discussion preceding these plans. Recap: - -- **Identity**: the project's value proposition is the educational, fully hand-rolled toolchain. Cranelift dissolves that. -- **PHP semantics**: Mixed boxing, COW, ownership lattice, exact eval order, fatal vs throw, `__rt_*` runtime calls — all hostile to a generic optimizing IR. They are first-class in EIR. -- **Migration cost**: Cranelift migration is 6–9 months of refactor work with no visible features. EIR Phase 1 alone is 4–6 weeks and unlocks subsequent optimization phases. -- **Reversibility**: if we ever want Cranelift, EIR → CLIF is a much smaller hop than AST → CLIF. - -## Architecture - -``` -PHP source - → Lexer - → Parser - → Magic constants - → Conditional compilation - → Resolver - → NameResolver - → Constant folding (AST) - → Type checker / warnings - → Optimizer passes (AST) ◄── unchanged - → AST → EIR lowering ◄── NEW - → EIR passes: - • validation - • effect annotation finalization - • (later) peephole / CSE / LICM / register allocation - → EIR → ASM emission ◄── replaces direct AST → ASM - → assembler / linker - → binary -``` - -Two new modules will be introduced under `src/`: - -- `src/ir/` — EIR types, builder, validator, printer, passes -- `src/codegen_ir/` — the new IR-consuming backend (renamed once stable; see Phase 5) - -The current `src/codegen/` keeps emitting assembly during the migration. A feature flag selects which pipeline runs. When EIR reaches parity, the legacy path is removed. - -## Tech stack - -- Rust (existing toolchain, no new dependencies) -- No external crates required for Phases 1–6 -- Insta or hand-rolled snapshot tests for IR pretty-printer (decide in Phase 02; prefer no new dep) -- Existing `as` + `ld` test infrastructure remains the final correctness gate - -## Phases and deliverables - -Each phase is **independently shippable**. After every phase the test suite must pass with `cargo test -- --include-ignored` and no regression in the benchmark harness. - -| Phase | Plan | Deliverable | Visible to users? | -|-------|------|-------------|-------------------| -| 1 | [01](eir-01-design-spec.md) | EIR design specification document | No | -| 2 | [02](eir-02-ir-module-skeleton.md) | `src/ir/` module: types, instructions, builder, validator, printer | No | -| 3 | [03](eir-03-ast-to-ir-lowering.md) | AST → EIR lowering, no optimizations, full test parity through `--emit-ir` | Yes (`--emit-ir` flag) | -| 4 | [04](eir-04-ir-to-asm-backend.md) | EIR → ASM backend producing equivalent assembly to current codegen | No (parity check only) | -| 5 | [05](eir-05-switchover-behind-flag.md) | `--ir-backend` flag, then default switchover, then legacy codegen removed | Yes (perf neutral) | -| 6 | [06](eir-06-linear-scan-register-allocator.md) | Linear-scan register allocator, first real perf gain | Yes (~15–25% perf on compute) | -| 7 | [07](eir-07-peephole-and-local-opts.md) | IR-level peephole, dead store elimination, identity ops | Yes (~5–10% more) | -| 8 | [08](eir-08-cse-licm-inlining.md) | CSE, LICM, inlining of small functions | Yes (~10–20% more on loops) | -| 9 | [09](eir-09-legacy-cleanup.md) | Remove legacy path, consolidate docs, finalize internals chapter | No (cleanup) | - -## Definition of done (per phase) - -- All existing tests pass (`cargo test`, `cargo test -- --include-ignored`) -- New tests cover the phase's added surface -- Benchmark harness shows no regression (or shows the expected gain for Phases 6–8) -- `cargo build` clean, zero warnings -- Linux x86_64 and Linux ARM64 verified via `scripts/test-linux-*.sh` -- `docs/internals/` updated where the change is user-visible internally (e.g., `the-codegen.md`, new `the-ir.md`) -- Commit history follows project conventions (`feat:`, `refactor:`, no `Co-Authored-By`) - -## Risks and mitigations - -| Risk | Likelihood | Mitigation | -|------|-----------|------------| -| EIR design too abstract / not PHP-shaped | High if rushed | Phase 01 must spec each instruction by walking 10+ real codegen sites | -| Phase 4 fails to produce byte-identical assembly | High | Phases 4 and 5 do *not* require byte-identical assembly, only semantically equivalent (same `compile_and_run` output). Snapshot tests on stdout, not on `.s` | -| Phase 3 PR becomes a 20k-line monster | Very high without discipline | Lower AST → IR per AST family in separate sub-tasks within Phase 3 (literals, locals, arithmetic, calls, control flow, classes, etc.) | -| Register allocator interacts badly with ABI helpers | Medium | Phase 6 starts with caller-saved-only scratch allocation, then expands. ABI helpers in `src/codegen/abi/` are reused, not reinvented | -| Ownership/refcount semantics drift during IR lowering | High | Encode ownership in EIR as explicit ops (`Acquire`/`Release`/`Move`/`Borrow`). Validator rejects unbalanced cleanup paths | -| Performance gain is smaller than predicted | Medium | Benchmark before/after every optimization phase. If Phase 6 alone delivers <10% on compute benchmarks, stop and diagnose before continuing to Phase 7 | -| Educational value lost to abstraction | Low if disciplined | EIR is *added* between AST and ASM; the ASM emitter retains every comment. New `docs/internals/the-ir.md` teaches IR. The pipeline story actually gets better | - -## Out of scope - -- Generic IR features (linear scan SSA destruction with phi nodes — we use block parameters instead) -- Cross-module optimization (we don't have separate compilation yet) -- Profile-guided optimization -- SIMD vectorization (later, post-1.0 if at all) -- A textual IR parser for round-tripping `.eir` files (printer is for tests/debug only, not bidirectional) -- WebAssembly backend (post-1.0 product track, see ROADMAP v1.2.x) - -## Relationship to existing optimizer - -`src/optimize/` operates on the AST and stays there. AST-level folding and propagation are good at: - -- Constant folding of pure arithmetic on literal subtrees -- Dead branch elimination in `if (false)` -- Reachability/control-flow normalization -- Alias-aware scalar propagation across statement boundaries - -These are kept. EIR-level optimizations target what AST-level can't see: - -- **Liveness** of values across an entire function -- **Value identity** for CSE (two `array_get %a, 0` calls produce the same SSA value; AST nodes are structural but not value-identified) -- **Basic-block dominance** for LICM -- **Register placement** for elimination of redundant moves and spills -- **Instruction scheduling** for pipelining - -## Relationship to ROADMAP - -- The external plan series was written against an older roadmap shape where register allocation, peephole optimization, inlining, tail-call optimization, and deeper DCE/propagation were standalone performance bullets. -- This repository's `ROADMAP.md` has already been reconciled: **v0.24.x** covers EIR introduction and register allocation, **v0.25.x** covers EIR optimization passes, and **v0.26.x** covers performance closure, legacy cleanup, and 0.x stabilization. -- Treat `ROADMAP.md` as the source of truth for release placement; treat these `.plans/eir-*` files as the execution detail behind those roadmap bullets. - -## Open design questions (decided here) - -These are decided in this proposal, not deferred: - -- **SSA form**: SSA-lite with block parameters. No phi nodes. Block params are easier to construct and lower than phi-based SSA, at no real cost for our scale. -- **Value naming**: `ValueId` is a u32 index into a per-function value table. Values are SSA: defined exactly once. -- **CFG representation**: Functions own a `Vec`, each block owns a `Vec` and one terminator. Blocks reference each other by `BlockId(u32)`. -- **Types in IR**: minimal — `I64`, `F64`, `Str`, `Heap`, `Void`. Heap subkind (`Array`/`Hash`/`Object`/`Mixed`/`Iterable`/`Union`) is carried as metadata on operations, not in the IR type itself, because the runtime handles them uniformly via heap headers. -- **Ownership**: tracked in EIR as explicit ops (`Acquire`, `Release`, `Move`, `Borrow`). The validator checks for balance along all paths. -- **PHP-specific ops**: yes, first-class. `MixedBox`, `ArrayCowEnsureUnique`, `Fatal(msg_idx)`, `RuntimeCall`, `BuiltinCall` are distinct from generic `Call`. -- **Effects**: each instruction carries effect bits (Pure, ReadsHeap, ReadsGlobal, ReadsFs, WritesHeap, WritesGlobal, WritesFs, MayThrow, MayFatal, MayDeoptimize). Set at builder time, refined by validator. - -## What success looks like - -After Phase 9: - -- The compiler still ships PHP-correct programs. -- Benchmarks show **40–70% performance improvement** on compute-heavy workloads (function calls, tight loops, arithmetic-heavy programs) compared to pre-EIR baseline. -- `docs/internals/` has a new chapter (`the-ir.md`) explaining EIR with the same pedagogical care as `the-codegen.md`. -- The codebase passes all gates: tests, ignored tests, Docker Linux tests, benchmark harness, zero compiler warnings. -- A future migration to Cranelift, if ever desired, is a 6–8 week project instead of 6–9 months — because EIR is the hard part. diff --git a/.plans/eir-01-design-spec.md b/.plans/eir-01-design-spec.md deleted file mode 100644 index ea0c8fb469..0000000000 --- a/.plans/eir-01-design-spec.md +++ /dev/null @@ -1,662 +0,0 @@ -# Phase 01 — EIR Design Specification - -> **For agentic workers:** This phase produces *documentation only*. No code is written. Phase 02 implements the spec defined here. - -**Goal:** Produce a complete written specification of elephc IR (EIR) — types, instructions, terminators, effects, ownership semantics, and validation rules — covering every existing AST node the codegen currently lowers. - -**Architecture:** SSA-form CFG with block parameters. PHP-specific instructions (Mixed boxing, COW, fatal, runtime calls) are first-class. Effects are explicit metadata, not implicit ordering. - -**Tech Stack:** Markdown. No code. Output is a single doc that Phase 02 implements verbatim. - ---- - -## File Structure - -The output of this phase is one document: - -- Create: `docs/internals/the-ir.md` — the canonical EIR specification (replaces nothing; new doc) - -This repository now has `docs/internals/the-ir.md` and a `docs/README.md` link. Later phases should update `docs/internals/the-codegen.md` when the backend transition becomes real. - ---- - -## Task 1: Specify EIR types - -**Files:** -- Create: `docs/internals/the-ir.md` (section: "Types") - -- [ ] **Step 1: Write the types section** - -```markdown -## Types - -EIR uses a minimal type lattice. Type-level distinctions that the runtime -treats uniformly (e.g., Array vs Hash, Object vs Mixed) are *not* separate -IR types — they are carried as metadata on operations. - -| EIR type | Storage | Maps from `PhpType` | -|----------|---------|---------------------| -| `I64` | 1 integer register | `Int`, `Bool`, `Pointer`, `Resource`, `Callable` | -| `F64` | 1 float register | `Float` | -| `Str` | pair `(ptr, len)`, 2 registers | `Str` | -| `Heap` | 1 integer register (pointer to heap header) | `Array(_)`, `AssocArray{..}`, `Object(_)`, `Mixed`, `Iterable`, `Union(_)`, `Buffer(_)` | -| `Void` | zero registers | `Void`, `Never` | - -Notes: -- `Bool` and `Int` share `I64` storage. The PHP-level distinction is preserved - via type metadata on the producing operation (`def_php_type`), not via a - separate IR type. This avoids per-operation duplication of arithmetic - opcodes. -- `Str` is two registers everywhere it appears, matching the existing - `(ptr, len)` ABI from `src/codegen/abi/registers.rs`. -- `Heap` is uniform pointer-to-header. The runtime's `__rt_decref_any` uses - the heap header to dispatch by kind. Operations that need the kind - (e.g., `ArrayGet` vs `HashGetStr`) take the kind as an immediate - attribute. - -PHP-level type information is preserved on each `Value`'s metadata -(`Value.php_type: PhpType`) for diagnostics, validator checks, and to -inform passes that care (e.g., `MixedBox` cannot apply to a value whose -PHP type is already `Mixed`). -``` - -- [ ] **Step 2: Self-review for completeness** - -Ensure each variant of `PhpType` (Int, Float, Str, Bool, Void, Never, Iterable, Mixed, Array, AssocArray, Buffer, Callable, Object, Packed, Pointer, Resource, Union) maps to exactly one EIR type. Cross-reference `src/types/model.rs`. - ---- - -## Task 2: Specify `Value`, `BasicBlock`, `Function`, `Module` - -**Files:** -- Modify: `docs/internals/the-ir.md` (append: "Module structure") - -- [ ] **Step 1: Write the module structure section** - -````markdown -## Module structure - -### `ValueId` and `Value` - -Values are SSA: each `ValueId` is defined exactly once. A `ValueId` is a -`u32` index into the owning function's value table. - -```rust -pub struct Value { - pub ir_type: IrType, - pub php_type: PhpType, - pub def: ValueDef, - pub ownership: Ownership, -} - -pub enum ValueDef { - BlockParam { block: BlockId, index: u16 }, - Instruction { block: BlockId, index: u32 }, -} - -pub enum Ownership { - NonHeap, // I64/F64/Void scalars; never need release - Owned, // refcounted; this value owns +1 refcount - Borrowed, // refcounted; this value does not own a refcount - MaybeOwned, // refcounted; ownership joins across CFG merges -} -``` - -`Ownership` mirrors `HeapOwnership` from `src/codegen/context.rs` but is -attached to *every* SSA value, not just locals. - -### `BasicBlock` - -```rust -pub struct BasicBlock { - pub id: BlockId, - pub params: Vec, // block parameters (SSA-lite) - pub instructions: Vec, // indices into function's instruction pool - pub terminator: Terminator, -} -``` - -Blocks have a single terminator at the end and may have parameters at the -top. Branch arguments carry SSA values into the destination block, -replacing phi nodes. - -### `Function` - -```rust -pub struct Function { - pub name: String, - pub params: Vec, - pub return_type: IrType, - pub return_php_type: PhpType, - pub blocks: Vec, - pub values: Vec, - pub instructions: Vec, - pub locals: Vec, // stack slots for PHP locals - pub entry: BlockId, - pub source_signature_ref: Option, - pub flags: FunctionFlags, // is_main, is_method, is_closure, etc. -} - -pub struct LocalSlot { - pub name: String, // PHP variable name; "" for synthetic - pub php_type: PhpType, - pub kind: LocalKind, // PhpVariable, Hidden, Static, Global -} -``` - -### `Module` - -```rust -pub struct Module { - pub functions: Vec, - pub class_methods: Vec, // flattened class methods - pub data: DataPool, // string literals, runtime tables - pub extern_decls: Vec, - pub target: Target, -} -``` - -A Module is one compilation unit. The runtime (`__rt_*` routines) is -*not* in the Module — it lives outside, exactly as today. -```` - -- [ ] **Step 2: Cross-check field names against existing types** - -Walk `src/types/`, `src/parser/ast/`, and `src/codegen/context.rs` and verify field names match the spelling already used (e.g., `php_type` matches `PhpType`, `class_id` matches `ClassInfo.class_id`). - ---- - -## Task 3: Specify the Instruction set - -**Files:** -- Modify: `docs/internals/the-ir.md` (append: "Instructions") - -This is the largest task. Specify every opcode by walking the existing codegen sites that produce it. Each opcode entry MUST include: - -- Operands and result type -- Effects (Pure / Reads* / Writes* / MayThrow / MayFatal / MayDeoptimize) -- Lowering target (which `__rt_*` routine or inline ASM pattern) -- AST node(s) that produce it - -- [ ] **Step 1: Write the literals/locals/globals section** - -```markdown -### Literals and locals - -| Op | Operands | Result | Effects | Lowers to | -|----|----------|--------|---------|-----------| -| `ConstI64(i64)` | — | `I64` | Pure | `mov reg, #imm` (or constant pool for large values) | -| `ConstF64(f64)` | — | `F64` | Pure | adr + ldr from data section | -| `ConstStr(string_id)` | — | `Str` | Pure | adr to label + immediate length | -| `ConstNull` | — | `I64` | Pure | `mov reg, #0` | -| `LoadLocal(slot_id)` | — | (slot.ir_type) | Reads(local) | `ldr reg, [x29, #-off]` | -| `StoreLocal(slot_id)` | val | `Void` | Writes(local) | `str reg, [x29, #-off]` | -| `LoadGlobal(name_id)` | — | (decl.ir_type) | Reads(global) | adr + ldr | -| `StoreGlobal(name_id)` | val | `Void` | Writes(global) | adr + str | -``` - -- [ ] **Step 2: Write the arithmetic/bitwise/comparison section** - -```markdown -### Scalar arithmetic and bitwise - -All scalar ops operate on `I64` or `F64` operands matching the op's domain. - -| Op | Operands | Result | Effects | Lowers to (ARM64) | -|----|----------|--------|---------|--------------------| -| `IAdd(a, b)` | I64, I64 | I64 | Pure | `add` | -| `ISub(a, b)` | I64, I64 | I64 | Pure | `sub` | -| `IMul(a, b)` | I64, I64 | I64 | Pure | `mul` | -| `ISDiv(a, b)` | I64, I64 | I64 | MayFatal (div by zero in PHP modes that fatal) | `sdiv` | -| `ISMod(a, b)` | I64, I64 | I64 | MayFatal | `sdiv`+`msub` | -| `INeg(a)` | I64 | I64 | Pure | `neg` | -| `IBitAnd/Or/Xor/Not(a,b)/a` | I64 (1 or 2) | I64 | Pure | `and`/`orr`/`eor`/`mvn` | -| `IShl(a, b)` | I64, I64 | I64 | Pure | `lsl` | -| `IShrA(a, b)` | I64, I64 | I64 | Pure | `asr` (PHP `>>` is arithmetic) | -| `FAdd/FSub/FMul/FDiv(a, b)` | F64, F64 | F64 | Pure | `fadd`/`fsub`/`fmul`/`fdiv` | -| `FNeg(a)` | F64 | F64 | Pure | `fneg` | -| `FPow(a, b)` | F64, F64 | F64 | Pure (libc) | `bl pow` | - -### Comparison - -| Op | Operands | Result | Effects | -|----|----------|--------|---------| -| `ICmp(predicate, a, b)` | I64, I64 | I64 (0/1) | Pure | -| `FCmp(predicate, a, b)` | F64, F64 | I64 (0/1) | Pure | -| `StrCmpEq(a, b)` | Str, Str | I64 (0/1) | Pure (calls `__rt_str_eq`) | -| `PhpLooseEq(a, b)` | any, any | I64 (0/1) | MayDeoptimize (object comparison may invoke __toString) | -| `PhpIdentical(a, b)` | any, any | I64 (0/1) | Pure (type-tag aware) | -| `Spaceship(a, b)` | any, any | I64 | as PhpLooseEq | - -`predicate` is `Eq`, `Ne`, `Slt`, `Sle`, `Sgt`, `Sge` for integers and the -float equivalents (`Olt`, ...) for floats, mapping to PHP's signed -comparison semantics. -``` - -- [ ] **Step 3: Write the conversion / cast section** - -```markdown -### Conversions - -| Op | From | To | Effects | Notes | -|----|------|----|---------|-------| -| `IToF(a)` | I64 | F64 | Pure | PHP int-to-float widening | -| `FToI(a)` | F64 | I64 | Pure | PHP float-to-int (truncate, PHP rules) | -| `IToStr(a)` | I64 | Str | AllocConcatBuf | calls `__rt_itoa` | -| `FToStr(a)` | F64 | Str | AllocConcatBuf | calls `__rt_ftoa` | -| `BoolToStr(a)` | I64 | Str | Pure | "" or "1" | -| `StrToI(a)` | Str | I64 | Pure | calls `__rt_str_to_int` | -| `StrToF(a)` | Str | F64 | Pure | calls `__rt_str_to_float` | -| `MixedBox(a)` | any (non-Mixed) | Heap | AllocHeap | tags value into Mixed cell | -| `MixedUnbox(a, expected_tag)` | Heap (Mixed) | I64/F64/Str/Heap | Pure + MayFatal | extracts payload | -| `MixedTagOf(a)` | Heap (Mixed) | I64 | Pure | returns tag | -| `Cast(a, to_php_type)` | any | matching IR type | as PHP cast | dispatches to specific helper | -``` - -- [ ] **Step 4: Write the string ops section** - -```markdown -### String operations - -| Op | Operands | Result | Effects | Lowers to | -|----|----------|--------|---------|-----------| -| `StrConcat(a, b)` | Str, Str | Str | AllocConcatBuf | `__rt_str_concat` | -| `StrLen(a)` | Str | I64 | Pure | inline `mov reg, len` | -| `StrCharAt(a, i)` | Str, I64 | Str (1-char) | MayFatal (oob), AllocConcatBuf | `__rt_str_char_at` | -| `StrPersist(a)` | Str | Str | AllocHeap (idempotent) | `__rt_str_persist` | -| `StrInterpolate(parts, vals)` | varargs | Str | AllocConcatBuf | builds in concat buf | -``` - -- [ ] **Step 5: Write the array/hash/object ops section** - -```markdown -### Array and hash - -The `kind` immediate distinguishes indexed Array, hash AssocArray, and -mixed-key payload. - -| Op | Operands | Result | Effects | Notes | -|----|----------|--------|---------|-------| -| `ArrayNew(kind, capacity)` | — | Heap | AllocHeap | calls `__rt_array_new` | -| `ArrayLen(a)` | Heap | I64 | Pure | inline header read | -| `ArrayGet(arr, idx)` | Heap, I64 | (element ir_type) | Reads(heap) + MayFatal? | `__rt_array_get_int` | -| `ArraySet(arr, idx, val)` | Heap, I64, any | Void | Writes(heap), AllocHeap (COW) | `__rt_array_set_int` | -| `ArrayPush(arr, val)` | Heap, any | Void | Writes(heap), AllocHeap | `__rt_array_push` | -| `ArrayCowEnsureUnique(arr)` | Heap | Heap | AllocHeap (maybe) | `__rt_array_cow_ensure` | -| `HashGetStr(h, key)` | Heap, Str | (value ir_type) | Reads(heap) + MayFatal? | `__rt_hash_get_str` | -| `HashGetInt(h, key)` | Heap, I64 | (value ir_type) | Reads(heap) + MayFatal? | `__rt_hash_get_int` | -| `HashSetStr/Int(h, k, v)` | Heap, key, val | Void | Writes(heap), AllocHeap | `__rt_hash_set_*` | -| `HashKeyExists(h, k)` | Heap, key | I64 | Reads(heap) | `__rt_hash_exists_*` | -| `IterNext(iter)` | Heap | (key, value) | Reads(heap), MayDeoptimize (Iterator::next) | `__rt_iter_next` | -| `IterCurrent(iter)` | Heap | (key, value) | Reads(heap) | `__rt_iter_current` | - -### Object - -| Op | Operands | Result | Effects | Notes | -|----|----------|--------|---------|-------| -| `ObjectNew(class_id)` | — | Heap | AllocHeap | `__rt_object_alloc` then constructor call | -| `PropGet(obj, offset, ir_type)` | Heap, immediate | ir_type | Reads(heap) | inline `ldr` | -| `PropSet(obj, offset, val)` | Heap, immediate, any | Void | Writes(heap) | inline `str` + retain handling | -| `VTableLookup(obj, method_id)` | Heap | I64 (fn ptr) | Reads(heap) | reads class header | -| `InstanceOf(obj, class_id)` | Heap | I64 | Reads(heap) | `__rt_instanceof` | -``` - -- [ ] **Step 6: Write the calls section** - -```markdown -### Calls - -| Op | Operands | Result | Effects | Notes | -|----|----------|--------|---------|-------| -| `Call(func_id, args)` | varargs | (sig return) | per callee | user-defined PHP call | -| `IndirectCall(fn_ptr, sig, args)` | varargs | (sig return) | MayDeoptimize | closure/callable | -| `MethodCall(obj, method_id, args)` | Heap + varargs | (sig return) | per callee | virtual dispatch | -| `BuiltinCall(builtin, args)` | varargs | (builtin return) | per builtin (from `src/optimize/effects/builtins.rs`) | inline-emit | -| `RuntimeCall(rt_routine, args)` | varargs | (rt return) | per routine | `__rt_*` | -| `ExternCall(name, args)` | varargs | (extern return) | per FFI sig | direct C call | -``` - -Each call instruction declares its **effect summary** at lowering time -from the existing `src/optimize/effects/` analysis. The IR keeps the -summary; passes do not re-derive it. - -- [ ] **Step 7: Write the ownership/control-flow section** - -```markdown -### Ownership operations - -These operations are *explicit* in the IR. The AST → IR builder inserts -them. The validator checks balance along every path. The register -allocator must not eliminate them; later passes may move them. - -| Op | Operands | Result | Effects | Lowers to | -|----|----------|--------|---------|-----------| -| `Acquire(a)` | refcounted | Void | Writes(refcount) | `bl __rt_incref` | -| `Release(a)` | refcounted | Void | Writes(refcount), MayFatal (debug heap) | `bl __rt_decref_any` | -| `Move(a)` | any | (same type) | Pure (transfers ownership) | no-op at codegen, validator-only | -| `Borrow(a)` | refcounted | (same type) | Pure | no-op at codegen, validator-only | - -### Terminators - -Every basic block ends with exactly one terminator. - -| Term | Operands | Notes | -|------|----------|-------| -| `Br(target, args)` | block, list | unconditional branch | -| `CondBr(cond, then, then_args, else_, else_args)` | I64 cond | conditional | -| `Switch(scrutinee, cases, default)` | I64 | jump table or chain | -| `Return(value?)` | optional value | function epilogue | -| `Throw(value)` | Heap (exception object) | longjmp via `__rt_throw` | -| `Fatal(msg_id)` | immediate | unrecoverable error path | -| `Unreachable` | — | provably unreachable (after `never`-typed call) | -``` - -- [ ] **Step 8: Self-review the instruction set** - -Walk every file under `src/codegen/expr/` and `src/codegen/stmt/`. For each emission helper, ask "which EIR ops does this lower to?" — if you cannot answer, the IR is missing something or the helper is doing too much (which may be a refactor finding). Make a checklist of files visited. - -Files to walk (minimum): -- `src/codegen/expr/scalars.rs` -- `src/codegen/expr/binops/arithmetic.rs` -- `src/codegen/expr/binops/comparison.rs` -- `src/codegen/expr/binops/array_union.rs` -- `src/codegen/expr/calls.rs` and `src/codegen/expr/calls/args.rs` -- `src/codegen/expr/arrays.rs` (and sub-dir) -- `src/codegen/expr/objects.rs` (and sub-dir) -- `src/codegen/expr/chains.rs` -- `src/codegen/expr/assignment.rs` -- `src/codegen/expr/variables.rs` -- `src/codegen/expr/ternary.rs` -- `src/codegen/expr/compare/` -- `src/codegen/expr/coerce.rs` -- `src/codegen/expr/helpers.rs` -- `src/codegen/expr/ownership.rs` -- `src/codegen/stmt/` (all) -- `src/codegen/builtins/` (sample 5 categories: strings, arrays, math, io, oop) -- `src/codegen/runtime/` (sample 5 categories) - ---- - -## Task 4: Specify effect lattice - -**Files:** -- Modify: `docs/internals/the-ir.md` (append: "Effects") - -- [ ] **Step 1: Write the effects section** - -```markdown -## Effects - -Each EIR instruction carries an immutable `Effects` bitset assigned at -construction time. Effects model what an instruction *may* do: - -```rust -bitflags! { - pub struct Effects: u16 { - const READS_LOCAL = 0b0000_0000_0000_0001; - const READS_HEAP = 0b0000_0000_0000_0010; - const READS_GLOBAL = 0b0000_0000_0000_0100; - const READS_FS = 0b0000_0000_0000_1000; - const WRITES_LOCAL = 0b0000_0000_0001_0000; - const WRITES_HEAP = 0b0000_0000_0010_0000; - const WRITES_GLOBAL = 0b0000_0000_0100_0000; - const WRITES_FS = 0b0000_0000_1000_0000; - const ALLOC_HEAP = 0b0000_0001_0000_0000; - const ALLOC_CONCAT = 0b0000_0010_0000_0000; - const MAY_THROW = 0b0000_0100_0000_0000; - const MAY_FATAL = 0b0000_1000_0000_0000; - const MAY_DEOPT = 0b0001_0000_0000_0000; - const REFCOUNT_OP = 0b0010_0000_0000_0000; - } -} - -impl Effects { - pub const PURE: Effects = Effects::empty(); - pub fn is_pure(&self) -> bool { self.is_empty() } - pub fn may_observe(&self) -> bool { - self.intersects( - Effects::READS_LOCAL | Effects::READS_HEAP | Effects::READS_GLOBAL - | Effects::READS_FS | Effects::ALLOC_HEAP - | Effects::MAY_THROW | Effects::MAY_FATAL | Effects::MAY_DEOPT - ) - } - pub fn may_mutate(&self) -> bool { - self.intersects( - Effects::WRITES_LOCAL | Effects::WRITES_HEAP | Effects::WRITES_GLOBAL - | Effects::WRITES_FS | Effects::REFCOUNT_OP - ) - } -} -``` - -Sources of effect data for the builder: -- Arithmetic / comparison / scalar ops — hardcoded -- BuiltinCall — looked up from `src/optimize/effects/builtins.rs` -- Call / MethodCall — derived from `FunctionSig` purity flags, falling - back to "all effects" when unknown -- ExternCall — always conservative (`READS_HEAP | WRITES_HEAP | MAY_THROW`) -- RuntimeCall — declared in a per-routine table maintained in the IR - module - -The validator checks that operations with `MAY_DEOPT` are not silently -reordered across `ALLOC_HEAP` instructions before passes that don't -preserve allocation order. -``` - ---- - -## Task 5: Specify validator rules - -**Files:** -- Modify: `docs/internals/the-ir.md` (append: "Validator") - -- [ ] **Step 1: Write the validator rules section** - -```markdown -## Validator - -The validator runs after every IR pass (cheap mode: structural; -expensive mode: ownership + dominance). Failures are *bugs*, not -diagnostics — they abort compilation. - -### Structural rules - -1. Every basic block ends with exactly one terminator. The terminator - appears nowhere else in the block. -2. Every `ValueId` is defined exactly once. -3. Every use of a `ValueId` either (a) follows its definition in the - same block, or (b) the defining block dominates the using block. -4. Block parameter counts at the destination match the argument counts - in every incoming branch. -5. `IrType` matches between definition and every use. -6. `entry` block has no parameters (function parameters are loaded via - `LoadLocal` in the entry block). - -### Ownership rules - -1. Every `Owned` value reaches exactly one consuming op (`Release`, - `Move`, or `Return`) along every CFG path from its definition. -2. `Borrow` does not increase or decrease refcount; the source must - outlive the borrowed value. -3. At every CFG merge, ownership states from incoming edges must be - compatible: identical states merge to themselves; `Owned` + `Owned` - = `Owned`; `Borrowed` + `Borrowed` = `Borrowed`; mixed = `MaybeOwned` - (the validator emits a runtime branch in lowering). -4. `Return` of an `Owned` value transfers ownership to the caller; - `Return` of `Borrowed` requires an explicit prior `Acquire`. - -### Effect rules - -1. `Pure` operations may not have side-effect dependencies via memory. -2. Operations with `MAY_FATAL` define a control-flow effect: passes that - reorder them must not move them past observable operations. -3. `ALLOC_CONCAT` operations are sensitive to PHP's concat-buffer reuse - policy: per-statement reset is preserved by the lowering, and passes - must not reorder concat ops across statement boundaries. -``` - -- [ ] **Step 2: Self-review** - -Verify ownership rules against `src/codegen/expr/ownership.rs` and `src/codegen/context.rs::HeapOwnership`. Adjust rules if the actual codegen has cases not covered (e.g., container propagation, foreach values). - ---- - -## Task 6: Specify the textual format - -**Files:** -- Modify: `docs/internals/the-ir.md` (append: "Textual format") - -- [ ] **Step 1: Write the textual format section** - -````markdown -## Textual format - -EIR has a printable textual format used for snapshot tests, `--emit-ir`, -and debugging. It is **printer-only** — no parser. Format does not need -to be machine-readable, only human-readable. - -Example: - -```eir -function add_pair(p0: I64, p1: I64) -> I64 { - entry: - v0 = const_i64 0 - store_local slot[0] "result", v0 - v1 = load_local slot[0] - v2 = iadd v1, p0 ; effects: pure - store_local slot[0], v2 - v3 = load_local slot[0] - v4 = iadd v3, p1 - store_local slot[0], v4 - v5 = load_local slot[0] - return v5 -} - -function map_word_count(p0: Str) -> Heap[Hash] { - entry: - v0 = builtin_call explode " ", p0 ; effects: alloc_heap, alloc_concat - own v0 - v1 = hash_new ; effects: alloc_heap - own v1 - br loop(v0, 0_i64, v1) - - loop(arr: Heap[Array], i: I64, acc: Heap[Hash]): - v2 = array_len arr - v3 = icmp slt i, v2 - cond_br v3, body(arr, i, acc), exit(acc) - - body(arr: Heap[Array], i: I64, acc: Heap[Hash]): - v4 = array_get arr, i ; effects: reads_heap, may_fatal - borrow v4 - ; ... omitted ... - v5 = iadd i, 1_i64 - br loop(arr, v5, acc) - - exit(acc: Heap[Hash]): - release v0 ; arr no longer needed - return acc ; transfer ownership -} -``` - -Notes: -- Function header shows IR type. `Heap[Hash]` displays subkind metadata. -- Block params use `name: Type` after `block_name(`. -- Branch arguments use the destination's param order. -- `own`, `borrow`, `release` are the ownership ops. -- Effects, when displayed, appear as `; effects: ...` comments. -```` - ---- - -## Task 7: Cross-reference Lowering catalogue - -**Files:** -- Modify: `docs/internals/the-ir.md` (append: "AST → EIR lowering catalogue") - -- [ ] **Step 1: Write the lowering catalogue** - -For every `ExprKind` and `StmtKind` variant in `src/parser/ast/expr.rs` and `src/parser/ast/stmt.rs`, give a one-paragraph lowering recipe. Example entries: - -```markdown -### `ExprKind::BinaryOp { op: Add, lhs, rhs }` - -Lower `lhs` and `rhs` recursively. Their result types determine the op: - -- Both `I64` → `IAdd` -- Both `F64` → `FAdd` -- Mixed → coerce via `IToF` on the int side, then `FAdd` -- `Str` + `Str` → `StrConcat` (PHP `.` is a separate op; `+` on strings - is an array union or coerced numeric add depending on operand types, - per PHP rules) - -### `StmtKind::Foreach { iter, key, value, body }` - -Lower `iter` → `Heap` value. Insert `IterStart`. Loop block reads -`IterCurrent`, binds key/value to locals, runs the body, then -`IterNext` and branches back. Exit block runs `IterEnd`. Owned-vs- -borrowed semantics follow PHP foreach rules (by-value vs by-ref). -``` - -The full catalogue is mandatory and lists every variant. Step 1 produces the catalogue document; Step 2 is the review. - -- [ ] **Step 2: Validate completeness against AST** - -Run: -```bash -grep -E "^[[:space:]]*[A-Z][A-Za-z0-9_]*[ {,(]" src/parser/ast/expr.rs src/parser/ast/stmt.rs -``` -Cross-check every variant has an entry. Missing entries are plan failures. - ---- - -## Task 8: Specify how `--emit-ir` works - -**Files:** -- Modify: `docs/internals/the-ir.md` (append: "CLI surface") - -- [ ] **Step 1: Write the CLI section** - -```markdown -## CLI - -`--emit-ir` (added in Phase 03) prints the EIR for the compiled program -to stdout and exits without invoking the assembler. Useful for debugging -and for snapshot tests. - -`--ir-backend` (added in Phase 05) selects the IR pipeline; default off -during Phases 03–04, default on at the end of Phase 05, removed in -Phase 09 when the legacy backend is deleted. -``` - ---- - -## Task 9: Self-review and commit - -- [ ] **Step 1: Read the document end-to-end** - -Check for: -- Placeholders, "TBD", "fill in later" -- Field-name inconsistencies (e.g., `ir_type` vs `irType`) -- Op names that overlap or duplicate -- Missing AST variants in the lowering catalogue -- Missing effects on instructions - -Fix inline. - -- [ ] **Step 2: Commit** - -```bash -git add docs/internals/the-ir.md -git commit -m "docs: introduce EIR design specification (phase 01)" -``` - ---- - -## Exit criteria - -- `docs/internals/the-ir.md` exists and is complete -- Every `ExprKind` and `StmtKind` has a lowering entry -- Every EIR op has effects, operands, result type, and a lowering target -- Validator rules cover SSA, ownership, and effects -- Textual format example renders a non-trivial function correctly -- No "TBD" / "TODO" / placeholder text anywhere diff --git a/.plans/eir-02-ir-module-skeleton.md b/.plans/eir-02-ir-module-skeleton.md deleted file mode 100644 index 82571d396c..0000000000 --- a/.plans/eir-02-ir-module-skeleton.md +++ /dev/null @@ -1,1741 +0,0 @@ -# Phase 02 — IR Module Skeleton - -> **For agentic workers:** Implement the data structures defined in Phase 01. No AST → IR lowering yet; no codegen consumer yet. The validator and printer are testable on hand-built `Module` instances. This phase produces no user-visible behavior change. - -**Goal:** Implement `src/ir/` containing types, instructions, values, blocks, functions, modules, a builder API, a validator, and a textual printer. Cover with unit tests. - -**Architecture:** Pure data structures. No interaction with the rest of the compiler. The next phases consume this module. - -**Tech Stack:** Rust. One new dependency: `bitflags` (already an indirect dep of common crates; verify with `cargo tree` before adding). - ---- - -## File Structure - -All new files under `src/ir/`: - -- Create: `src/ir/mod.rs` — module root, re-exports -- Create: `src/ir/types.rs` — `IrType`, `IrHeapKind`, conversions from `PhpType` -- Create: `src/ir/value.rs` — `ValueId`, `Value`, `Ownership`, value table -- Create: `src/ir/instr.rs` — `Instruction`, opcode enum (`Op`), `InstId` -- Create: `src/ir/effects.rs` — `Effects` bitset -- Create: `src/ir/block.rs` — `BasicBlock`, `BlockId`, `Terminator` -- Create: `src/ir/function.rs` — `Function`, `LocalSlot`, `LocalKind`, `FunctionParam`, `FunctionFlags` -- Create: `src/ir/module.rs` — `Module`, `DataPool`, `ExternDecl` -- Create: `src/ir/builder.rs` — `Builder` API for constructing IR by hand (used by tests now, by Phase 03 lowering later) -- Create: `src/ir/validator.rs` — structural + ownership + effect checks -- Create: `src/ir/print.rs` — textual format printer -- Create: `src/ir/tests/mod.rs` — unit tests -- Create: `src/ir/tests/print_test.rs` — snapshot-style tests for printer -- Create: `src/ir/tests/validator_test.rs` — positive and negative validator cases -- Create: `src/ir/tests/builder_test.rs` — build a couple of hand-rolled functions -- Modify: `src/lib.rs` — add `pub mod ir;` -- Modify: `src/main.rs` — add `mod ir;` for the binary crate module tree - ---- - -## Task 1: Wire up the module - -**Files:** -- Create: `src/ir/mod.rs` -- Modify: `src/lib.rs` -- Modify: `src/main.rs` - -- [ ] **Step 1: Create `src/ir/mod.rs` with the module preamble and re-exports** - -```rust -//! Purpose: -//! Defines elephc IR (EIR), a CFG-based SSA-lite intermediate representation -//! used between AST-level optimization and assembly emission. -//! -//! Called from: -//! - Phase 03: `crate::ir::builder` from a new AST → EIR lowering pass -//! - Phase 04: a new `src/codegen_ir/` backend consuming `Module` -//! -//! Key details: -//! - Block parameters replace SSA phi nodes; ownership is explicit; effects -//! are immutable bitset metadata. See `docs/internals/the-ir.md`. - -mod block; -mod builder; -mod effects; -mod function; -mod instr; -mod module; -mod print; -mod types; -mod validator; -mod value; - -#[cfg(test)] -mod tests; - -pub use block::{BasicBlock, BlockId, Terminator}; -pub use builder::Builder; -pub use effects::Effects; -pub use function::{Function, FunctionFlags, FunctionParam, LocalKind, LocalSlot}; -pub use instr::{InstId, Instruction, Op}; -pub use module::{DataPool, ExternDecl, Module}; -pub use print::print_module; -pub use types::{IrHeapKind, IrType}; -pub use validator::{validate_function, validate_module, ValidationError}; -pub use value::{Ownership, Value, ValueDef, ValueId}; -``` - -- [ ] **Step 2: Add module to crate root** - -```rust -// src/lib.rs — add this line in alphabetical order with existing pub mod entries -pub mod ir; -``` - -- [ ] **Step 3: Add the binary module declaration** - -```rust -// src/main.rs — add this line near the other module declarations -mod ir; -``` - -- [ ] **Step 4: Build to verify the empty skeleton compiles** - -Run: `cargo build` -Expected: `error[E0583]: file not found for module 'types'` (and others) — this confirms the module is wired up; the missing files come next. - -- [ ] **Step 5: Add stub files so the build proceeds** - -Create each of `types.rs`, `value.rs`, `instr.rs`, `effects.rs`, `block.rs`, `function.rs`, `module.rs`, `builder.rs`, `validator.rs`, `print.rs` with only the module preamble and no items: - -```rust -//! Purpose: -//! -//! -//! Called from: -//! - `crate::ir` -//! -//! Key details: -//! - Implemented in phase 02. -``` - -Run: `cargo build` -Expected: clean (with warnings for unused imports in `mod.rs`, acceptable for now) - -- [ ] **Step 6: Commit** - -```bash -git add src/ir/ src/lib.rs src/main.rs -git commit -m "feat(ir): scaffold src/ir/ module skeleton" -``` - ---- - -## Task 2: Implement `IrType` and `IrHeapKind` - -**Files:** -- Modify: `src/ir/types.rs` -- Test: `src/ir/tests/types_test.rs` - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir/tests/types_test.rs -use crate::ir::{IrHeapKind, IrType}; -use crate::types::PhpType; - -#[test] -fn maps_int_to_i64() { - assert_eq!(IrType::from_php(&PhpType::Int), IrType::I64); -} - -#[test] -fn maps_float_to_f64() { - assert_eq!(IrType::from_php(&PhpType::Float), IrType::F64); -} - -#[test] -fn maps_str_to_str() { - assert_eq!(IrType::from_php(&PhpType::Str), IrType::Str); -} - -#[test] -fn maps_bool_to_i64() { - assert_eq!(IrType::from_php(&PhpType::Bool), IrType::I64); -} - -#[test] -fn maps_array_int_to_heap_array() { - let php_ty = PhpType::Array(Box::new(PhpType::Int)); - assert_eq!(IrType::from_php(&php_ty), IrType::Heap(IrHeapKind::Array)); -} - -#[test] -fn maps_mixed_to_heap_mixed() { - assert_eq!(IrType::from_php(&PhpType::Mixed), IrType::Heap(IrHeapKind::Mixed)); -} - -#[test] -fn register_count_matches_php_type() { - assert_eq!(IrType::I64.register_count(), 1); - assert_eq!(IrType::F64.register_count(), 1); - assert_eq!(IrType::Str.register_count(), 2); - assert_eq!(IrType::Heap(IrHeapKind::Array).register_count(), 1); - assert_eq!(IrType::Void.register_count(), 0); -} -``` - -Add `mod types_test;` to `src/ir/tests/mod.rs` (creating the file if needed): - -```rust -// src/ir/tests/mod.rs -mod types_test; -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::types_test` -Expected: `error: cannot find type IrType` etc. - -- [ ] **Step 3: Implement `IrType`** - -```rust -// src/ir/types.rs -//! Purpose: -//! Defines the EIR type lattice and conversions from `PhpType`. -//! -//! Called from: -//! - `crate::ir::value`, `crate::ir::builder`, lowering passes (phase 03) -//! -//! Key details: -//! - Heap subkind is metadata on `IrType::Heap`; runtime treats heap values -//! uniformly via `__rt_decref_any`. - -use crate::types::PhpType; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum IrType { - I64, - F64, - Str, - Heap(IrHeapKind), - Void, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum IrHeapKind { - Array, - Hash, - Object, - Mixed, - Iterable, - Union, - Buffer, -} - -impl IrType { - pub fn from_php(php: &PhpType) -> Self { - match php { - PhpType::Int | PhpType::Bool | PhpType::Pointer(_) - | PhpType::Resource(_) | PhpType::Callable => IrType::I64, - PhpType::Float => IrType::F64, - PhpType::Str => IrType::Str, - PhpType::Void | PhpType::Never => IrType::Void, - PhpType::Array(_) => IrType::Heap(IrHeapKind::Array), - PhpType::AssocArray { .. } => IrType::Heap(IrHeapKind::Hash), - PhpType::Object(_) | PhpType::Packed(_) => IrType::Heap(IrHeapKind::Object), - PhpType::Mixed => IrType::Heap(IrHeapKind::Mixed), - PhpType::Iterable => IrType::Heap(IrHeapKind::Iterable), - PhpType::Union(_) => IrType::Heap(IrHeapKind::Union), - PhpType::Buffer(_) => IrType::Heap(IrHeapKind::Buffer), - } - } - - pub fn register_count(&self) -> usize { - match self { - IrType::I64 | IrType::F64 | IrType::Heap(_) => 1, - IrType::Str => 2, - IrType::Void => 0, - } - } - - pub fn is_refcounted(&self) -> bool { - matches!(self, IrType::Heap(_)) - } - - pub fn is_float(&self) -> bool { - matches!(self, IrType::F64) - } -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::types_test` -Expected: all tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir/types.rs src/ir/tests/types_test.rs src/ir/tests/mod.rs -git commit -m "feat(ir): implement IrType and IrHeapKind" -``` - ---- - -## Task 3: Implement `Ownership` and `Value` - -**Files:** -- Modify: `src/ir/value.rs` -- Test: `src/ir/tests/value_test.rs` - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir/tests/value_test.rs -use crate::ir::{IrType, Ownership, Value, ValueDef, ValueId}; -use crate::types::PhpType; - -#[test] -fn ownership_merge_same_state() { - assert_eq!(Ownership::Owned.merge(Ownership::Owned), Ownership::Owned); - assert_eq!(Ownership::Borrowed.merge(Ownership::Borrowed), Ownership::Borrowed); -} - -#[test] -fn ownership_merge_distinct_states_yields_maybe_owned() { - assert_eq!(Ownership::Owned.merge(Ownership::Borrowed), Ownership::MaybeOwned); -} - -#[test] -fn ownership_for_php_type_int_is_nonheap() { - assert_eq!(Ownership::for_php_type(&PhpType::Int), Ownership::NonHeap); -} - -#[test] -fn ownership_for_php_type_array_starts_maybe_owned() { - let ty = PhpType::Array(Box::new(PhpType::Int)); - assert_eq!(Ownership::for_php_type(&ty), Ownership::MaybeOwned); -} - -#[test] -fn value_id_is_zero_indexed() { - let v = ValueId::from_raw(0); - assert_eq!(v.as_raw(), 0); -} -``` - -Add `mod value_test;` to `src/ir/tests/mod.rs`. - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::value_test` -Expected: undefined symbols. - -- [ ] **Step 3: Implement** - -```rust -// src/ir/value.rs -//! Purpose: -//! Defines SSA-style `ValueId`, the owning `Value`, and the `Ownership` lattice -//! mirroring `crate::codegen::context::HeapOwnership`. -//! -//! Called from: -//! - `crate::ir::function`, `crate::ir::builder`, `crate::ir::validator` -//! -//! Key details: -//! - Each `ValueId` is defined exactly once. -//! - Ownership lattice tracks heap retention across SSA values, not just locals. - -use crate::ir::{block::BlockId, types::IrType}; -use crate::types::PhpType; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ValueId(u32); - -impl ValueId { - pub fn from_raw(raw: u32) -> Self { Self(raw) } - pub fn as_raw(self) -> u32 { self.0 } -} - -#[derive(Debug, Clone)] -pub struct Value { - pub ir_type: IrType, - pub php_type: PhpType, - pub def: ValueDef, - pub ownership: Ownership, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValueDef { - BlockParam { block: BlockId, index: u16 }, - Instruction { block: BlockId, index: u32 }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Ownership { - NonHeap, - Owned, - Borrowed, - MaybeOwned, -} - -impl Ownership { - pub fn for_php_type(ty: &PhpType) -> Self { - if ty.is_refcounted() || matches!(ty, PhpType::Str) { - Ownership::MaybeOwned - } else { - Ownership::NonHeap - } - } - - pub fn merge(self, other: Self) -> Self { - use Ownership::*; - match (self, other) { - (NonHeap, NonHeap) => NonHeap, - (Owned, Owned) => Owned, - (Borrowed, Borrowed) => Borrowed, - (MaybeOwned, _) | (_, MaybeOwned) => MaybeOwned, - (Owned, Borrowed) | (Borrowed, Owned) => MaybeOwned, - (NonHeap, x) | (x, NonHeap) => x, - } - } -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::value_test` -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir/value.rs src/ir/tests/value_test.rs src/ir/tests/mod.rs -git commit -m "feat(ir): implement Value, ValueId, Ownership" -``` - ---- - -## Task 4: Implement `Effects` - -**Files:** -- Modify: `Cargo.toml` (add `bitflags = "2"` only if not already present) -- Modify: `src/ir/effects.rs` -- Test: `src/ir/tests/effects_test.rs` - -- [ ] **Step 1: Check whether `bitflags` is already a dependency** - -Run: `cargo tree | grep bitflags` -If present and version >= 2.0, skip adding to Cargo.toml. If absent, add to `[dependencies]` in `Cargo.toml`: - -```toml -bitflags = "2" -``` - -- [ ] **Step 2: Write the failing test** - -```rust -// src/ir/tests/effects_test.rs -use crate::ir::Effects; - -#[test] -fn pure_has_no_bits() { - assert!(Effects::PURE.is_empty()); - assert!(Effects::PURE.is_pure()); -} - -#[test] -fn reads_and_writes_are_orthogonal() { - let r = Effects::READS_HEAP; - let w = Effects::WRITES_HEAP; - assert!(r.may_observe()); - assert!(!r.may_mutate()); - assert!(w.may_mutate()); - assert!(!w.may_observe()); -} - -#[test] -fn combined_effects_compose() { - let e = Effects::READS_HEAP | Effects::MAY_FATAL; - assert!(e.contains(Effects::READS_HEAP)); - assert!(e.contains(Effects::MAY_FATAL)); -} -``` - -Add `mod effects_test;` to `src/ir/tests/mod.rs`. - -- [ ] **Step 3: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::effects_test` -Expected: undefined symbols. - -- [ ] **Step 4: Implement** - -```rust -// src/ir/effects.rs -//! Purpose: -//! Defines the immutable per-instruction effect bitset used by IR-level passes. -//! -//! Called from: -//! - `crate::ir::instr` and every pass over IR instructions -//! -//! Key details: -//! - Effects are assigned at builder time and are not mutated by subsequent -//! passes. They are inferred conservatively for unknown calls. - -use bitflags::bitflags; - -bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct Effects: u16 { - const READS_LOCAL = 1 << 0; - const READS_HEAP = 1 << 1; - const READS_GLOBAL = 1 << 2; - const READS_FS = 1 << 3; - const WRITES_LOCAL = 1 << 4; - const WRITES_HEAP = 1 << 5; - const WRITES_GLOBAL = 1 << 6; - const WRITES_FS = 1 << 7; - const ALLOC_HEAP = 1 << 8; - const ALLOC_CONCAT = 1 << 9; - const MAY_THROW = 1 << 10; - const MAY_FATAL = 1 << 11; - const MAY_DEOPT = 1 << 12; - const REFCOUNT_OP = 1 << 13; - } -} - -impl Effects { - pub const PURE: Effects = Effects::empty(); - - pub fn is_pure(&self) -> bool { self.is_empty() } - - pub fn may_observe(&self) -> bool { - self.intersects( - Effects::READS_LOCAL | Effects::READS_HEAP | Effects::READS_GLOBAL - | Effects::READS_FS | Effects::ALLOC_HEAP - | Effects::MAY_THROW | Effects::MAY_FATAL | Effects::MAY_DEOPT, - ) - } - - pub fn may_mutate(&self) -> bool { - self.intersects( - Effects::WRITES_LOCAL | Effects::WRITES_HEAP | Effects::WRITES_GLOBAL - | Effects::WRITES_FS | Effects::REFCOUNT_OP, - ) - } -} -``` - -- [ ] **Step 5: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::effects_test` -Expected: pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/ir/effects.rs src/ir/tests/effects_test.rs src/ir/tests/mod.rs Cargo.toml Cargo.lock -git commit -m "feat(ir): implement Effects bitset" -``` - ---- - -## Task 5: Implement `BlockId`, `BasicBlock`, `Terminator` - -**Files:** -- Modify: `src/ir/block.rs` -- Test: `src/ir/tests/block_test.rs` - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir/tests/block_test.rs -use crate::ir::{BasicBlock, BlockId, Terminator, ValueId}; - -#[test] -fn block_id_is_zero_indexed() { - assert_eq!(BlockId::from_raw(0).as_raw(), 0); - assert_eq!(BlockId::from_raw(42).as_raw(), 42); -} - -#[test] -fn terminator_return_with_value() { - let term = Terminator::Return(Some(ValueId::from_raw(7))); - if let Terminator::Return(Some(v)) = term { - assert_eq!(v.as_raw(), 7); - } else { - panic!("expected Return"); - } -} - -#[test] -fn block_construction_records_params_and_terminator() { - let block = BasicBlock { - id: BlockId::from_raw(0), - params: vec![ValueId::from_raw(0)], - instructions: vec![], - terminator: Terminator::Return(None), - }; - assert_eq!(block.params.len(), 1); - assert!(matches!(block.terminator, Terminator::Return(None))); -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::block_test` -Expected: undefined symbols. - -- [ ] **Step 3: Implement** - -```rust -// src/ir/block.rs -//! Purpose: -//! Defines basic blocks, terminators, and the typed block identifier. -//! -//! Called from: -//! - `crate::ir::function`, `crate::ir::builder`, `crate::ir::validator` -//! -//! Key details: -//! - Block parameters replace SSA phi nodes; every block has exactly one -//! terminator at the end. - -use crate::ir::instr::InstId; -use crate::ir::value::ValueId; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub struct BlockId(u32); - -impl BlockId { - pub fn from_raw(raw: u32) -> Self { Self(raw) } - pub fn as_raw(self) -> u32 { self.0 } -} - -#[derive(Debug, Clone)] -pub struct BasicBlock { - pub id: BlockId, - pub params: Vec, - pub instructions: Vec, - pub terminator: Terminator, -} - -#[derive(Debug, Clone)] -pub enum Terminator { - Br { - target: BlockId, - args: Vec, - }, - CondBr { - cond: ValueId, - then_block: BlockId, - then_args: Vec, - else_block: BlockId, - else_args: Vec, - }, - Switch { - scrutinee: ValueId, - cases: Vec<(i64, BlockId, Vec)>, - default: (BlockId, Vec), - }, - Return(Option), - Throw(ValueId), - Fatal { message_id: u32 }, - Unreachable, -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::block_test` -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir/block.rs src/ir/tests/block_test.rs src/ir/tests/mod.rs -git commit -m "feat(ir): implement BasicBlock and Terminator" -``` - ---- - -## Task 6: Implement `Op`, `Instruction`, `InstId` - -**Files:** -- Modify: `src/ir/instr.rs` -- Test: `src/ir/tests/instr_test.rs` - -This is a large enum. Implement it in one pass; tests are minimal at this stage. Effect assignments per opcode are added in Task 7 via a helper. - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir/tests/instr_test.rs -use crate::ir::{Effects, InstId, Instruction, Op, ValueId}; - -#[test] -fn inst_id_is_zero_indexed() { - assert_eq!(InstId::from_raw(0).as_raw(), 0); -} - -#[test] -fn iadd_op_is_pure() { - assert_eq!(Op::IAdd.default_effects(), Effects::PURE); -} - -#[test] -fn array_set_writes_heap_and_allocs() { - let e = Op::ArraySet.default_effects(); - assert!(e.contains(Effects::WRITES_HEAP)); - assert!(e.contains(Effects::ALLOC_HEAP)); -} - -#[test] -fn fatal_terminator_is_separate_from_op() { - // Fatal is a Terminator, not an Op. This test guards against accidentally - // adding it to Op. - let names: Vec<&'static str> = vec![]; // no-op probe - assert!(names.is_empty()); -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::instr_test` -Expected: undefined symbols. - -- [ ] **Step 3: Implement the opcode enum** - -The full enum is large. Lay it out in the order from `docs/internals/the-ir.md` (Task 3 of Phase 01). Here is the canonical shape; copy verbatim: - -```rust -// src/ir/instr.rs -//! Purpose: -//! Defines `Op` (the opcode enum), the `Instruction` payload, and `InstId`. -//! -//! Called from: -//! - `crate::ir::builder`, lowering, validator, printer, codegen consumer -//! -//! Key details: -//! - Each opcode has a single canonical effect set returned by `default_effects()`. -//! Builders may not weaken effects; passes may not mutate them. - -use crate::ir::effects::Effects; -use crate::ir::types::IrType; -use crate::ir::value::ValueId; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct InstId(u32); - -impl InstId { - pub fn from_raw(raw: u32) -> Self { Self(raw) } - pub fn as_raw(self) -> u32 { self.0 } -} - -#[derive(Debug, Clone)] -pub struct Instruction { - pub op: Op, - pub operands: Vec, - pub immediate: Option, - pub result: Option, - pub result_type: IrType, - pub effects: Effects, -} - -#[derive(Debug, Clone)] -pub enum Immediate { - I64(i64), - F64(f64), - Str(u32), // string_id into DataPool - LocalSlot(u32), - GlobalName(u32), - FunctionRef(u32), - BuiltinRef(BuiltinId), - RuntimeRef(RuntimeId), - ExternRef(u32), - ClassRef(u32), - MethodRef { class: u32, method: u32 }, - PropOffset(u32), - HeapKind(crate::ir::types::IrHeapKind), - MixedTag(u8), - CmpPredicate(CmpPredicate), - CastTarget(IrType), - None, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CmpPredicate { - Eq, Ne, Slt, Sle, Sgt, Sge, - Olt, Ole, Ogt, Oge, // float -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct BuiltinId(pub u32); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct RuntimeId(pub u32); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Op { - // -- constants and locals -- - ConstI64, ConstF64, ConstStr, ConstNull, - LoadLocal, StoreLocal, - LoadGlobal, StoreGlobal, - // -- arithmetic / bitwise -- - IAdd, ISub, IMul, ISDiv, ISMod, INeg, - IBitAnd, IBitOr, IBitXor, IBitNot, IShl, IShrA, - FAdd, FSub, FMul, FDiv, FNeg, FPow, - // -- comparison -- - ICmp, FCmp, StrCmpEq, PhpLooseEq, PhpIdentical, Spaceship, - // -- conversion -- - IToF, FToI, IToStr, FToStr, BoolToStr, - StrToI, StrToF, - MixedBox, MixedUnbox, MixedTagOf, Cast, - // -- strings -- - StrConcat, StrLen, StrCharAt, StrPersist, StrInterpolate, - // -- arrays / hashes -- - ArrayNew, ArrayLen, ArrayGet, ArraySet, ArrayPush, ArrayCowEnsureUnique, - HashGetStr, HashGetInt, HashSetStr, HashSetInt, HashKeyExists, - IterStart, IterNext, IterCurrent, IterEnd, - // -- objects -- - ObjectNew, PropGet, PropSet, VTableLookup, InstanceOf, - // -- calls -- - Call, IndirectCall, MethodCall, BuiltinCall, RuntimeCall, ExternCall, - // -- closures -- - ClosureNew, ClosureCapture, - // -- ownership (no-op at runtime, semantic for validator/passes) -- - Acquire, Release, Move, Borrow, - // -- misc -- - Nop, -} - -impl Op { - pub fn default_effects(self) -> Effects { - use Effects as E; - use Op::*; - match self { - ConstI64 | ConstF64 | ConstStr | ConstNull - | INeg | IAdd | ISub | IMul - | IBitAnd | IBitOr | IBitXor | IBitNot | IShl | IShrA - | FAdd | FSub | FMul | FDiv | FNeg - | ICmp | FCmp | StrCmpEq | PhpIdentical - | IToF | FToI | BoolToStr | StrToI | StrToF - | StrLen | MixedTagOf - | Move | Borrow | Nop => E::PURE, - ISDiv | ISMod => E::MAY_FATAL, - FPow => E::PURE, // libc-backed but pure for our purposes - LoadLocal => E::READS_LOCAL, - StoreLocal => E::WRITES_LOCAL, - LoadGlobal => E::READS_GLOBAL, - StoreGlobal => E::WRITES_GLOBAL, - IToStr | FToStr | StrConcat | StrInterpolate => E::ALLOC_CONCAT, - StrPersist => E::ALLOC_HEAP, - StrCharAt => E::ALLOC_CONCAT | E::MAY_FATAL, - MixedBox => E::ALLOC_HEAP, - MixedUnbox => E::MAY_FATAL, - Cast => E::MAY_FATAL | E::ALLOC_CONCAT, // worst-case until refined - PhpLooseEq | Spaceship => E::MAY_DEOPT, - ArrayNew | HashGetStr | HashGetInt | HashKeyExists => { - // ArrayNew allocates; gets read heap. - match self { - ArrayNew => E::ALLOC_HEAP, - HashGetStr | HashGetInt => E::READS_HEAP | E::MAY_FATAL, - HashKeyExists => E::READS_HEAP, - _ => unreachable!(), - } - } - ArrayLen => E::READS_HEAP, - ArrayGet => E::READS_HEAP | E::MAY_FATAL, - ArraySet | HashSetStr | HashSetInt | ArrayPush => { - E::WRITES_HEAP | E::ALLOC_HEAP - } - ArrayCowEnsureUnique => E::ALLOC_HEAP, - IterStart | IterNext | IterCurrent => E::READS_HEAP | E::MAY_DEOPT, - IterEnd => E::WRITES_HEAP, - ObjectNew => E::ALLOC_HEAP | E::MAY_DEOPT, // constructor user code - PropGet => E::READS_HEAP, - PropSet => E::WRITES_HEAP, - VTableLookup => E::READS_HEAP, - InstanceOf => E::READS_HEAP, - Call | IndirectCall | MethodCall => E::all() - E::REFCOUNT_OP, // worst-case; refined per-callsite from FunctionSig - BuiltinCall => E::all() - E::REFCOUNT_OP, // refined per-builtin at builder time - RuntimeCall => E::all() - E::REFCOUNT_OP, // refined per-routine - ExternCall => E::READS_HEAP | E::WRITES_HEAP | E::MAY_THROW, - ClosureNew => E::ALLOC_HEAP, - ClosureCapture => E::READS_LOCAL, - Acquire | Release => E::REFCOUNT_OP, - } - } -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::instr_test` -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir/instr.rs src/ir/tests/instr_test.rs src/ir/tests/mod.rs -git commit -m "feat(ir): implement Op enum and Instruction" -``` - ---- - -## Task 7: Implement `LocalSlot`, `Function`, `Module` - -**Files:** -- Modify: `src/ir/function.rs` -- Modify: `src/ir/module.rs` -- Test: `src/ir/tests/function_test.rs` - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir/tests/function_test.rs -use crate::ir::{Function, FunctionFlags, IrType, LocalKind, LocalSlot, Module}; -use crate::ir::block::{BasicBlock, BlockId, Terminator}; -use crate::types::PhpType; - -#[test] -fn empty_function_has_no_blocks() { - let f = Function::new("foo".to_string(), IrType::Void, PhpType::Void); - assert_eq!(f.blocks.len(), 0); - assert_eq!(f.name, "foo"); -} - -#[test] -fn module_has_target_metadata() { - let target = crate::codegen::platform::Target::new( - crate::codegen::platform::Platform::MacOS, - crate::codegen::platform::Arch::AArch64, - ); - let m = Module::new(target); - assert!(m.functions.is_empty()); - assert!(m.class_methods.is_empty()); -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::function_test` -Expected: undefined symbols. - -- [ ] **Step 3: Implement `Function`** - -```rust -// src/ir/function.rs -//! Purpose: -//! Defines IR-level functions, their local slots, parameters, and per-function flags. -//! -//! Called from: -//! - `crate::ir::module`, builder, validator, codegen consumer -//! -//! Key details: -//! - The entry block has no block parameters; function parameters surface via -//! `LoadLocal` at the top of the entry block. - -use crate::ir::block::{BasicBlock, BlockId}; -use crate::ir::instr::Instruction; -use crate::ir::types::IrType; -use crate::ir::value::Value; -use crate::types::PhpType; - -#[derive(Debug, Clone)] -pub struct Function { - pub name: String, - pub params: Vec, - pub return_type: IrType, - pub return_php_type: PhpType, - pub blocks: Vec, - pub values: Vec, - pub instructions: Vec, - pub locals: Vec, - pub entry: BlockId, - pub flags: FunctionFlags, -} - -impl Function { - pub fn new(name: String, return_type: IrType, return_php_type: PhpType) -> Self { - Self { - name, - params: Vec::new(), - return_type, - return_php_type, - blocks: Vec::new(), - values: Vec::new(), - instructions: Vec::new(), - locals: Vec::new(), - entry: BlockId::from_raw(0), - flags: FunctionFlags::default(), - } - } -} - -#[derive(Debug, Clone)] -pub struct FunctionParam { - pub name: String, - pub ir_type: IrType, - pub php_type: PhpType, - pub by_ref: bool, - pub variadic: bool, -} - -#[derive(Debug, Clone)] -pub struct LocalSlot { - pub name: String, - pub php_type: PhpType, - pub kind: LocalKind, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LocalKind { - PhpVariable, - Hidden, - Static, - Global, -} - -#[derive(Debug, Clone, Default)] -pub struct FunctionFlags { - pub is_main: bool, - pub is_method: bool, - pub is_closure: bool, - pub is_generator: bool, - pub is_static: bool, -} -``` - -- [ ] **Step 4: Implement `Module`** - -```rust -// src/ir/module.rs -//! Purpose: -//! Top-level IR container: functions, class methods, data pool, extern decls. -//! -//! Called from: -//! - The AST → IR pass (phase 03) and the IR → ASM backend (phase 04). -//! -//! Key details: -//! - The runtime (`__rt_*`) lives outside the module; only declarations and -//! IDs of runtime routines used by the program are referenced. - -use crate::codegen::platform::Target; -use crate::ir::function::Function; - -#[derive(Debug, Clone)] -pub struct Module { - pub target: Target, - pub functions: Vec, - pub class_methods: Vec, - pub data: DataPool, - pub extern_decls: Vec, -} - -impl Module { - pub fn new(target: Target) -> Self { - Self { - target, - functions: Vec::new(), - class_methods: Vec::new(), - data: DataPool::default(), - extern_decls: Vec::new(), - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct DataPool { - pub strings: Vec, // strings[idx] -> literal - pub float_literals: Vec, - pub global_names: Vec, - pub function_names: Vec, - pub class_names: Vec, -} - -impl DataPool { - pub fn intern_string(&mut self, s: &str) -> u32 { - if let Some(idx) = self.strings.iter().position(|existing| existing == s) { - return idx as u32; - } - let id = self.strings.len() as u32; - self.strings.push(s.to_string()); - id - } -} - -#[derive(Debug, Clone)] -pub struct ExternDecl { - pub name: String, - pub link_libs: Vec, -} -``` - -- [ ] **Step 5: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::function_test` -Expected: pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/ir/function.rs src/ir/module.rs src/ir/tests/function_test.rs src/ir/tests/mod.rs -git commit -m "feat(ir): implement Function, Module, DataPool" -``` - ---- - -## Task 8: Implement the `Builder` - -**Files:** -- Modify: `src/ir/builder.rs` -- Test: `src/ir/tests/builder_test.rs` - -The `Builder` API is the only sanctioned way to add blocks, instructions, and values to a `Function`. It maintains invariants the validator relies on. - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir/tests/builder_test.rs -use crate::ir::{Builder, Effects, Function, IrType, Op, Terminator}; -use crate::types::PhpType; - -#[test] -fn build_function_with_return() { - let mut f = Function::new("ret_42".to_string(), IrType::I64, PhpType::Int); - let mut b = Builder::new(&mut f); - let entry = b.create_block_with_params(vec![]); - b.set_entry(entry); - b.position_at_end(entry); - let v = b.emit_const_i64(42); - b.terminate(Terminator::Return(Some(v))); - assert_eq!(f.blocks.len(), 1); - assert_eq!(f.values.len(), 1); -} - -#[test] -fn build_function_with_iadd_and_branch() { - let mut f = Function::new("add_one".to_string(), IrType::I64, PhpType::Int); - let mut b = Builder::new(&mut f); - let entry = b.create_block_with_params(vec![(IrType::I64, PhpType::Int)]); - b.set_entry(entry); - let arg = f.blocks[0].params[0]; // param ValueId - b.position_at_end(entry); - let one = b.emit_const_i64(1); - let sum = b.emit_iadd(arg, one); - b.terminate(Terminator::Return(Some(sum))); - assert_eq!(f.blocks[0].instructions.len(), 2); -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::builder_test` -Expected: undefined symbols. - -- [ ] **Step 3: Implement `Builder`** - -```rust -// src/ir/builder.rs -//! Purpose: -//! Mutator API for constructing IR functions. Maintains SSA invariants -//! that the validator depends on. -//! -//! Called from: -//! - Phase 03: AST → IR lowering -//! - Phase 02 tests: hand-built functions for validator and printer tests -//! -//! Key details: -//! - The builder enforces single terminator per block via `terminate()`. -//! - Builders fix effects at instruction-emit time using `Op::default_effects()`, -//! refined by per-callsite information passed to `emit_call()`-style helpers. - -use crate::ir::block::{BasicBlock, BlockId, Terminator}; -use crate::ir::effects::Effects; -use crate::ir::function::Function; -use crate::ir::instr::{Immediate, InstId, Instruction, Op}; -use crate::ir::types::IrType; -use crate::ir::value::{Ownership, Value, ValueDef, ValueId}; -use crate::types::PhpType; - -pub struct Builder<'f> { - func: &'f mut Function, - current: Option, -} - -impl<'f> Builder<'f> { - pub fn new(func: &'f mut Function) -> Self { - Self { func, current: None } - } - - pub fn set_entry(&mut self, block: BlockId) { - self.func.entry = block; - } - - pub fn create_block_with_params( - &mut self, - params: Vec<(IrType, PhpType)>, - ) -> BlockId { - let block_id = BlockId::from_raw(self.func.blocks.len() as u32); - let mut param_value_ids = Vec::with_capacity(params.len()); - for (idx, (ir_ty, php_ty)) in params.into_iter().enumerate() { - let value_id = ValueId::from_raw(self.func.values.len() as u32); - self.func.values.push(Value { - ir_type: ir_ty, - ownership: Ownership::for_php_type(&php_ty), - php_type: php_ty, - def: ValueDef::BlockParam { block: block_id, index: idx as u16 }, - }); - param_value_ids.push(value_id); - } - self.func.blocks.push(BasicBlock { - id: block_id, - params: param_value_ids, - instructions: Vec::new(), - terminator: Terminator::Unreachable, - }); - block_id - } - - pub fn position_at_end(&mut self, block: BlockId) { - self.current = Some(block); - } - - pub fn terminate(&mut self, term: Terminator) { - let block_id = self.current.expect("no block positioned"); - self.func.blocks[block_id.as_raw() as usize].terminator = term; - } - - fn push_inst( - &mut self, - op: Op, - operands: Vec, - immediate: Option, - result_type: IrType, - result_php_type: PhpType, - result_ownership: Ownership, - effects: Effects, - ) -> Option { - let block_id = self.current.expect("no block positioned"); - let block_idx = block_id.as_raw() as usize; - let inst_idx_in_block = self.func.blocks[block_idx].instructions.len() as u32; - let result_id = if matches!(result_type, IrType::Void) { - None - } else { - let value_id = ValueId::from_raw(self.func.values.len() as u32); - self.func.values.push(Value { - ir_type: result_type, - php_type: result_php_type, - def: ValueDef::Instruction { block: block_id, index: inst_idx_in_block }, - ownership: result_ownership, - }); - Some(value_id) - }; - let inst_id = InstId::from_raw(self.func.instructions.len() as u32); - self.func.instructions.push(Instruction { - op, - operands, - immediate, - result: result_id, - result_type, - effects, - }); - self.func.blocks[block_idx].instructions.push(inst_id); - result_id - } - - // -- convenience emitters -- - - pub fn emit_const_i64(&mut self, val: i64) -> ValueId { - self.push_inst( - Op::ConstI64, vec![], Some(Immediate::I64(val)), - IrType::I64, PhpType::Int, Ownership::NonHeap, - Op::ConstI64.default_effects(), - ).unwrap() - } - - pub fn emit_const_null(&mut self) -> ValueId { - self.push_inst( - Op::ConstNull, vec![], None, - IrType::I64, PhpType::Void, Ownership::NonHeap, - Op::ConstNull.default_effects(), - ).unwrap() - } - - pub fn emit_iadd(&mut self, a: ValueId, b: ValueId) -> ValueId { - self.push_inst( - Op::IAdd, vec![a, b], None, - IrType::I64, PhpType::Int, Ownership::NonHeap, - Op::IAdd.default_effects(), - ).unwrap() - } - - pub fn emit_load_local(&mut self, slot_id: u32, ir_ty: IrType, php_ty: PhpType) -> ValueId { - let own = Ownership::for_php_type(&php_ty); - self.push_inst( - Op::LoadLocal, vec![], Some(Immediate::LocalSlot(slot_id)), - ir_ty, php_ty, own, - Op::LoadLocal.default_effects(), - ).unwrap() - } - - pub fn emit_store_local(&mut self, slot_id: u32, val: ValueId) { - self.push_inst( - Op::StoreLocal, vec![val], Some(Immediate::LocalSlot(slot_id)), - IrType::Void, PhpType::Void, Ownership::NonHeap, - Op::StoreLocal.default_effects(), - ); - } - - // Additional emitters are added in phase 03 as lowering needs them. - // Pattern: one emit_* per Op variant; each pins the effect set from - // Op::default_effects() (refinable for Call/Builtin/Runtime/Extern). -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::builder_test` -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir/builder.rs src/ir/tests/builder_test.rs src/ir/tests/mod.rs -git commit -m "feat(ir): implement Builder API" -``` - ---- - -## Task 9: Implement the validator - -**Files:** -- Modify: `src/ir/validator.rs` -- Test: `src/ir/tests/validator_test.rs` - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir/tests/validator_test.rs -use crate::ir::{Builder, Function, IrType, Terminator, validate_function}; -use crate::types::PhpType; - -#[test] -fn empty_function_fails_validation() { - let f = Function::new("empty".to_string(), IrType::Void, PhpType::Void); - assert!(validate_function(&f).is_err()); -} - -#[test] -fn well_formed_function_passes() { - let mut f = Function::new("ok".to_string(), IrType::I64, PhpType::Int); - let mut b = Builder::new(&mut f); - let entry = b.create_block_with_params(vec![]); - b.set_entry(entry); - b.position_at_end(entry); - let v = b.emit_const_i64(7); - b.terminate(Terminator::Return(Some(v))); - assert!(validate_function(&f).is_ok()); -} - -#[test] -fn return_type_mismatch_fails() { - let mut f = Function::new("bad".to_string(), IrType::F64, PhpType::Float); - let mut b = Builder::new(&mut f); - let entry = b.create_block_with_params(vec![]); - b.set_entry(entry); - b.position_at_end(entry); - let v = b.emit_const_i64(1); - b.terminate(Terminator::Return(Some(v))); - assert!(validate_function(&f).is_err()); -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::validator_test` -Expected: undefined symbols. - -- [ ] **Step 3: Implement the validator (structural only in Phase 02)** - -```rust -// src/ir/validator.rs -//! Purpose: -//! Structural, type, and ownership validation for `Function`/`Module`. -//! -//! Called from: -//! - After every IR pass; called from tests directly. -//! -//! Key details: -//! - Phase 02 implements structural checks; ownership and dominance checks -//! land in phase 03 once the AST → IR pass has more realistic IR to test. - -use crate::ir::block::{BlockId, Terminator}; -use crate::ir::function::Function; -use crate::ir::module::Module; -use crate::ir::value::ValueDef; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ValidationError { - NoBlocks, - NoEntryBlock, - BlockMissingTerminator(BlockId), - DuplicateValueDef(u32), - UseBeforeDef { value_raw: u32 }, - UnknownBlock(BlockId), - BranchArgCountMismatch { target: BlockId, expected: usize, actual: usize }, - BranchArgTypeMismatch { target: BlockId, index: usize }, - ReturnTypeMismatch, - UnreachableTerminatorIsReachable(BlockId), -} - -pub fn validate_function(f: &Function) -> Result<(), ValidationError> { - if f.blocks.is_empty() { - return Err(ValidationError::NoBlocks); - } - if (f.entry.as_raw() as usize) >= f.blocks.len() { - return Err(ValidationError::NoEntryBlock); - } - - for block in &f.blocks { - // structural: there's always a terminator field; Unreachable counts. - // Type checks per terminator: - match &block.terminator { - Terminator::Return(Some(val)) => { - let v = &f.values[val.as_raw() as usize]; - if v.ir_type != f.return_type { - return Err(ValidationError::ReturnTypeMismatch); - } - } - Terminator::Return(None) => { - if !matches!(f.return_type, crate::ir::types::IrType::Void) { - return Err(ValidationError::ReturnTypeMismatch); - } - } - Terminator::Br { target, args } => { - let dest = f.blocks.get(target.as_raw() as usize) - .ok_or(ValidationError::UnknownBlock(*target))?; - if dest.params.len() != args.len() { - return Err(ValidationError::BranchArgCountMismatch { - target: *target, - expected: dest.params.len(), - actual: args.len(), - }); - } - for (i, (param_id, arg_id)) in dest.params.iter().zip(args.iter()).enumerate() { - let p = &f.values[param_id.as_raw() as usize]; - let a = &f.values[arg_id.as_raw() as usize]; - if p.ir_type != a.ir_type { - return Err(ValidationError::BranchArgTypeMismatch { - target: *target, - index: i, - }); - } - } - } - // Remaining terminators: similar checks for CondBr, Switch, etc. - // For phase 02 brevity, implement Br and Return only; phase 03 - // extends to all terminators with corresponding tests. - _ => {} - } - } - - Ok(()) -} - -pub fn validate_module(m: &Module) -> Result<(), ValidationError> { - for f in &m.functions { - validate_function(f)?; - } - for f in &m.class_methods { - validate_function(f)?; - } - Ok(()) -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::validator_test` -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir/validator.rs src/ir/tests/validator_test.rs src/ir/tests/mod.rs -git commit -m "feat(ir): implement structural validator" -``` - ---- - -## Task 10: Implement the textual printer - -**Files:** -- Modify: `src/ir/print.rs` -- Test: `src/ir/tests/print_test.rs` - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir/tests/print_test.rs -use crate::ir::{Builder, Function, IrType, Module, Terminator, print_module}; -use crate::types::PhpType; -use crate::codegen::platform::{Arch, Platform, Target}; - -#[test] -fn prints_minimal_function() { - let target = Target::new(Platform::MacOS, Arch::AArch64); - let mut m = Module::new(target); - let mut f = Function::new("ret_seven".to_string(), IrType::I64, PhpType::Int); - let mut b = Builder::new(&mut f); - let entry = b.create_block_with_params(vec![]); - b.set_entry(entry); - b.position_at_end(entry); - let v = b.emit_const_i64(7); - b.terminate(Terminator::Return(Some(v))); - m.functions.push(f); - - let printed = print_module(&m); - assert!(printed.contains("function ret_seven")); - assert!(printed.contains("const_i64 7")); - assert!(printed.contains("return v")); -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test --lib ir::tests::print_test` -Expected: undefined symbol `print_module`. - -- [ ] **Step 3: Implement printer** - -```rust -// src/ir/print.rs -//! Purpose: -//! Textual format for `Module` and `Function`. Used by `--emit-ir` and tests. -//! -//! Called from: -//! - `Module::print()` wrapper, CLI in phase 03 (`--emit-ir`). -//! -//! Key details: -//! - Printer-only; no parser. Output stability is a soft guarantee for tests. - -use std::fmt::Write; - -use crate::ir::block::Terminator; -use crate::ir::function::Function; -use crate::ir::instr::{Immediate, Op}; -use crate::ir::module::Module; -use crate::ir::types::{IrHeapKind, IrType}; - -pub fn print_module(m: &Module) -> String { - let mut out = String::new(); - for f in &m.functions { - print_function(&mut out, f, &m.data); - out.push('\n'); - } - for f in &m.class_methods { - print_function(&mut out, f, &m.data); - out.push('\n'); - } - out -} - -fn print_function(out: &mut String, f: &Function, data: &crate::ir::module::DataPool) { - let _ = write!(out, "function {}(", f.name); - for (i, p) in f.params.iter().enumerate() { - if i > 0 { out.push_str(", "); } - let _ = write!(out, "{}: {}", p.name, type_name(p.ir_type)); - } - let _ = writeln!(out, ") -> {} {{", type_name(f.return_type)); - for block in &f.blocks { - let _ = write!(out, " bb{}", block.id.as_raw()); - if !block.params.is_empty() { - out.push('('); - for (i, pid) in block.params.iter().enumerate() { - if i > 0 { out.push_str(", "); } - let v = &f.values[pid.as_raw() as usize]; - let _ = write!(out, "v{}: {}", pid.as_raw(), type_name(v.ir_type)); - } - out.push(')'); - } - out.push_str(":\n"); - for inst_id in &block.instructions { - let inst = &f.instructions[inst_id.as_raw() as usize]; - out.push_str(" "); - if let Some(r) = inst.result { - let _ = write!(out, "v{} = ", r.as_raw()); - } - let _ = write!(out, "{:?}", inst.op); - for operand in &inst.operands { - let _ = write!(out, " v{}", operand.as_raw()); - } - if let Some(imm) = &inst.immediate { - print_immediate(out, imm, data); - } - if !inst.effects.is_empty() { - let _ = write!(out, " ; effects: {:?}", inst.effects); - } - out.push('\n'); - } - out.push_str(" "); - print_terminator(out, &block.terminator); - out.push('\n'); - } - out.push_str("}\n"); -} - -fn print_immediate(out: &mut String, imm: &Immediate, data: &crate::ir::module::DataPool) { - match imm { - Immediate::I64(v) => { let _ = write!(out, " {}", v); } - Immediate::F64(v) => { let _ = write!(out, " {}", v); } - Immediate::Str(idx) => { - let s = data.strings.get(*idx as usize).map(|s| s.as_str()).unwrap_or("?"); - let _ = write!(out, " {:?}", s); - } - Immediate::LocalSlot(idx) => { let _ = write!(out, " slot[{}]", idx); } - Immediate::GlobalName(idx) => { - let s = data.global_names.get(*idx as usize).map(|s| s.as_str()).unwrap_or("?"); - let _ = write!(out, " global({})", s); - } - Immediate::FunctionRef(idx) => { - let s = data.function_names.get(*idx as usize).map(|s| s.as_str()).unwrap_or("?"); - let _ = write!(out, " fn({})", s); - } - Immediate::BuiltinRef(id) => { let _ = write!(out, " builtin#{}", id.0); } - Immediate::RuntimeRef(id) => { let _ = write!(out, " runtime#{}", id.0); } - Immediate::ExternRef(idx) => { let _ = write!(out, " extern#{}", idx); } - Immediate::ClassRef(idx) => { - let s = data.class_names.get(*idx as usize).map(|s| s.as_str()).unwrap_or("?"); - let _ = write!(out, " class({})", s); - } - Immediate::MethodRef { class, method } => { let _ = write!(out, " method({},{})", class, method); } - Immediate::PropOffset(off) => { let _ = write!(out, " prop@{}", off); } - Immediate::HeapKind(k) => { let _ = write!(out, " kind={}", heap_kind_name(*k)); } - Immediate::MixedTag(t) => { let _ = write!(out, " tag={}", t); } - Immediate::CmpPredicate(p) => { let _ = write!(out, " pred={:?}", p); } - Immediate::CastTarget(t) => { let _ = write!(out, " to={}", type_name(*t)); } - Immediate::None => {} - } -} - -fn print_terminator(out: &mut String, term: &Terminator) { - match term { - Terminator::Br { target, args } => { - let _ = write!(out, "br bb{}", target.as_raw()); - if !args.is_empty() { print_args(out, args); } - } - Terminator::CondBr { cond, then_block, then_args, else_block, else_args } => { - let _ = write!(out, "cond_br v{}, bb{}", cond.as_raw(), then_block.as_raw()); - if !then_args.is_empty() { print_args(out, then_args); } - let _ = write!(out, ", bb{}", else_block.as_raw()); - if !else_args.is_empty() { print_args(out, else_args); } - } - Terminator::Switch { scrutinee, cases, default } => { - let _ = write!(out, "switch v{} [", scrutinee.as_raw()); - for (val, target, args) in cases { - let _ = write!(out, "{} => bb{}", val, target.as_raw()); - if !args.is_empty() { print_args(out, args); } - out.push_str(", "); - } - let _ = write!(out, "default => bb{}", default.0.as_raw()); - if !default.1.is_empty() { print_args(out, &default.1); } - out.push(']'); - } - Terminator::Return(Some(v)) => { let _ = write!(out, "return v{}", v.as_raw()); } - Terminator::Return(None) => { out.push_str("return"); } - Terminator::Throw(v) => { let _ = write!(out, "throw v{}", v.as_raw()); } - Terminator::Fatal { message_id } => { let _ = write!(out, "fatal msg#{}", message_id); } - Terminator::Unreachable => { out.push_str("unreachable"); } - } -} - -fn print_args(out: &mut String, args: &[crate::ir::value::ValueId]) { - out.push('('); - for (i, v) in args.iter().enumerate() { - if i > 0 { out.push_str(", "); } - let _ = write!(out, "v{}", v.as_raw()); - } - out.push(')'); -} - -fn type_name(t: IrType) -> String { - match t { - IrType::I64 => "I64".to_string(), - IrType::F64 => "F64".to_string(), - IrType::Str => "Str".to_string(), - IrType::Heap(k) => format!("Heap[{}]", heap_kind_name(k)), - IrType::Void => "Void".to_string(), - } -} - -fn heap_kind_name(k: IrHeapKind) -> &'static str { - match k { - IrHeapKind::Array => "Array", - IrHeapKind::Hash => "Hash", - IrHeapKind::Object => "Object", - IrHeapKind::Mixed => "Mixed", - IrHeapKind::Iterable => "Iterable", - IrHeapKind::Union => "Union", - IrHeapKind::Buffer => "Buffer", - } -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test --lib ir::tests::print_test` -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir/print.rs src/ir/tests/print_test.rs src/ir/tests/mod.rs -git commit -m "feat(ir): implement textual format printer" -``` - ---- - -## Task 11: Run full test suite and verify zero regressions - -- [ ] **Step 1: Run full suite** - -Run: -```bash -cargo build -cargo test -cargo test -- --include-ignored -``` - -Expected: all green. No new code paths are exercised by the existing test suite — `src/ir/` is untouched by the compiler. New tests live under `src/ir/tests/`. - -- [ ] **Step 2: Run Docker Linux gates** - -Run: -```bash -./scripts/test-linux-x86_64.sh -./scripts/test-linux-arm64.sh -``` - -Expected: green. - -- [ ] **Step 3: `git diff --check` and final commit if anything left** - -Run: -```bash -git diff --check -git status -``` - -If any whitespace or untracked files appear, fix and amend the last commit only if it is the most recent; otherwise create a new commit. - ---- - -## Exit criteria - -- `src/ir/` module compiles cleanly -- All Phase 02 unit tests pass -- Full suite (`cargo test -- --include-ignored`) passes -- Docker Linux gates pass -- Zero compiler warnings -- All commits follow `feat(ir):` prefix -- No file in `src/ir/` exceeds 500 LOC unless it is a cohesive leaf (see file-size policy in `CLAUDE.md`) diff --git a/.plans/eir-03-ast-to-ir-lowering.md b/.plans/eir-03-ast-to-ir-lowering.md deleted file mode 100644 index 1533478271..0000000000 --- a/.plans/eir-03-ast-to-ir-lowering.md +++ /dev/null @@ -1,872 +0,0 @@ -# Phase 03 — AST → IR Lowering - -> **Current status:** Historical phase plan. EIR lowering is now part of the -> active production path, and the legacy direct AST backend is frozen. References -> below to the legacy backend as production path are superseded by `AGENTS.md` -> and `ROADMAP.md`. - -> **For agentic workers:** Build the pass that consumes a typed AST (after frontend/optimizer) and produces an EIR `Module`. No assembly is emitted; the existing AST → ASM backend remains the production path. Output is exercised by a new `--emit-ir` CLI flag and by a parallel test harness. - -**Goal:** Lower every `ExprKind` and `StmtKind` variant currently handled by `src/codegen/` into EIR, preserving PHP semantics. Validate the resulting module after each top-level lowering. - -**Architecture:** New module `src/ir_lower/` that mirrors the structure of `src/codegen/expr/` and `src/codegen/stmt/`. The lowering walks the AST top-down and feeds the EIR `Builder`. Ownership operations are inserted explicitly. Function dispatchers, class methods, variants, and main emission are mirrored. - -**Tech Stack:** Rust. Uses `src/ir/`, `src/parser/ast/`, `src/types/`, `src/codegen/program_usage/`. No new external dependencies. - ---- - -## File Structure - -All new files under `src/ir_lower/`: - -- Create: `src/ir_lower/mod.rs` — entry `pub fn lower_program(...)` -- Create: `src/ir_lower/context.rs` — lowering context (local slot map, label counter, ownership table) -- Create: `src/ir_lower/program.rs` — orchestration: functions, class methods, main, variants -- Create: `src/ir_lower/function.rs` — function body lowering (entry block, param load, prologue/epilogue) -- Create: `src/ir_lower/expr/mod.rs` — dispatcher -- Create: `src/ir_lower/expr/literals.rs` — literals and constants -- Create: `src/ir_lower/expr/variables.rs` — variable load/store, super-globals, statics -- Create: `src/ir_lower/expr/arithmetic.rs` — binary arithmetic, unary, casts, conversions -- Create: `src/ir_lower/expr/comparison.rs` — comparison, equality, spaceship -- Create: `src/ir_lower/expr/strings.rs` — concat, interpolation, char access -- Create: `src/ir_lower/expr/arrays.rs` — array literals, access, assignment, push -- Create: `src/ir_lower/expr/objects.rs` — `new`, `->`, methods, `::`, `instanceof` -- Create: `src/ir_lower/expr/calls.rs` — function calls, builtins, externs, closures, first-class callables -- Create: `src/ir_lower/expr/control.rs` — ternary, short-circuit `&&`/`||`, null coalesce, throw, error suppress, print -- Create: `src/ir_lower/expr/closures.rs` — closure expressions and arrow functions -- Create: `src/ir_lower/expr/ptr_ffi.rs` — ptr_cast, buffer_new -- Create: `src/ir_lower/expr/match_expr.rs` — `match` expression -- Create: `src/ir_lower/stmt/mod.rs` — dispatcher -- Create: `src/ir_lower/stmt/control_flow.rs` — `if`, `while`, `do_while`, `for`, `foreach`, `break`, `continue`, `switch` -- Create: `src/ir_lower/stmt/exceptions.rs` — `try`, `catch`, `finally`, `throw` -- Create: `src/ir_lower/stmt/assignments.rs` — `Assign`, `ArrayAssign`, `ArrayPush`, `PropertyAssign`, `StaticPropertyAssign`, all variants -- Create: `src/ir_lower/stmt/declarations.rs` — `FunctionDecl`, `ClassDecl`, `EnumDecl`, `InterfaceDecl`, `TraitDecl`, `PackedClassDecl`, `ExternFunctionDecl`, etc. (most are no-ops at lowering since they were already handled by frontend; lowering records IDs) -- Create: `src/ir_lower/stmt/includes.rs` — `Include`, `IncludeOnce*` -- Create: `src/ir_lower/stmt/output.rs` — `Echo`, statement-form `Print` -- Create: `src/ir_lower/ownership.rs` — helpers for inserting `Acquire`/`Release` on heap values -- Create: `src/ir_lower/effects_lookup.rs` — looks up effect summaries for builtins/runtime calls from existing analysis -- Create: `src/ir_lower/tests/` — integration tests (snapshot-based using the printer) -- Modify: `src/lib.rs` — add `pub mod ir_lower;` -- Modify: `src/main.rs` — add `mod ir_lower;` for the binary crate module tree -- Modify: `src/cli.rs` — add `--emit-ir` parsing to `CliConfig` -- Modify: `src/pipeline.rs` — honor `emit_ir` after frontend and optimization - ---- - -## Task 1: Wire the module and add `--emit-ir` - -**Files:** -- Create: `src/ir_lower/mod.rs` (skeleton) -- Modify: `src/lib.rs` -- Modify: `src/main.rs` (module declaration) -- Modify: `src/cli.rs` (CLI argument parser) -- Modify: `src/pipeline.rs` (emit path) - -- [ ] **Step 1: Confirm CLI parser and module roots** - -Run: `grep -rln "pub(crate) fn parse_args\\|fn main" src/` -In this branch, `src/cli.rs` owns argument parsing and `src/main.rs` owns the binary crate module declarations. - -- [ ] **Step 2: Create skeleton** - -```rust -// src/ir_lower/mod.rs -//! Purpose: -//! Lowers a typed `Program` AST into an EIR `Module`. Preserves PHP semantics -//! including evaluation order, ownership, and effect annotations. -//! -//! Called from: -//! - `crate::pipeline::compile()` when `--emit-ir` or `--ir-backend` is set -//! -//! Key details: -//! - Lowering is one pass over the AST; ownership ops are inserted explicitly. -//! - Validation runs after every function lowering to catch builder bugs early. - -mod context; -mod effects_lookup; -mod expr; -mod function; -mod ownership; -mod program; -mod stmt; - -#[cfg(test)] -mod tests; - -use std::collections::HashMap; - -use crate::codegen::platform::Target; -use crate::ir::Module; -use crate::parser::ast::Program; -use crate::types::{ - ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, InterfaceInfo, - PackedClassInfo, PhpType, TypeEnv, -}; - -#[allow(clippy::too_many_arguments)] -pub fn lower_program( - program: &Program, - global_env: &TypeEnv, - functions: &HashMap, - interfaces: &HashMap, - classes: &HashMap, - enums: &HashMap, - packed_classes: &HashMap, - extern_functions: &HashMap, - extern_classes: &HashMap, - extern_globals: &HashMap, - target: Target, -) -> Module { - program::lower( - program, global_env, functions, interfaces, classes, enums, - packed_classes, extern_functions, extern_classes, extern_globals, target, - ) -} -``` - -- [ ] **Step 3: Add `--emit-ir` to the CLI** - -Locate the existing argument parser. Add a boolean flag `--emit-ir`. When set: - -1. Run the full frontend (lex, parse, name-resolve, type-check, optimize). -2. Call `ir_lower::lower_program(...)`. -3. Call `ir::print_module(&module)`, print to stdout. -4. Skip codegen, assembler, linker. -5. Exit zero. - -Place implementation in `src/cli.rs` alongside `--emit-asm` and `--check`. Add an `emit_ir` field to `CliConfig`, extend the usage string, and reject `--emit-ir` with `--emit-asm` or `--check` because all three are mutually exclusive output modes. - -- [ ] **Step 4: Add an end-to-end test** - -```rust -// tests/ir_emit_test.rs (or inside an existing tests/ file) -#[test] -fn emit_ir_prints_a_function_for_hello_world() { - let temp = tests_common::compile_inline( - "`. Stores to -//! a local rebind the local to a fresh SSA value at the current program -//! point; uses load from the most recent binding (no explicit phi insertion -//! — block parameters at merge points handle joins). - -use std::collections::HashMap; - -use crate::ir::{BlockId, Builder, Function, IrType, ValueId}; -use crate::types::PhpType; - -pub struct LoweringContext<'a, 'f> { - pub func: &'f mut Function, - pub builder_state: BuilderState, - // local name -> most recent SSA value bound - pub local_bindings: HashMap, - // local name -> slot index in func.locals - pub local_slots: HashMap, - pub loop_stack: Vec, - pub handler_stack: Vec, - pub captures: HashMap, - pub label_counter: &'a std::sync::atomic::AtomicUsize, -} - -pub struct BuilderState { - pub current_block: Option, -} - -pub struct LoopFrame { - pub continue_block: BlockId, - pub break_block: BlockId, - pub continue_args_template: Vec, - pub break_args_template: Vec, -} - -pub struct HandlerFrame { - pub catch_block: BlockId, - pub finally_block: Option, -} - -impl<'a, 'f> LoweringContext<'a, 'f> { - pub fn declare_local(&mut self, name: &str, php_ty: PhpType) -> u32 { - if let Some(idx) = self.local_slots.get(name) { return *idx; } - let idx = self.func.locals.len() as u32; - self.func.locals.push(crate::ir::LocalSlot { - name: name.to_string(), - php_type: php_ty, - kind: crate::ir::LocalKind::PhpVariable, - }); - self.local_slots.insert(name.to_string(), idx); - idx - } - - pub fn current_value_of(&self, name: &str) -> Option { - self.local_bindings.get(name).copied() - } - - pub fn rebind(&mut self, name: &str, value: ValueId) { - self.local_bindings.insert(name.to_string(), value); - } -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add src/ir_lower/context.rs -git commit -m "feat(ir_lower): add LoweringContext skeleton" -``` - ---- - -## Task 3: Implement function-level lowering - -**Files:** -- Create: `src/ir_lower/function.rs` - -- [ ] **Step 1: Write the function lowering driver** - -The driver: -1. Creates the entry block with no parameters. -2. Allocates locals for each function parameter, each `static`, each `global`, each PHP local discovered (mirroring `src/codegen/functions/locals.rs` collection logic). -3. Emits parameter `LoadLocal` ops at the top of the entry block. -4. Walks the function body via `stmt::lower_stmt(...)`. -5. If control falls off the end and the return type is `Void`, emits `Return(None)`; otherwise `Fatal` (mirroring PHP behavior where falling off a non-void function with declared return type is undefined / fatal in strict types). -6. Runs `validate_function`. - -```rust -//! Purpose: -//! Lowers a single PHP function body into an IR `Function`. -//! -//! Called from: -//! - `crate::ir_lower::program::lower` -//! -//! Key details: -//! - Locals are pre-collected before lowering to ensure stable slot indices. -//! - Validation runs at end-of-function. - -use crate::ir::{Builder, Function, FunctionFlags, FunctionParam, IrType, Terminator}; -use crate::ir_lower::context::LoweringContext; -use crate::parser::ast::Stmt; -use crate::types::{FunctionSig, PhpType, TypeEnv}; - -pub fn lower_function( - name: &str, - sig: &FunctionSig, - body: &[Stmt], - global_env: &TypeEnv, - // ... + frontend metadata maps; same signature as src/codegen/functions emit_function -) -> Function { - let return_ir = IrType::from_php(&sig.return_type); - let mut func = Function::new(name.to_string(), return_ir, sig.return_type.clone()); - // Populate params: - for p in &sig.params { - func.params.push(FunctionParam { - name: p.name.clone(), - ir_type: IrType::from_php(&p.ty), - php_type: p.ty.clone(), - by_ref: p.by_ref, - variadic: p.variadic, - }); - } - func.flags = FunctionFlags { /* derive */ ..Default::default() }; - - // Build entry block. - { - let mut b = Builder::new(&mut func); - let entry = b.create_block_with_params(vec![]); - b.set_entry(entry); - b.position_at_end(entry); - // ... allocate locals, load params, lower body via stmt::lower_stmt - } - - // Validate. - crate::ir::validate_function(&func).expect("ir validation failed"); - func -} -``` - -- [ ] **Step 2: Commit (incomplete; expanded in later tasks)** - -```bash -git add src/ir_lower/function.rs -git commit -m "feat(ir_lower): function-level lowering skeleton" -``` - ---- - -## Task 4: Lower literals and locals - -**Files:** -- Create: `src/ir_lower/expr/mod.rs` -- Create: `src/ir_lower/expr/literals.rs` -- Create: `src/ir_lower/expr/variables.rs` -- Test: `src/ir_lower/tests/literals_test.rs` - -- [ ] **Step 1: Write the failing test** - -```rust -// src/ir_lower/tests/literals_test.rs -use crate::ir_lower::tests::lower_and_print; - -#[test] -fn lowers_int_literal_return() { - let printed = lower_and_print("= 2, "expected at least 2 branches, got {br_count}"); -} - -#[test] -fn lowers_break_branches_to_loop_break_block() { - let printed = lower_and_print(" 1];"#); - assert!(printed.contains("Hash")); - assert!(printed.contains("hash_set_str")); -} - -#[test] -fn lowers_array_get_indexed() { - let printed = lower_and_print(", ::, instanceof, method calls) - -**Files:** -- Create: `src/ir_lower/expr/objects.rs` -- Test: `src/ir_lower/tests/objects_test.rs` - -- [ ] **Step 1: Failing tests** (one per primitive shape) -- [ ] **Step 2 / 3 / 4**: Implement. - -Notes: -- `new ClassName(args)` → `ObjectNew(class_id)` + `Call(ctor)` with args. -- `$obj->prop` → `PropGet(obj, offset)`. Offset is computed from `ClassInfo.property_layout` at lowering time (already known after type checking). -- `$obj->method(args)` → `VTableLookup(obj, method_id)` returning a function pointer, then `IndirectCall(ptr, sig, args)`. For statically known final methods, skip the lookup and emit `Call(direct_fn_id)`. -- `instanceof` → `InstanceOf(obj, class_id)`. -- Static method/property accesses → direct `Call`/`LoadGlobal` on the class's static storage. -- Nullsafe (`?->`) → conditional branch: if null, the chain result is null; else proceed. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir_lower/expr/objects.rs src/ir_lower/tests/objects_test.rs -git commit -m "feat(ir_lower): lower objects and method calls" -``` - ---- - -## Task 10: Lower exception flow (`try`/`catch`/`finally`, `throw`) - -**Files:** -- Create: `src/ir_lower/stmt/exceptions.rs` -- Test: `src/ir_lower/tests/exceptions_test.rs` - -- [ ] **Step 1: Failing tests** -- [ ] **Step 2 / 3 / 4**: Implement. - -Notes: -- Push a `HandlerFrame` on `ctx.handler_stack` when entering `try`. -- Operations with `MAY_THROW` effect lowered while a handler is on the stack get an *implicit edge* to the handler block. The structural validator does not check this yet (Phase 09 hardens it); for now, ensure cleanup paths run. -- `throw expr` → `Throw(exc_value)` terminator. Builder records the value as owned at the throw point; the runtime takes ownership via `__rt_throw`. -- `finally` runs on all exit paths (normal, throw, return, break, continue). Lower as a *cleanup block* that the handler-walk inserts before each exit terminator. Implementation strategy: lower the `finally` body once into a synthetic block, then duplicate-or-call from each exit point. PHP semantics require the finally body to run; calling is simpler than inlining. - -- [ ] **Step 5: Commit** - -```bash -git add src/ir_lower/stmt/exceptions.rs src/ir_lower/tests/exceptions_test.rs -git commit -m "feat(ir_lower): lower try/catch/finally and throw" -``` - ---- - -## Task 11: Lower assignments, ownership transfer ops - -**Files:** -- Create: `src/ir_lower/stmt/assignments.rs` -- Create: `src/ir_lower/ownership.rs` -- Test: `src/ir_lower/tests/ownership_test.rs` - -- [ ] **Step 1: Failing tests** - -```rust -#[test] -fn array_local_assignment_releases_previous_owned_array() { - let src = " **Current status:** Historical phase plan. EIR is now the default and only -> active implementation backend. References below to keeping the legacy backend -> working were Phase 04 parity scaffolding, not current feature-work policy. - -> **For agentic workers:** Build a new backend that takes an EIR `Module` and produces assembly. The goal is **semantic equivalence** to the current `src/codegen/` backend on every `tests/codegen/` fixture, NOT byte-identical assembly. Zero new optimizations. - -**Goal:** A new module `src/codegen_ir/` that walks EIR and emits ASM identical-in-behavior to today's AST → ASM path. After this phase, choosing the IR backend produces correct programs; choosing the legacy backend continues to work. No default switch yet (that's Phase 05). - -**Architecture:** Mirror the structure of `src/codegen/` but consume `Function` instances instead of AST. Reuse `src/codegen/abi/` register helpers and `src/codegen/runtime/` runtime emission unchanged. Reuse `src/codegen/data_section.rs` for the data pool. - -**Tech Stack:** Rust. No new dependencies. - ---- - -## File Structure - -All new files under `src/codegen_ir/`: - -- Create: `src/codegen_ir/mod.rs` — entry `pub fn generate_user_asm_from_ir(...)` -- Create: `src/codegen_ir/context.rs` — backend state: ABI cursor, value→register map, label counter, emitter, frame layout -- Create: `src/codegen_ir/frame.rs` — frame size calculation, prologue/epilogue emission (mirrors `src/codegen/functions/locals.rs` + frame emission) -- Create: `src/codegen_ir/value_placement.rs` — value → register-or-slot mapping for the 1:1 backend (no real register allocator yet — values land in fixed slots, then move into ABI registers at uses) -- Create: `src/codegen_ir/lower_inst.rs` — per-opcode emitter dispatcher -- Create: `src/codegen_ir/lower_inst/arithmetic.rs` — IAdd/ISub/IMul/ISDiv/INeg/bitwise/shifts -- Create: `src/codegen_ir/lower_inst/floats.rs` — FAdd/FSub/.../FPow -- Create: `src/codegen_ir/lower_inst/comparison.rs` — ICmp/FCmp/StrCmp/PhpLooseEq/PhpIdentical/Spaceship -- Create: `src/codegen_ir/lower_inst/conversion.rs` — IToF/FToI/IToStr/.../MixedBox/MixedUnbox/Cast -- Create: `src/codegen_ir/lower_inst/strings.rs` — StrConcat/StrLen/StrCharAt/StrPersist/StrInterpolate -- Create: `src/codegen_ir/lower_inst/arrays.rs` — ArrayNew/Get/Set/Push/CowEnsureUnique -- Create: `src/codegen_ir/lower_inst/hashes.rs` — HashGet*/Set*/KeyExists/Iter* -- Create: `src/codegen_ir/lower_inst/objects.rs` — ObjectNew/PropGet/PropSet/VTableLookup/InstanceOf -- Create: `src/codegen_ir/lower_inst/calls.rs` — Call/IndirectCall/MethodCall/BuiltinCall/RuntimeCall/ExternCall, closure ops -- Create: `src/codegen_ir/lower_inst/ownership.rs` — Acquire/Release/Move/Borrow (no-op for Move/Borrow at runtime) -- Create: `src/codegen_ir/lower_term.rs` — Br/CondBr/Switch/Return/Throw/Fatal/Unreachable -- Create: `src/codegen_ir/block_emit.rs` — block label naming, block ordering, jumps between blocks -- Create: `src/codegen_ir/tests/` — integration tests using `compile_and_run`-style helpers with the IR backend -- Modify: `src/lib.rs` — add `pub mod codegen_ir;` -- Modify: `src/main.rs` — add `mod codegen_ir;` for the binary crate module tree -- Modify: `src/pipeline.rs` — when the IR backend feature is requested, call `codegen_ir::generate_user_asm_from_ir(...)` instead of `codegen::generate_user_asm(...)` - ---- - -## Task 1: Wire the module and add `--ir-backend` CLI flag - -**Files:** -- Create: `src/codegen_ir/mod.rs` (skeleton) -- Modify: `src/lib.rs` -- Modify: `src/main.rs` (module declaration) -- Modify: `src/cli.rs` (CLI argument parser) -- Modify: `src/pipeline.rs` - -- [ ] **Step 1: Module skeleton** - -```rust -// src/codegen_ir/mod.rs -//! Purpose: -//! IR-consuming assembly backend. Produces functionally equivalent ASM to -//! `src/codegen/` while reading from an EIR `Module` instead of an AST. -//! -//! Called from: -//! - `crate::pipeline::compile()` when the `--ir-backend` flag is set. -//! -//! Key details: -//! - Phase 04: 1:1 lowering, no optimization, no register allocation. -//! - Phase 06: adds linear-scan register allocator. -//! - Phase 09: replaces `src/codegen/` as the default. - -mod block_emit; -mod context; -mod frame; -mod lower_inst; -mod lower_term; -mod value_placement; - -#[cfg(test)] -mod tests; - -use crate::codegen::platform::Target; -use crate::ir::Module; - -pub fn generate_user_asm_from_ir(module: &Module, _gc_stats: bool, _heap_debug: bool) -> String { - // Implementation in Task 3. - unimplemented!("phase 04") -} -``` - -- [ ] **Step 2: CLI flag** - -Add `--ir-backend` (boolean) to `src/cli.rs` and thread it through `CliConfig`. When set: -1. Run frontend + optimizer + AST → IR lowering. -2. Call `codegen_ir::generate_user_asm_from_ir(...)`. -3. Hand the assembly to the existing assembler/linker pipeline (unchanged). -4. Produce a binary as usual. - -- [ ] **Step 3: Pipeline switch** - -In `src/pipeline.rs::compile`, add a branch: - -```rust -let user_asm = if config.ir_backend { - let ir = crate::ir_lower::lower_program(/* args */); - crate::codegen_ir::generate_user_asm_from_ir(&ir, gc_stats, heap_debug) -} else { - crate::codegen::generate_user_asm(/* args */) -}; -``` - -The runtime path (`codegen::generate_runtime`) stays identical regardless of backend choice. - -- [ ] **Step 4: Stub test passes** - -```rust -// tests/ir_backend_smoke_test.rs -#[test] -#[ignore] -fn ir_backend_hello_world() { - // Will be unignored in Task 4 once smallest emitters work. - let out = compile_and_run_with(["--ir-backend"], ", // ValueId raw -> negative offset from x29 - pub total_slot_bytes: usize, -} - -pub fn allocate(func: &Function) -> ValuePlacement { - let mut placement = ValuePlacement { - slot_of: HashMap::new(), - total_slot_bytes: 0, - }; - let mut offset: i32 = 0; - for v in &func.values { - let bytes = bytes_for(v.ir_type); - if bytes == 0 { continue; } - offset -= bytes as i32; - let raw = match &v.def { - crate::ir::ValueDef::BlockParam { .. } | crate::ir::ValueDef::Instruction { .. } => { - // raw is the index in func.values - offset_into_func_values(func, v) - } - }; - placement.slot_of.insert(raw, offset); - } - placement.total_slot_bytes = (-offset as usize).next_multiple_of(16); - placement -} - -fn bytes_for(t: IrType) -> usize { - match t { - IrType::I64 | IrType::F64 | IrType::Heap(_) => 8, - IrType::Str => 16, - IrType::Void => 0, - } -} - -fn offset_into_func_values(func: &Function, v: &crate::ir::Value) -> u32 { - func.values.iter() - .position(|other| std::ptr::eq(other, v)) - .map(|i| i as u32) - .unwrap() -} -``` - -- [ ] **Step 2: Implement frame emission** - -```rust -// src/codegen_ir/frame.rs -//! Purpose: -//! Emits function prologue and epilogue using ABI helpers from `crate::codegen::abi`. -//! -//! Called from: -//! - `crate::codegen_ir::context::emit_function` -//! -//! Key details: -//! - Frame size = locals + value-placement slots, 16-byte aligned. - -use crate::codegen::abi::{self, frame}; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::ir::{Function, LocalKind}; - -pub fn emit_prologue(emitter: &mut Emitter, func: &Function, frame_bytes: usize) { - // Mirror src/codegen/functions/* prologue emission. Use abi::frame helpers. - // ARM64: - // sub sp, sp, #N - // stp x29, x30, [sp, #N-16] - // add x29, sp, #N-16 - // X86_64: - // push rbp - // mov rbp, rsp - // sub rsp, #N - abi::frame::emit_function_prologue(emitter, frame_bytes); -} - -pub fn emit_epilogue(emitter: &mut Emitter, frame_bytes: usize) { - abi::frame::emit_function_epilogue(emitter, frame_bytes); -} -``` - -(Confirm helper names by reading `src/codegen/abi/frame.rs` and adjusting; the existing AST backend already has the primitives we need.) - -- [ ] **Step 3: Test placement** - -A minimal test asserts that allocation for a function with one I64 value yields one 8-byte slot. - -- [ ] **Step 4: Commit** - -```bash -git add src/codegen_ir/frame.rs src/codegen_ir/value_placement.rs src/codegen_ir/tests/frame_test.rs -git commit -m "feat(codegen_ir): implement Phase 04 value placement and frame emission" -``` - ---- - -## Task 3: Implement block emission and terminators - -**Files:** -- Create: `src/codegen_ir/block_emit.rs` -- Create: `src/codegen_ir/lower_term.rs` -- Test: `src/codegen_ir/tests/block_test.rs` - -- [ ] **Step 1: Failing test** - -```rust -#[test] -fn emits_label_for_each_block() { - let asm = compile_simple_ir_to_asm(/* fn returning const 7 */); - assert!(asm.contains(".LBB_")); -} -``` - -- [ ] **Step 2 / 3: Implement** - -Block labels: `format!(".LBB_{fn_name}_{block_id}")`. Use `ctx.next_label(prefix)` style with a global counter so labels don't collide across functions (mirroring the existing emitter's label-counter discipline in `src/codegen/context.rs`). - -Block ordering: topological, with the entry block first. Fall-through optimization where possible: if the terminator of block N is `Br(N+1)`, omit the branch instruction. - -Terminator lowering: -- `Br(target, args)` — move args into the target's parameter slots, then `b .LBB_target`. -- `CondBr(cond, then, else_)` — load `cond` into a register, `cbz reg, else_label`, branch to `then_label` (or fall-through if next block is `then`). -- `Switch` — if dense, emit a jump table from `src/codegen/runtime/data.rs` patterns; otherwise chained `cmp`+`b.eq`. -- `Return(v)` — move `v` into the result register (`x0`/`d0`/`x1+x2` per type), branch to epilogue. -- `Throw(v)` — call `__rt_throw` with `v` in `x0`. -- `Fatal { message_id }` — call `__rt_fatal` with message pointer. -- `Unreachable` — `udf #0` on ARM64 / `ud2` on x86_64. - -Block-parameter move scheduling: when a `Br(target, [v0, v1])` enters a block with params `[p0, p1]`, copy `v0 -> slot(p0)`, `v1 -> slot(p1)`. Handle parallel-move semantics with a topological sort + cycle break (one extra scratch register, mirroring how `src/codegen/abi/values.rs` handles register shuffles). - -- [ ] **Step 4: Commit** - -```bash -git add src/codegen_ir/block_emit.rs src/codegen_ir/lower_term.rs src/codegen_ir/tests/block_test.rs -git commit -m "feat(codegen_ir): emit blocks, labels, and terminators" -``` - ---- - -## Task 4: Lower scalar arithmetic and comparison - -**Files:** -- Create: `src/codegen_ir/lower_inst.rs` (dispatcher) -- Create: `src/codegen_ir/lower_inst/arithmetic.rs` -- Create: `src/codegen_ir/lower_inst/floats.rs` -- Create: `src/codegen_ir/lower_inst/comparison.rs` -- Modify: `tests/ir_backend_smoke_test.rs` to unignore the hello-world test - -- [ ] **Step 1: Failing test** - -Unignore `ir_backend_hello_world`. It expects `42`. Will fail until scalar lowering is in place. - -- [ ] **Step 2: Implement scalar arithmetic** - -Each op emits the same instruction sequence the AST backend emits today, with operands loaded from the placement slots: - -``` -// Op::IAdd, operands [a, b], result r: -ldr x1, [x29, #slot(a)] // load left operand from slot -ldr x0, [x29, #slot(b)] // load right operand from slot -add x0, x1, x0 // add operands, result in x0 -str x0, [x29, #slot(r)] // store result to result slot -``` - -Each `emitter.instruction(...)` MUST have a `// ` comment at column 81 per `CLAUDE.md` policy. - -- [ ] **Step 3: Run smoke test** - -Run: `cargo test --test ir_backend_smoke_test` -Expected: pass (prints `42`). - -- [ ] **Step 4: Commit** - -```bash -git add src/codegen_ir/lower_inst.rs src/codegen_ir/lower_inst/arithmetic.rs src/codegen_ir/lower_inst/floats.rs src/codegen_ir/lower_inst/comparison.rs tests/ir_backend_smoke_test.rs -git commit -m "feat(codegen_ir): lower scalar arithmetic and comparison" -``` - ---- - -## Task 5: Lower constants, locals, conversions - -**Files:** -- Create: `src/codegen_ir/lower_inst/conversion.rs` -- Modify: `src/codegen_ir/lower_inst.rs` -- Test: `src/codegen_ir/tests/conversions_test.rs` - -- [ ] **Step 1 / 2 / 3 / 4 / 5**: TDD pattern as before. - -`ConstI64` / `ConstF64` / `ConstStr` / `ConstNull` lower to literal-load patterns from the existing AST emitter (use the same data-section interning). - -`LoadLocal(slot_id)` / `StoreLocal(slot_id)` lower to `ldr`/`str` against the PHP local slot (separate from value-placement slots — locals are named, values are anonymous). `IToF` / `FToI` / `IToStr` etc. call the existing `__rt_*` routines. - -- [ ] **Step 6: Commit** - ---- - -## Task 6: Lower strings, arrays, hashes, objects - -Five separate commits, one per file: - -- [ ] **Step 1**: `lower_inst/strings.rs` — reuse `__rt_str_concat`, `__rt_str_persist`, `__rt_str_char_at` runtime routines. The emitter just sets up arguments and `bl`s. - -- [ ] **Step 2**: `lower_inst/arrays.rs` — reuse `__rt_array_new`, `__rt_array_get_int`, `__rt_array_set_int`, `__rt_array_push`, `__rt_array_cow_ensure`. - -- [ ] **Step 3**: `lower_inst/hashes.rs` — reuse `__rt_hash_*`, `__rt_iter_*`. - -- [ ] **Step 4**: `lower_inst/objects.rs` — reuse `__rt_object_alloc`, inline vtable lookup pattern from existing class-methods emission. - -- [ ] **Step 5**: Tests + commits per cluster. - -Read the existing emitter file (e.g., `src/codegen/expr/arrays/`) for each cluster and reproduce its assembly pattern. - ---- - -## Task 7: Lower calls, builtins, externs, closures - -**Files:** -- Create: `src/codegen_ir/lower_inst/calls.rs` - -This is the most surface-area cluster. Recipe: - -- **`Call(func_id, args)`**: - 1. Look up `func_id` in `module.data.function_names` → function symbol. - 2. Use ABI helpers (`abi::values::*`) to place args in registers/stack: int args in `x0..x7`, floats in `d0..d7`, stack overflow per ABI. - 3. Emit `bl ` (ARM64) / `call ` (x86_64). - 4. Move result from result register into the result value's slot. - -- **`IndirectCall(fn_ptr, sig, args)`**: same but `blr ` / `call `. - -- **`MethodCall(obj, method_id, args)`**: - 1. Load `obj` into the first arg register (`x0` ARM64, `rdi` x86_64). - 2. `VTableLookup` produced a function-pointer SSA value earlier; emit indirect call. - 3. Same arg placement and result handling. - -- **`BuiltinCall(builtin, args)`**: dispatch to the existing builtin emitters in `src/codegen/builtins/`. The IR knows which builtin, the lowering knows where to find its codegen. This is the largest opcode — *do not* re-implement each builtin; call into the existing emitter helpers with the operand values placed appropriately. - -- **`RuntimeCall(rt_routine, args)`**: similar to `BuiltinCall` but always inline `bl __rt_`. - -- **`ExternCall(name, args)`**: same as `Call` but with C-ABI conversions on strings (already handled by `src/codegen/ffi.rs` — reuse). - -- **`ClosureNew`**: allocate closure object on heap with captured environment. Reuse `src/codegen/functions/closures.rs` patterns. - -- [ ] **Step 1 / 2 / 3 / 4**: TDD pattern. -- [ ] **Step 5: Commit** - -```bash -git commit -m "feat(codegen_ir): lower calls, builtins, externs, closures" -``` - ---- - -## Task 8: Lower ownership ops - -**Files:** -- Create: `src/codegen_ir/lower_inst/ownership.rs` - -- `Acquire(v)` → `bl __rt_incref` with `v` in `x0`. -- `Release(v)` → `bl __rt_decref_any` with `v` in `x0`. -- `Move(v)` → no-op (semantic only). Validator already checked balance. -- `Borrow(v)` → no-op (semantic only). - -- [ ] **Step 1: Failing test**: a fixture that allocates an array, assigns over it (forcing release of the old), and checks `--gc-stats` reports correct alloc/free counts. - -- [ ] **Step 2 / 3 / 4: Implement and verify.** - -- [ ] **Step 5: Commit** - -```bash -git commit -m "feat(codegen_ir): lower Acquire/Release ownership ops" -``` - ---- - -## Task 9: Function-level driver - -**Files:** -- Create: `src/codegen_ir/context.rs` -- Modify: `src/codegen_ir/mod.rs` - -- [ ] **Step 1: Implement `emit_function`** - -```rust -// src/codegen_ir/context.rs -pub struct BackendContext<'e> { - pub emitter: &'e mut Emitter, - pub placement: ValuePlacement, - pub func: &'e Function, - pub data: &'e mut DataSection, -} - -pub fn emit_function(emitter: &mut Emitter, data: &mut DataSection, func: &Function) { - let placement = value_placement::allocate(func); - let locals_bytes = func.locals.iter().map(|s| s.php_type.stack_size()).sum::(); - let frame_bytes = ((locals_bytes + placement.total_slot_bytes).next_multiple_of(16)) + 16; - - emit_function_label(emitter, &func.name); - frame::emit_prologue(emitter, func, frame_bytes); - block_emit::emit_blocks(emitter, &mut BackendContext { - emitter, placement, func, data, - }); - // Epilogue is emitted by Return terminators. -} -``` - -- [ ] **Step 2: Wire `generate_user_asm_from_ir`** - -```rust -// src/codegen_ir/mod.rs (fix the unimplemented! from Task 1) -pub fn generate_user_asm_from_ir(module: &Module, _gc_stats: bool, _heap_debug: bool) -> String { - let mut emitter = Emitter::new(module.target); - if module.target.arch == Arch::X86_64 { - emitter.emit_text_prelude(); - } - let mut data = DataSection::new(); - // Translate module.data into data_section. - seed_data_section(&mut data, &module.data); - - for f in &module.functions { - context::emit_function(&mut emitter, &mut data, f); - } - for f in &module.class_methods { - context::emit_function(&mut emitter, &mut data, f); - } - // Interface return wrappers and main emission stay in src/codegen/ for now; - // Phase 09 either ports them or keeps them via a shared helper. - crate::codegen::emit_main_and_finalize_from_ir(/* ... */) -} -``` - -The `emit_main_and_finalize_from_ir` is a new entry point that mirrors `src/codegen/main_emission.rs::emit_main_and_finalize` but reads the `main` function body from the IR module (the AST → IR pass produces a `main` function). Add this helper to `src/codegen/main_emission.rs` in a separate commit if its surface is too small to deserve its own file. - -- [ ] **Step 3: Commit** - -```bash -git commit -m "feat(codegen_ir): wire generate_user_asm_from_ir end-to-end" -``` - ---- - -## Task 10: Parity testing — run every existing codegen test with `--ir-backend` - -**Files:** -- Create: `src/codegen_ir/tests/parity.rs` - -- [ ] **Step 1: Replay codegen tests through `--ir-backend`** - -For each `tests/codegen/*.rs` test, the standard helper is `compile_and_run`. Add a sibling `compile_and_run_ir` (or a flag on the existing helper) that compiles with `--ir-backend` set. Then **mirror every test** in a new `tests/ir_backend_parity/` directory. - -A pragmatic approach: a single integration test file that walks the public test fixtures (where available) and asserts each `--ir-backend` output matches the `compile_and_run` (legacy) output. Where tests use inline source, the parity test re-runs the same inline source with the IR backend. - -The Docker Linux scripts must be run as well. Use `./scripts/test-linux-*.sh ir_backend` to filter to the new tests. - -- [ ] **Step 2: Iterate until parity** - -This is where most of Phase 04's calendar time lives. Each parity failure points at: -- A missing or incorrectly emitted opcode -- An ABI mismatch -- An ownership op imbalance -- A wrong effect annotation -- A wrong block-parameter move sequencing - -Fix one failure at a time, with a focused regression test in `tests/ir_backend_parity/`. - -- [ ] **Step 3: Commit each fix individually** - -Aim for many small commits (`fix(codegen_ir): correct stack alignment for str result`, `fix(codegen_ir): release order at if-merge`, etc.). This keeps the PR review tractable and lets the team revert specific regressions if needed. - ---- - -## Task 11: Final parity gate - -- [ ] **Step 1: Run the gates** - -```bash -cargo build -cargo test # legacy backend (default) -cargo test -- --include-ignored # legacy backend incl. SDL2 etc. -cargo test --features ir-backend # if a feature flag is used; else use env or a separate test runner -./scripts/test-linux-x86_64.sh -./scripts/test-linux-arm64.sh -``` - -The new `tests/ir_backend_parity/` corpus must pass with `--ir-backend` enabled at the binary level. - -- [ ] **Step 2: Benchmark parity (no regression expected, possibly small slowdown)** - -Run the benchmark harness with `--ir-backend` and verify there is *no major regression* compared to the legacy backend on compute benchmarks. A 0–20% slowdown is acceptable in Phase 04 — Phase 06 recovers this and more. - -If a >50% slowdown appears on any benchmark, stop and diagnose before claiming Phase 04 done. Likely cause: redundant slot loads (every value spilled and re-loaded). This is expected, but pathological cases (deep call chains, hot loop bodies) may need a minimal Phase 04 mitigation: keep up to 4 SSA values "pinned" in callee-saved scratch registers across the loop body. Document the mitigation in `docs/internals/the-codegen.md`. - -- [ ] **Step 3: Commit final benchmark output as a baseline** - -```bash -git add benchmarks/ir_backend_phase4_baseline.json -git commit -m "perf(codegen_ir): record phase 04 baseline (parity, no opt yet)" -``` - ---- - -## Exit criteria - -- Every `tests/codegen/` fixture passes with `--ir-backend` set. -- Docker Linux gates green for `--ir-backend`. -- Benchmark regression bounded (≤20% slowdown vs legacy on any single benchmark). -- Legacy backend untouched and still default. -- Zero compiler warnings. -- Each `emitter.instruction(...)` call in `src/codegen_ir/` has a column-81 `//` comment per project policy. diff --git a/.plans/eir-05-switchover-behind-flag.md b/.plans/eir-05-switchover-behind-flag.md deleted file mode 100644 index b959248161..0000000000 --- a/.plans/eir-05-switchover-behind-flag.md +++ /dev/null @@ -1,208 +0,0 @@ -# Phase 05 - Switch EIR Backend to Default - -> **Current status:** Historical phase plan. The EIR switch is complete, and -> the legacy AST backend is now frozen as a diagnostic-only fallback. Do not use -> this plan to justify new work on the legacy backend; follow `AGENTS.md` and -> `ROADMAP.md` instead. - -> **For agentic workers:** This historical phase shipped the EIR backend as the -> user-facing default. The legacy AST backend is now frozen behind the explicit -> `--ast-backend` diagnostic fallback while the old emitter remains in-tree. No -> optimization work belongs in the legacy path. - -**Goal:** Move the default backend to EIR now that parity gates are green. Keep -the legacy backend frozen as a diagnostic-only fallback until it is removed in -Phase 09. - -**Architecture:** `src/cli.rs` owns backend selection, and `src/pipeline.rs` -uses that selection to choose AST -> ASM or AST -> EIR -> ASM. CI should run -the EIR backend on the normal path and keep only frozen fallback smoke coverage. - -**Tech Stack:** Rust, existing CLI argument parser, existing CI configuration. - ---- - -## File Structure - -No new modules. Edits to: - -- Modify: `src/cli.rs` - backend flags and default -- Modify: `src/pipeline.rs` - backend selection plumbing if needed -- Modify: `docs/internals/the-codegen.md` - document EIR as the default backend -- Modify: `docs/internals/the-ir.md` - promote from preview language to default-backend language -- Modify: `.github/workflows/*.yml` or equivalent - CI coverage for default EIR and legacy fallback -- Modify: `Cargo.toml` - version bump once Phase 05 ships -- Modify: `ROADMAP.md` - tick off Phase 05 entries - ---- - -## Task 1: Make EIR the default backend - -**Files:** -- Modify: `src/cli.rs` -- Modify: `src/pipeline.rs` -- Modify: `docs/internals/the-ir.md` - -- [ ] **Step 1: Change backend selection defaults** - -In `src/cli.rs`, make EIR the default backend. Keep: - -- `--ir-backend`: explicit selection of the default EIR backend -- `--ast-backend`: explicit fallback to the legacy AST backend - -If both flags are set, fail with: - -```text -cannot use --ir-backend and --ast-backend together -``` - -- [ ] **Step 2: Document backend selection** - -Add a "Backend selection" section to `docs/internals/the-ir.md`: - -```markdown -## Backend selection - -The compiler currently supports two backends: - -- EIR backend (default): lowers the AST to EIR first, then emits ASM from EIR -- `--ast-backend`: legacy fallback that walks the AST directly and emits ASM - -Use `--ir-backend` to select the default explicitly. Use `--ast-backend` only -as a temporary fallback while the legacy emitter remains in-tree. - -The EIR backend is feature-complete against the supported test matrix. It -currently does not register-allocate; register allocation is a later v0.24.x -task. -``` - -- [ ] **Step 3: Commit** - -```bash -git add src/cli.rs src/pipeline.rs docs/internals/the-ir.md -git commit -m "feat: switch default backend to eir" -``` - ---- - -## Task 2: CI default-EIR and legacy fallback coverage - -**Files:** -- Modify: `.github/workflows/ci.yml` - -- [ ] **Step 1: Audit existing CI** - -Run `find .github/workflows -name '*.yml'`, or check the repository's -equivalent CI configuration. - -- [ ] **Step 2: Run ordinary CI with the default EIR backend** - -Ensure the normal CI path runs `cargo test` without selecting the legacy -backend. This makes EIR the default gate. - -- [ ] **Step 3: Add explicit legacy fallback coverage** - -Add a smaller explicit legacy job or matrix entry that invokes the compiler -with `--ast-backend` for the codegen/parity surfaces that still need fallback -coverage. - -- [ ] **Step 4: Add an EIR benchmark job** - -Benchmarks already exist. Add a job that runs them with the default EIR backend -and stores results in `benchmarks/results/ir/`. - -- [ ] **Step 5: Commit** - -```bash -git add .github/workflows/ci.yml -git commit -m "ci: make eir the default backend gate" -``` - ---- - -## Task 3: Mark legacy AST backend deprecated - -**Files:** -- Modify: `src/cli.rs` -- Modify: `src/codegen/mod.rs` -- Modify: `docs/internals/the-codegen.md` - -- [ ] **Step 1: Emit a deprecation note** - -When `--ast-backend` is passed, print to stderr: - -```text -warning: --ast-backend is deprecated and will be removed in v0.26.0. The EIR -backend is now the default. See docs/internals/the-ir.md for details. -``` - -Only emit once per compilation. Do not fail. - -- [ ] **Step 2: Add `#[deprecated]` to crate-internal entry points used only by the legacy backend** - -Only on public-ish entry points such as `codegen::generate_user_asm` and -`codegen::generate`. Internal helpers stay untouched. - -- [ ] **Step 3: Update `the-codegen.md`** - -At the top of `docs/internals/the-codegen.md`, explain that EIR is the default -backend and that the legacy AST backend is documented only as a frozen -diagnostic-only fallback implementation. - -- [ ] **Step 4: Commit** - -```bash -git add src/cli.rs src/codegen/mod.rs docs/internals/the-codegen.md -git commit -m "chore: mark ast backend deprecated" -``` - ---- - -## Task 4: Release notes and version - -**Files:** -- Modify: `Cargo.toml` -- Modify: release notes location, if present -- Modify: `ROADMAP.md` - -- [ ] **Step 1: Write release notes** - -Add a "Backend rework - what changed and why" section to the v0.24.0 release -notes. Explain: - -- EIR is now the default backend -- `--ir-backend` remains accepted as an explicit default selection -- `--ast-backend` is the temporary legacy fallback -- register allocation and IR optimization are still future work - -- [ ] **Step 2: Bump version** - -```toml -[package] -version = "0.24.0" -``` - -- [ ] **Step 3: Update ROADMAP** - -Mark EIR parity and the default switch completed under v0.24.x. Leave register -allocation and register-pressure mitigation as the remaining v0.24.x work. - -- [ ] **Step 4: Commit** - -```bash -git add Cargo.toml ROADMAP.md -git commit -m "chore: prepare v0.24 eir default" -``` - ---- - -## Exit Criteria - -- EIR backend is the default backend. -- `--ir-backend` still works as an explicit default selection. -- `--ast-backend` still works as a legacy fallback and warns on use. -- Normal CI uses the EIR backend. -- Legacy fallback has explicit targeted CI coverage. -- Benchmark suite has an EIR baseline. -- Documentation reflects the new default. -- ROADMAP is up to date. diff --git a/.plans/eir-07-peephole-and-local-opts.md b/.plans/eir-07-peephole-and-local-opts.md deleted file mode 100644 index 09a6a6e25f..0000000000 --- a/.plans/eir-07-peephole-and-local-opts.md +++ /dev/null @@ -1,335 +0,0 @@ -# Phase 07 — IR-Level Peephole and Local Optimizations - -> **For agentic workers:** Add a set of small, local, fixed-point IR optimizations that pay off measurably. Expected gain: 5–10% on top of Phase 06. - -**Goal:** Implement on-the-IR optimizations that the AST-level passes cannot reach: redundant move elimination, dead store elimination, identity arithmetic folding, branch chain shortening, and per-block constant propagation. - -**Architecture:** Each optimization is a `Pass` over a `Function` producing a new `Function` (or mutating in place where safe). All passes run inside a fixed-point loop until no pass reports changes. Validation runs after every pass during testing. - -**Tech Stack:** Rust, EIR module. No new dependencies. - ---- - -## File Structure - -- Create: `src/ir_passes/peephole.rs` — peephole patterns -- Create: `src/ir_passes/dead_inst.rs` — DCE on instructions whose result is unused and effect-free -- Create: `src/ir_passes/dead_store.rs` — eliminate stores to locals never re-read -- Create: `src/ir_passes/branch_simplify.rs` — chain-of-branches collapse, constant-cond folding -- Create: `src/ir_passes/identity_fold.rs` — `x + 0`, `x * 1`, `x | 0`, `x ^ 0`, `x - x`, etc. -- Create: `src/ir_passes/move_elimination.rs` — remove `Move`/`Borrow` no-ops that the allocator left untouched -- Create: `src/ir_passes/pass_driver.rs` — fixed-point driver -- Modify: `src/ir_passes/mod.rs` — re-export -- Modify: `src/pipeline.rs` — invoke pass pipeline after lowering, before register allocation - ---- - -## Task 1: Pass driver - -**Files:** -- Create: `src/ir_passes/pass_driver.rs` -- Test: `src/ir_passes/tests/pass_driver_test.rs` - -- [ ] **Step 1: Failing test** - -```rust -#[test] -fn driver_runs_to_fixed_point() { - let mut func = build_function_with_redundant_pattern(); - let report = run_pass_pipeline(&mut func, PassConfig::default()); - assert!(report.iterations >= 1); - assert!(report.changed); - assert!(no_redundant_patterns(&func)); -} -``` - -- [ ] **Step 2: Implement driver** - -```rust -//! Purpose: -//! Drives the fixed-point loop over IR optimization passes. -//! -//! Called from: -//! - `crate::pipeline::compile()` after lowering, before register allocation -//! -//! Key details: -//! - Passes report `Changed::Yes` if they modified the function; the driver -//! re-runs the pipeline until all passes report `Changed::No` in one round. -//! - Capped at `MAX_ITERATIONS` to prevent oscillation bugs. - -use crate::ir::Function; - -const MAX_ITERATIONS: usize = 16; - -#[derive(Debug, Default)] -pub struct PassConfig { - pub run_peephole: bool, - pub run_identity_fold: bool, - pub run_branch_simplify: bool, - pub run_dead_inst: bool, - pub run_dead_store: bool, - pub run_move_elimination: bool, -} - -impl PassConfig { - pub fn all_phase_07() -> Self { - Self { - run_peephole: true, - run_identity_fold: true, - run_branch_simplify: true, - run_dead_inst: true, - run_dead_store: true, - run_move_elimination: true, - } - } -} - -pub struct PassReport { - pub iterations: usize, - pub changed: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Changed { Yes, No } - -pub fn run_pass_pipeline(func: &mut Function, cfg: PassConfig) -> PassReport { - let mut total_changed = false; - for i in 0..MAX_ITERATIONS { - let mut iter_changed = false; - if cfg.run_move_elimination { - iter_changed |= matches!(super::move_elimination::run(func), Changed::Yes); - } - if cfg.run_identity_fold { - iter_changed |= matches!(super::identity_fold::run(func), Changed::Yes); - } - if cfg.run_peephole { - iter_changed |= matches!(super::peephole::run(func), Changed::Yes); - } - if cfg.run_branch_simplify { - iter_changed |= matches!(super::branch_simplify::run(func), Changed::Yes); - } - if cfg.run_dead_inst { - iter_changed |= matches!(super::dead_inst::run(func), Changed::Yes); - } - if cfg.run_dead_store { - iter_changed |= matches!(super::dead_store::run(func), Changed::Yes); - } - total_changed |= iter_changed; - crate::ir::validate_function(func).expect("invariant broken by IR pass"); - if !iter_changed { - return PassReport { iterations: i + 1, changed: total_changed }; - } - } - panic!("IR pass pipeline did not converge in {} iterations", MAX_ITERATIONS); -} -``` - -- [ ] **Step 3 / 4 / 5: Test and commit** - -```bash -git commit -m "feat(ir_passes): pass driver with fixed-point loop" -``` - ---- - -## Task 2: Identity fold - -**Files:** -- Create: `src/ir_passes/identity_fold.rs` -- Test: `src/ir_passes/tests/identity_fold_test.rs` - -Patterns to fold: - -- `IAdd(x, 0)` / `IAdd(0, x)` → `x` -- `ISub(x, 0)` → `x` -- `IMul(x, 1)` / `IMul(1, x)` → `x` -- `IMul(x, 0)` / `IMul(0, x)` → `ConstI64(0)` -- `IBitAnd(x, 0)` → `ConstI64(0)` -- `IBitOr(x, 0)` → `x` -- `IBitXor(x, x)` → `ConstI64(0)` -- `IShl(x, 0)` → `x`, `IShrA(x, 0)` → `x` -- `FMul(x, 1.0)` → `x`; do NOT fold `FAdd(x, 0.0)` (signed zero / -0.0) -- `ICmp(Eq, x, x)` → `ConstI64(1)` if `x` is not `F64` (NaN-aware) - -Each pattern is one match arm. - -- [ ] **Step 1**: failing tests for at least 8 patterns above. -- [ ] **Step 2 / 3 / 4**: implement and verify. -- [ ] **Step 5: Commit** - -```bash -git commit -m "feat(ir_passes): identity-fold pass" -``` - ---- - -## Task 3: Peephole patterns - -**Files:** -- Create: `src/ir_passes/peephole.rs` -- Test: `src/ir_passes/tests/peephole_test.rs` - -Patterns to match: - -- **Redundant load after store**: `StoreLocal(slot, v); LoadLocal(slot)` → use `v` directly. -- **Box-unbox cancellation**: `MixedBox(v); MixedUnbox(_, tag=v.tag)` → `v`. -- **Acquire-Release pairs with no use between**: cancel if `v` is not observed otherwise. -- **String literal concat folding**: `StrConcat(ConstStr a, ConstStr b)` → `ConstStr "ab"` (interning into data pool). -- **Coalesced casts**: `IToF; FToI` → identity (only when round-trip is exact, e.g., integer literals). - -Each pattern is one helper in `peephole.rs`. The driver calls them all and reports whether anything matched. - -- [ ] **Step 1: Failing tests per pattern** -- [ ] **Step 2 / 3 / 4: Implement** -- [ ] **Step 5: Commit** - -```bash -git commit -m "feat(ir_passes): peephole pass with five core patterns" -``` - ---- - -## Task 4: Branch simplification - -**Files:** -- Create: `src/ir_passes/branch_simplify.rs` -- Test: `src/ir_passes/tests/branch_simplify_test.rs` - -Patterns: - -- **Constant-condition `CondBr`**: if the condition is `ConstI64(0)`, replace with `Br(else_block)`. If `ConstI64(non-zero)`, replace with `Br(then_block)`. -- **Trivially equivalent CondBr branches**: if `then_block == else_block` (and args match), replace with `Br(then_block)`. -- **Empty-block jump-thread**: a block that has no instructions and an unconditional `Br(next)` is collapsed: predecessors branch directly to `next`. Safe only when the block has no parameters whose values come from multiple distinct predecessor sources (otherwise parameter substitution must be applied). -- **Dead block removal**: after simplification, blocks unreachable from the entry are removed. - -- [ ] **Step 1 / 2 / 3 / 4 / 5**: TDD pattern. - -```bash -git commit -m "feat(ir_passes): branch simplification pass" -``` - ---- - -## Task 5: Dead instruction elimination - -**Files:** -- Create: `src/ir_passes/dead_inst.rs` -- Test: `src/ir_passes/tests/dead_inst_test.rs` - -An instruction is dead when: - -1. Its result is unused by any other instruction or terminator. -2. Its effects are `PURE` or limited to `READS_LOCAL` (which is also safe to drop since no observable state changes). - -Iterate until fixed point. (The driver gives us fixed point for free; one pass per iteration suffices.) - -Patterns to be careful about: -- Calls with unused returns may still have side effects. Check `Effects::may_mutate() || may_observe()` — if either, keep the call. -- `Acquire`/`Release` ops change refcounts; never remove individually. The move-elimination pass handles paired removal. - -- [ ] **Step 1 / 2 / 3 / 4 / 5** - -```bash -git commit -m "feat(ir_passes): dead instruction elimination" -``` - ---- - -## Task 6: Dead store elimination - -**Files:** -- Create: `src/ir_passes/dead_store.rs` -- Test: `src/ir_passes/tests/dead_store_test.rs` - -A `StoreLocal(slot, v)` is dead when: - -1. There is no `LoadLocal(slot)` reachable from this store before either another `StoreLocal(slot, _)` (no reads in between) or end-of-function. -2. The slot is not aliased through `Global`/`Static`/`ByRef` semantics. - -Care: -- Slots backing `Global` / `Static` PHP locals can be observed via `LoadGlobal`. The dead-store pass must ignore these slots. -- Slots passed to closures via capture: also not dead — the closure may read them later. - -Implementation: per-slot, walk the CFG; mark stores reachable to a load as live; the remaining stores are dead. - -- [ ] **Step 1 / 2 / 3 / 4 / 5** - -```bash -git commit -m "feat(ir_passes): dead store elimination" -``` - ---- - -## Task 7: Move elimination - -**Files:** -- Create: `src/ir_passes/move_elimination.rs` -- Test: `src/ir_passes/tests/move_elimination_test.rs` - -After register allocation, the lowering may emit `mov reg_a, reg_b` because the allocator placed both ends in different registers. Some of those can be removed: - -- `mov reg, reg` (same register) — always remove. -- Paired `Acquire(v); Release(v)` with no intervening use of `v` — remove both. -- `Move(v)` IR ops are no-ops at codegen; remove them from the IR after they've served their semantic-validation purpose. This is a "make it tidy" pass; impact on perf is negligible. - -This pass runs both *before* register allocation (to remove pure `Move`/`Borrow` ops that influence interval computation) and *after* (to clean up identity `mov`s the allocator left). - -- [ ] **Step 1 / 2 / 3 / 4 / 5** - -```bash -git commit -m "feat(ir_passes): move elimination" -``` - ---- - -## Task 8: Integrate and benchmark - -**Files:** -- Modify: `src/pipeline.rs` -- Test: existing benchmark suite - -- [ ] **Step 1: Run the pipeline** - -```rust -let mut func = func; // mutable -ir_passes::pass_driver::run_pass_pipeline(&mut func, PassConfig::all_phase_07()); -let alloc = ir_passes::allocate_registers(&func, target); -``` - -Apply to every function in the module. - -- [ ] **Step 2: Full test suite** - -```bash -cargo test -cargo test -- --include-ignored -./scripts/test-linux-x86_64.sh -./scripts/test-linux-arm64.sh -``` - -Expected: all green. - -- [ ] **Step 3: Benchmark gate** - -Run benchmarks. Expected: **5–10% improvement** over Phase 06 baseline on compute benchmarks, smaller (~2–3%) on benchmarks dominated by builtin calls or I/O. - -If less than 3% improvement, investigate which pass is paying off: -1. Run the pipeline with `PassConfig` selectively disabling each pass. -2. Identify the pass with the smallest contribution; ensure it's actually firing on real workloads (instrument the pass driver with per-pass change counts). - -- [ ] **Step 4: Commit** - -```bash -git commit -m "feat(ir_passes): integrate phase 07 optimization pipeline" -``` - ---- - -## Exit criteria - -- All passes integrated and gated by validator -- Pipeline converges in ≤16 iterations on every test fixture -- Test suite green -- Benchmark suite shows additional ≥3% gain over Phase 06 baseline (cumulatively, ≥18% vs Phase 04) -- Per-pass change counts logged so future tuning has a baseline -- Documentation: `docs/internals/the-ir.md` gains "Optimization passes" section listing each pass and what it does diff --git a/.plans/eir-08-cse-licm-inlining.md b/.plans/eir-08-cse-licm-inlining.md deleted file mode 100644 index 1c696bd6e5..0000000000 --- a/.plans/eir-08-cse-licm-inlining.md +++ /dev/null @@ -1,391 +0,0 @@ -# Phase 08 — CSE, LICM, and Inlining - -> **For agentic workers:** Add global IR-level optimizations: common subexpression elimination, loop-invariant code motion, and small-function inlining. Expected cumulative gain: 10–20% on loop-heavy workloads on top of Phase 07. - -**Goal:** Implement three optimizations that need basic-block reasoning and dominance information — operations that AST-level passes cannot do. - -**Architecture:** Three new IR passes plus a dominance analysis. All passes are *opt-in* via the existing `PassConfig` from Phase 07 and run in the fixed-point pipeline. - -**Tech Stack:** Rust, existing IR module. No new dependencies. - ---- - -## File Structure - -- Create: `src/ir_passes/dominance.rs` — dominator tree, dominance frontier, immediate-dominator table -- Create: `src/ir_passes/cse.rs` — common subexpression elimination -- Create: `src/ir_passes/licm.rs` — loop-invariant code motion -- Create: `src/ir_passes/loops.rs` — natural loop detection (back edges, headers, loop bodies) -- Create: `src/ir_passes/inliner.rs` — inline candidate analysis and inlining transformation -- Modify: `src/ir_passes/mod.rs` — re-export -- Modify: `src/ir_passes/pass_driver.rs` — add Phase 08 passes to `PassConfig` -- Modify: `src/pipeline.rs` — call the inliner before the rest of the pass pipeline - ---- - -## Task 1: Dominance analysis - -**Files:** -- Create: `src/ir_passes/dominance.rs` -- Test: `src/ir_passes/tests/dominance_test.rs` - -- [ ] **Step 1: Failing test** - -```rust -#[test] -fn entry_dominates_all_blocks() { - let f = build_diamond_cfg(); - let dom = compute_dominance(&f); - let entry = f.entry; - for b in &f.blocks { - assert!(dom.dominates(entry, b.id)); - } -} - -#[test] -fn diamond_blocks_dominator_is_entry() { - let f = build_diamond_cfg(); - let dom = compute_dominance(&f); - let merge = f.blocks.last().unwrap().id; - assert_eq!(dom.immediate_dominator(merge), Some(f.entry)); -} -``` - -- [ ] **Step 2: Implement** - -Use the Lengauer-Tarjan algorithm or the simpler iterative Cooper-Harvey-Kennedy method. The latter is ~30 LOC and good enough for our small CFGs. - -```rust -//! Purpose: -//! Computes the immediate-dominator table and offers dominance queries. -//! -//! Called from: -//! - `crate::ir_passes::cse::run` -//! - `crate::ir_passes::licm::run` -//! -//! Key details: -//! - Cooper-Harvey-Kennedy iterative dominators; small CFG so cost is negligible. - -use std::collections::HashMap; - -use crate::ir::{BlockId, Function}; - -pub struct Dominance { - pub idom: HashMap>, - pub depth: HashMap, -} - -impl Dominance { - pub fn immediate_dominator(&self, b: BlockId) -> Option { - self.idom.get(&b).copied().flatten() - } - pub fn dominates(&self, a: BlockId, b: BlockId) -> bool { - if a == b { return true; } - let mut cur = self.idom.get(&b).copied().flatten(); - while let Some(c) = cur { - if c == a { return true; } - cur = self.idom.get(&c).copied().flatten(); - } - false - } -} - -pub fn compute_dominance(func: &Function) -> Dominance { - unimplemented!() -} -``` - -- [ ] **Step 3 / 4 / 5: Test, pass, commit** - -```bash -git commit -m "feat(ir_passes): dominance analysis" -``` - ---- - -## Task 2: Loop detection - -**Files:** -- Create: `src/ir_passes/loops.rs` -- Test: `src/ir_passes/tests/loops_test.rs` - -- [ ] **Step 1: Failing tests**: detect a simple `while` loop and a nested loop. - -- [ ] **Step 2: Implement** - -Identify back edges (edges `u -> v` where `v` dominates `u`). Each back edge defines a natural loop: the set of blocks from which the source `u` is reachable without going through `v`. - -```rust -pub struct LoopInfo { - pub header: BlockId, - pub body: HashSet, - pub back_edges: Vec<(BlockId, BlockId)>, - pub preheader: Option, // synthesized by LICM if needed -} -``` - -- [ ] **Step 3 / 4 / 5**: Test and commit. - ---- - -## Task 3: Common Subexpression Elimination - -**Files:** -- Create: `src/ir_passes/cse.rs` -- Test: `src/ir_passes/tests/cse_test.rs` - -CSE finds two instructions that produce the same value and replaces uses of the second with the first. - -Conservative version (per-block): -- Within each block, hash instructions by `(op, operands, immediate)`. -- If a later instruction matches an earlier one *and* its effects are `PURE`, replace its result with the earlier one and drop the later instruction. - -Global version (cross-block, dominance-aware): -- Same hashing. -- When two matching instructions are in different blocks, only replace if the earlier dominates the later. -- Effects must remain `PURE` (or contain no observable bits). - -Start with per-block CSE; add cross-block CSE in a follow-up commit. - -- [ ] **Step 1**: failing tests for at least these patterns: - - `v1 = load_local slot[a]; v2 = load_local slot[a]` → `v2` becomes `v1`. (`LoadLocal` is `READS_LOCAL`, not pure — but it is safe if no `StoreLocal slot[a]` lies between def of `v1` and use point. The pass tracks per-block stores.) - - `v1 = iadd p0, p1; v2 = iadd p0, p1` → `v2` becomes `v1`. - - `v1 = array_get arr, 0; v2 = array_get arr, 0` — NOT CSE'd (effect = `READS_HEAP | MAY_FATAL`); careful here. - -- [ ] **Step 2 / 3 / 4: Implement** - -- [ ] **Step 5: Commit** - -```bash -git commit -m "feat(ir_passes): common subexpression elimination" -``` - ---- - -## Task 4: Loop-Invariant Code Motion - -**Files:** -- Create: `src/ir_passes/licm.rs` -- Test: `src/ir_passes/tests/licm_test.rs` - -LICM hoists `PURE` instructions out of loops when: -1. The instruction's operands are all loop-invariant (defined outside the loop or loop-invariant themselves). -2. The instruction has no side effects (`Effects::PURE`). -3. The instruction dominates all loop exits *OR* it is safe to execute speculatively (always true for pure instructions). - -The pass needs a *preheader* block — a block on the loop's entry edge with no other predecessors — to hoist into. If the loop doesn't have one, the pass synthesizes one. - -Patterns this catches that AST-level passes miss: -- Loop-invariant arithmetic: `for ($i = 0; $i < count($a); $i++) { $x = $a[0] + 1; ... }` — the `+ 1` and the `array_get a[0]` (if proven side-effect-free for the loop's array) are hoisted. -- Loop-invariant `count($a)`: when the loop doesn't mutate `$a`, the array length read is hoisted. - -Care: -- `count()` reads heap — not strictly pure. Adding a "safe within loop body" predicate based on memory dependencies is more sophisticated than the basic pass. For Phase 08, only hoist `PURE` operations. Memory-aware LICM is a v0.26 follow-up. - -- [ ] **Step 1: Failing tests** (3-5 patterns) -- [ ] **Step 2 / 3 / 4: Implement** - -```rust -//! Purpose: -//! Hoists pure loop-invariant instructions out of natural loops. -//! -//! Called from: -//! - `crate::ir_passes::pass_driver` when `run_licm` is enabled. -//! -//! Key details: -//! - Synthesizes a preheader if the loop entry edge has multiple predecessors. -//! - Only hoists `Effects::PURE` instructions; memory-effect-aware LICM is a -//! v0.26 follow-up. - -pub fn run(func: &mut crate::ir::Function) -> super::pass_driver::Changed { - // 1. compute dominance - // 2. detect loops - // 3. for each loop, find invariant instructions - // 4. ensure preheader exists; if not, synthesize one - // 5. move invariants to preheader, in dependency order - unimplemented!() -} -``` - -- [ ] **Step 5: Commit** - -```bash -git commit -m "feat(ir_passes): loop-invariant code motion" -``` - ---- - -## Task 5: Small-function inliner - -**Files:** -- Create: `src/ir_passes/inliner.rs` -- Test: `src/ir_passes/tests/inliner_test.rs` - -The inliner picks **small callees with no recursion and no exception handlers** and substitutes their bodies into the call site. Benefits: - -- Eliminates call/return overhead. -- Exposes additional CSE/LICM opportunities post-inline. -- Allows the register allocator to see the inlined body as part of the caller's live ranges. - -Cost model: - -- **Size threshold**: callee instruction count ≤ `INLINE_THRESHOLD` (start at 24 instructions). -- **No-recursion**: callee does not reach itself transitively in the call graph. -- **No exception handlers**: callee has no `Throw`, no `Try`. (Inlining a function that may throw makes the caller's stack-unwinding contract more complex; defer this case.) -- **No generator / fiber bodies**: not inlinable. -- **No closures with hidden environment unless the captures are loop-invariant or constant**: defer to later. - -The pass runs *once* before the rest of the pipeline (not per fixed-point iteration), because each inlining substantially increases function size and IR pass cost. - -- [ ] **Step 1: Failing test** - -```rust -#[test] -fn inlines_small_pure_callee() { - // function double($x) { return $x * 2; } - // function compute() { return double(7); } - // After inline, compute() should contain "imul 7, 2" directly. - let module = build_two_function_module(); - inline_pass(&mut module, InlineConfig::default()); - let compute = &module.functions[1]; - let has_call = compute.instructions.iter().any(|i| matches!(i.op, crate::ir::Op::Call)); - assert!(!has_call); -} -``` - -- [ ] **Step 2: Implement** - -```rust -//! Purpose: -//! Substitutes small callees into call sites to eliminate call overhead and -//! expose downstream optimization. -//! -//! Called from: -//! - `crate::pipeline::compile()` once after AST → IR lowering, before the -//! IR pass pipeline. -//! -//! Key details: -//! - Inlining renumbers ValueIds, BlockIds, slot indices. The transformation -//! must rebuild internal tables consistently. - -use crate::ir::{Function, Module}; - -pub struct InlineConfig { - pub size_threshold: usize, - pub max_inline_depth: usize, -} - -impl Default for InlineConfig { - fn default() -> Self { - Self { size_threshold: 24, max_inline_depth: 3 } - } -} - -pub fn inline_pass(module: &mut Module, cfg: InlineConfig) { - let call_graph = build_call_graph(module); - let candidates = select_inline_candidates(module, &call_graph, &cfg); - for cand in candidates { - inline_at_callsites(module, cand); - } -} - -fn build_call_graph(_m: &Module) -> CallGraph { unimplemented!() } -fn select_inline_candidates(_m: &Module, _cg: &CallGraph, _cfg: &InlineConfig) -> Vec { unimplemented!() } -fn inline_at_callsites(_m: &mut Module, _c: InlineCandidate) { unimplemented!() } - -struct CallGraph; -struct InlineCandidate; -``` - -- [ ] **Step 3 / 4: Test and verify** - -- [ ] **Step 5: Commit** - -```bash -git commit -m "feat(ir_passes): inline small pure functions" -``` - ---- - -## Task 6: Integration and benchmark gate - -**Files:** -- Modify: `src/pipeline.rs` -- Modify: `src/ir_passes/pass_driver.rs` - -- [ ] **Step 1: Wire passes into pipeline** - -```rust -// Phase 08 pipeline shape -let mut module = ir_lower::lower_program(/* ... */); -ir_passes::inliner::inline_pass(&mut module, InlineConfig::default()); - -for func in module.functions.iter_mut().chain(module.class_methods.iter_mut()) { - ir_passes::pass_driver::run_pass_pipeline(func, PassConfig::all_phase_08()); -} - -// Phase 06 register allocation runs as before. -``` - -`PassConfig::all_phase_08()` enables Phase 07 passes plus CSE and LICM. - -- [ ] **Step 2: Full test gate** - -```bash -cargo test -cargo test -- --include-ignored -./scripts/test-linux-x86_64.sh -./scripts/test-linux-arm64.sh -``` - -Expected: all green. - -- [ ] **Step 3: Benchmark gate** - -```bash -./scripts/run-benchmarks.sh --backend ir --baseline phase7 -``` - -Expected gains: -- **Tight loop benchmarks** (fibonacci, prime sieve, mandelbrot): ≥10% over Phase 07. -- **Function-call-heavy benchmarks**: ≥15% over Phase 07 (inlining payoff). -- **I/O-bound or builtin-bound benchmarks**: ≤5% over Phase 07 (CSE/LICM payoff small here). - -If gain is below target, prioritize debugging: -1. Verify each pass is firing — pass_driver instrumentation per-pass change counts. -2. Look at `--emit-asm` output of a hot benchmark function before and after to ensure inlining and hoisting actually happened. -3. Check the register allocator isn't getting *worse* (more spills) on inlined bodies. If so, raise the int pool or refine the inline threshold. - -- [ ] **Step 4: Commit** - -```bash -git commit -m "feat(ir_passes): integrate phase 08 pipeline (CSE, LICM, inlining)" -``` - ---- - -## Task 7: Documentation - -**Files:** -- Modify: `docs/internals/the-ir.md` - -- [ ] **Step 1: Document passes** - -Add an "Optimization passes" section listing each pass, its purpose, the patterns it catches, and the expected gain. Cross-reference `src/ir_passes/` files. - -- [ ] **Step 2: Commit** - -```bash -git commit -m "docs: document IR optimization passes" -``` - ---- - -## Exit criteria - -- CSE, LICM, inlining integrated -- All tests green on macOS and Docker Linux gates -- Benchmark suite shows ≥10% additional improvement on loop-heavy / call-heavy benchmarks vs Phase 07 baseline -- Cumulative improvement vs Phase 04 baseline: ≥30% on compute benchmarks -- Documentation updated -- Zero compiler warnings diff --git a/.plans/eir-09-legacy-cleanup.md b/.plans/eir-09-legacy-cleanup.md deleted file mode 100644 index bdae8e4b51..0000000000 --- a/.plans/eir-09-legacy-cleanup.md +++ /dev/null @@ -1,357 +0,0 @@ -# Phase 09 — Legacy Cleanup and Documentation Consolidation - -> **For agentic workers:** Final phase. Remove the legacy AST → ASM backend, consolidate documentation, audit naming, and freeze the IR contract for the v1.0 release. - -**Goal:** Delete `src/codegen/` paths that the IR backend has replaced. Rename `src/codegen_ir/` to `src/codegen/`. Move historical docs aside. Finalize `docs/internals/the-ir.md` as the canonical codegen explanation alongside `the-codegen.md`. - -**Architecture:** Pure cleanup. No new code, no functional change. The IR backend is already the default since Phase 05. - -**Tech Stack:** Rust, git, project documentation tooling. - ---- - -## File Structure - -Heavy delete/rename phase. Approximate map: - -- Delete: `src/codegen/expr.rs`, `src/codegen/expr/` (replaced by `src/codegen_ir/lower_inst/`) -- Delete: `src/codegen/stmt.rs`, `src/codegen/stmt/` (replaced by `src/codegen_ir/lower_inst/` + `src/codegen_ir/lower_term.rs`) -- Delete: `src/codegen/builtins/` *if* every builtin already routes through the IR backend; otherwise keep until parity is full -- Delete: `src/codegen/class_methods.rs`, `src/codegen/functions/` (replaced by IR function emission) -- Keep and merge into new home: `src/codegen/abi/`, `src/codegen/runtime/`, `src/codegen/emit.rs`, `src/codegen/data_section.rs`, `src/codegen/platform.rs`, `src/codegen/ffi.rs` -- Rename: `src/codegen_ir/` → `src/codegen/` (after the legacy delete) -- Move historical doc: `docs/internals/legacy-codegen.md` (created from the current `the-codegen.md` content describing the AST-walker, preserved for historical reference) -- Refresh: `docs/internals/the-codegen.md` to describe the IR-based pipeline as the only pipeline -- Refresh: `docs/internals/the-ir.md` to remove "preview" / "phase" language and present EIR as the canonical IR - ---- - -## Task 1: Verify no consumers of legacy paths - -**Files:** -- Inspect: `src/`, `tests/` - -- [ ] **Step 1: Find every reference to the legacy backend** - -Run: -```bash -grep -rn "generate_user_asm\|--ast-backend\|crate::codegen::expr\|crate::codegen::stmt" src/ tests/ | grep -v codegen_ir -``` - -Expected: only the deprecation shim from Phase 05, the `--ast-backend` CLI flag (which is about to be removed), and possibly some test helpers. - -If any production path still requires the legacy code, document it in `docs/internals/legacy-codegen.md` and defer that path's removal to a follow-up task. - -- [ ] **Step 2: Remove `--ast-backend` from CLI** - -In `src/cli.rs`, remove the `--ast-backend` flag. The argument parser should error with `unknown flag: --ast-backend; the EIR backend is the only supported backend`. - -- [ ] **Step 3: Run full gate** - -```bash -cargo test -cargo test -- --include-ignored -``` - -Expected: green. (Some tests may use `--ast-backend` — update them or remove if redundant.) - -- [ ] **Step 4: Commit** - -```bash -git add src/cli.rs -git commit -m "feat(cli): remove deprecated --ast-backend flag" -``` - ---- - -## Task 2: Delete the AST emitter - -**Files:** -- Delete: `src/codegen/expr/`, `src/codegen/expr.rs` -- Delete: `src/codegen/stmt/`, `src/codegen/stmt.rs` -- Delete: `src/codegen/class_methods.rs` -- Delete: `src/codegen/functions/` -- Delete: `src/codegen/main_emission.rs` -- Delete: `src/codegen/prescan.rs`, `src/codegen/program_usage/`, `src/codegen/program_usage.rs` -- Delete: `src/codegen/function_variants.rs` -- Delete: `src/codegen/interface_wrappers.rs` -- Delete: `src/codegen/driver_support.rs` -- Modify: `src/codegen/mod.rs` to remove deleted modules -- Modify: anything that referenced deleted symbols - -- [ ] **Step 1: Make the deletion** - -```bash -git rm -r src/codegen/expr src/codegen/expr.rs -git rm -r src/codegen/stmt src/codegen/stmt.rs -git rm src/codegen/class_methods.rs -git rm -r src/codegen/functions -git rm src/codegen/main_emission.rs -git rm src/codegen/prescan.rs src/codegen/program_usage.rs -git rm -r src/codegen/program_usage -git rm src/codegen/function_variants.rs src/codegen/interface_wrappers.rs src/codegen/driver_support.rs -``` - -- [ ] **Step 2: Fix `src/codegen/mod.rs`** - -Remove `mod` lines for deleted modules. Remove `pub use` re-exports of deleted symbols. - -- [ ] **Step 3: Build until clean** - -```bash -cargo build -``` - -Resolve every error, one at a time. Each one is a leftover caller of a deleted symbol. The IR backend should replace it (in most cases, the path was already through the IR backend; the leftover is a stale `pub use` or test helper). - -- [ ] **Step 4: Run full gate** - -```bash -cargo test -cargo test -- --include-ignored -./scripts/test-linux-x86_64.sh -./scripts/test-linux-arm64.sh -``` - -- [ ] **Step 5: Commit** - -```bash -git commit -m "refactor: delete legacy AST → ASM emitter" -``` - -This will be one large commit (likely 10k+ lines deleted, including tests that exercised the AST emitter specifically). That is acceptable for a delete-only change. - ---- - -## Task 3: Rename `codegen_ir` to `codegen` - -**Files:** -- Rename: `src/codegen_ir/` → (moved into `src/codegen/`) -- Modify: every `use crate::codegen_ir::*` → `use crate::codegen::*` - -- [ ] **Step 1: Decide the new layout** - -The new `src/codegen/` mixes: -- IR consumer (was `src/codegen_ir/lower_inst/`, `lower_term.rs`, `block_emit.rs`, `frame.rs`, `value_placement.rs`, `context.rs`) -- Preserved shared infrastructure (`abi/`, `runtime/`, `emit.rs`, `data_section.rs`, `platform.rs`, `ffi.rs`) - -New layout: - -``` -src/codegen/ -├── mod.rs # entry: generate_user_asm(module, ...) -├── abi/ # unchanged -├── runtime/ # unchanged -├── emit.rs # unchanged -├── data_section.rs # unchanged -├── platform.rs # unchanged -├── ffi.rs # unchanged -├── frame.rs # was codegen_ir/frame.rs -├── block_emit.rs # was codegen_ir/block_emit.rs -├── context.rs # was codegen_ir/context.rs -├── value_placement.rs # was codegen_ir/value_placement.rs -├── lower_inst/ # was codegen_ir/lower_inst/ -│ ├── arithmetic.rs -│ ├── arrays.rs -│ ├── calls.rs -│ ├── ... -└── lower_term.rs # was codegen_ir/lower_term.rs -``` - -- [ ] **Step 2: Execute the moves** - -```bash -git mv src/codegen_ir/frame.rs src/codegen/frame.rs -git mv src/codegen_ir/block_emit.rs src/codegen/block_emit.rs -git mv src/codegen_ir/context.rs src/codegen/context.rs -git mv src/codegen_ir/value_placement.rs src/codegen/value_placement.rs -git mv src/codegen_ir/lower_inst src/codegen/lower_inst -git mv src/codegen_ir/lower_term.rs src/codegen/lower_term.rs -git rm src/codegen_ir/mod.rs -rmdir src/codegen_ir -``` - -- [ ] **Step 3: Fix imports** - -```bash -grep -rln "crate::codegen_ir" src/ tests/ | xargs sed -i '' 's/crate::codegen_ir/crate::codegen/g' # macOS -# or on Linux: -# grep -rln "crate::codegen_ir" src/ tests/ | xargs sed -i 's/crate::codegen_ir/crate::codegen/g' -``` - -Edit `src/codegen/mod.rs` to expose the renamed submodules. - -Edit `src/lib.rs` to remove `pub mod codegen_ir`. - -- [ ] **Step 4: Build** - -```bash -cargo build -``` - -Fix the remaining stragglers. - -- [ ] **Step 5: Full test gate** - -```bash -cargo test -cargo test -- --include-ignored -./scripts/test-linux-x86_64.sh -./scripts/test-linux-arm64.sh -``` - -- [ ] **Step 6: Commit** - -```bash -git commit -m "refactor: rename codegen_ir to codegen" -``` - ---- - -## Task 4: Documentation consolidation - -**Files:** -- Modify: `docs/internals/the-codegen.md` -- Move: `docs/internals/legacy-codegen.md` (from old `the-codegen.md` contents) -- Modify: `docs/internals/the-ir.md` -- Modify: `docs/internals/architecture.md` -- Modify: `docs/internals/how-elephc-works.md` -- Modify: `docs/README.md` - -- [ ] **Step 1: Preserve the historical doc** - -```bash -cp docs/internals/the-codegen.md docs/internals/legacy-codegen.md -``` - -Open `legacy-codegen.md`. Edit the frontmatter: -- title: "Legacy AST → ASM emitter (historical)" -- description: "Description of the AST-walking emitter that was the default through v0.23. Retained for historical reference." -- sidebar.order: 100 (last) - -Add a top-line note: - -```markdown -> **Status:** Removed in v0.26.0. The current codegen pipeline is documented in -> [the-codegen.md](the-codegen.md) and [the-ir.md](the-ir.md). This page is -> retained for historical context only. -``` - -- [ ] **Step 2: Rewrite `the-codegen.md`** - -The new `the-codegen.md` describes the IR-based pipeline: -1. EIR functions arrive from `crate::ir_lower`. -2. IR-level optimization passes run via the pass pipeline. -3. Linear-scan register allocation produces an `Allocation`. -4. Block-by-block emission lowers each instruction to ASM using the same ABI helpers and runtime calls as before. - -- [ ] **Step 3: Cross-link** - -In `docs/internals/the-ir.md`, add a "See also" footer linking to `the-codegen.md`. In `the-codegen.md`, add the same back-link. - -- [ ] **Step 4: Update `architecture.md`** - -The pipeline diagram now includes "AST → EIR" and "EIR → ASM" boxes instead of one monolithic "AST → ASM" box. - -- [ ] **Step 5: Update `docs/README.md`** - -Add `the-ir.md` to the internals index. Add `legacy-codegen.md` under a "Historical" subsection. - -- [ ] **Step 6: Commit** - -```bash -git add docs/ -git commit -m "docs: consolidate codegen documentation around the IR pipeline" -``` - ---- - -## Task 5: Final verification - -**Files:** the whole repo - -- [ ] **Step 1: Run every gate** - -```bash -cargo build -cargo build --release -cargo test -cargo test -- --include-ignored -./scripts/test-linux-x86_64.sh -./scripts/test-linux-arm64.sh -./scripts/run-benchmarks.sh -``` - -- [ ] **Step 2: Audit zero-warning policy** - -`cargo build` must produce no warnings. Fix or `#[allow]` any that surface. - -- [ ] **Step 3: Audit assembly-comment policy** - -`CLAUDE.md` requires every `emitter.instruction(...)` to have a `//` comment at column 81. Run the audit script from `CLAUDE.md`: - -```bash -python3 -c " -import os -for root, _, files in os.walk('src'): - for fn in files: - if not fn.endswith('.rs'): continue - path = os.path.join(root, fn) - with open(path) as f: - for i, line in enumerate(f, 1): - if 'emitter.instruction' in line and '//' in line: - pos = line.rstrip().index('//') - if pos != 80 and len(line[:pos].rstrip()) < 80: - print(f'{path}:{i}: // at col {pos+1}') -" -``` - -Fix any drifted lines. - -- [ ] **Step 4: Audit file-size policy** - -```bash -find src -name '*.rs' -exec wc -l {} \; | sort -nr | head -20 -``` - -Any file >500 LOC that mixes responsibilities should be split. Cohesive single-feature leaves above the threshold are allowed (per `CLAUDE.md` policy). - -- [ ] **Step 5: Audit Rust module preamble policy** - -Every `*.rs` file in `src/` must start with a `//!` preamble. Run: - -```bash -for f in $(find src -name '*.rs'); do - head -1 "$f" | grep -q '^//!' || echo "missing preamble: $f" -done -``` - -Fix any misses. - -- [ ] **Step 6: Bench result archive** - -Take a clean benchmark run on macos-aarch64 and linux-x86_64. Save the JSON outputs as `benchmarks/results/v0.26.0-final.json`. Commit. - -- [ ] **Step 7: Update ROADMAP** - -Mark all v0.24, v0.25, v0.26 items complete. The next milestone is the v1.0 freeze. - -- [ ] **Step 8: Final release commit** - -```bash -git commit -m "chore: finalize v0.26.0 (IR backend default, legacy removed)" -``` - -Tag and release per project convention. - ---- - -## Exit criteria - -- Legacy AST emitter completely removed -- `src/codegen_ir/` renamed to `src/codegen/` -- Documentation reflects the IR-based pipeline as the only pipeline -- All audit checks pass: zero warnings, assembly-comment alignment, module preambles, file-size policy -- Benchmark results archived -- ROADMAP up to date -- The project is ready for the v1.0 freeze pass diff --git a/.plans/hot-path-data-type.md b/.plans/hot-path-data-type.md deleted file mode 100644 index 383ef3daef..0000000000 --- a/.plans/hot-path-data-type.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -name: hot-path data type -overview: Definire una nuova primitive/compiler feature esplicita per dati hot-path, privilegiando prestazioni e prevedibilita' rispetto alla compatibilita' PHP totale. Il piano riusa puntatori tipizzati, layout statici ed emissione diretta di load/store per evitare hash lookup, boxing e overhead del runtime array PHP. -todos: - - id: design-syntax - content: Definire la sintassi minima del nuovo tipo hot-path (`buffer` e/o `packed class`) e le semantiche POD consentite - status: pending - - id: type-layout - content: Progettare i nuovi metadata statici in `src/types/` per stride, offset e validazione dei tipi packed - status: pending - - id: codegen-runtime - content: Pianificare emissione diretta load/store e un runtime buffer separato dal path array/hash PHP - status: pending - - id: tests-benchmarks - content: Definire test e microbenchmark per dimostrare il vantaggio su array e assoc array PHP - status: pending -isProject: true ---- - -# Piano per un tipo hot-path esplicito - -## Obiettivo - -Introdurre in elephc una nuova astrazione dati per inner loop di giochi e renderer, separata dagli array PHP gestiti. Il target e' ottenere accesso contiguo, layout prevedibile e zero hash lookup, mantenendo la logica di alto livello in PHP. - -## Scelta di design consigliata - -La strada meno rischiosa e piu' performante e' **aggiungere un tipo compiler-specifico packed/SoA**, non cercare di far diventare gli `array` PHP il contenitore giusto per i path caldi. - -Perche': - -- Gli assoc array passano da `__rt_hash_get` e quindi ogni accesso e' una lookup runtime, non un load diretto: `src/codegen/expr/arrays.rs`, `src/codegen/runtime/arrays/hash_get.rs`. -- Gli array indicizzati vivono comunque nel runtime heap/COW: `src/codegen/runtime/arrays/array_new.rs`. -- Esistono gia' i ganci giusti per un percorso piu' basso livello: `PhpType::Pointer`, `ExternClassInfo`, field offsets e property access su `ptr`: `src/types/model.rs`, `src/codegen/expr/objects/access.rs`. - -## Forma iniziale proposta - -Partire con una feature esplicita e minimale, ad esempio una di queste due forme: - -- `packed class` / `extern-like class` allocabile da elephc, con layout POD fisso e accesso a campi a offset statici. -- `buffer` o `packed_array` come contenitore contiguo di POD (`int`, `float`, `bool`, `ptr`, oppure `packed class`). - -Raccomandazione: iniziare da **`buffer` + `packed class`**. - -Perche': - -- `packed class` da' un record denso e compilabile in offset statici. -- `buffer` permette sia AoS (`buffer`) sia SoA (`buffer $enemyX`, `buffer $enemyState`). -- Entrambi possono appoggiarsi a `malloc`/heap interno senza entrare nella semantica array/hash/COW. - -## Vincoli semantici da fissare subito - -Per avere prestazioni massime, il nuovo tipo deve essere volutamente piu' ristretto dei normali tipi PHP: - -- Nessun `mixed` dentro i buffer hot-path. -- Solo tipi POD inizialmente: `int`, `float`, `bool`, `ptr`, e `packed class` composte solo da POD. -- Niente stringhe/array/object refcounted nella v1. -- Indice numerico soltanto, bounds check opzionale o esplicito. -- Nessuna semantica COW/refcount per elemento. -- Layout e stride completamente statici al compile time. - -## Architettura proposta - -```mermaid -flowchart LR - PhpSource[PHPSourceWithPackedTypes] --> Parser - Parser --> TypedAst - TypedAst --> TypeChecker - TypeChecker --> PackedLayoutInfo - PackedLayoutInfo --> Codegen - Codegen --> DirectLoadsStores - DirectLoadsStores --> NativeBinary - - PackedLayoutInfo --> BufferRuntime - BufferRuntime --> NativeBinary -``` - -## Moduli da toccare - -### Parsing e AST - -Estendere parser e AST per rappresentare il nuovo tipo e le operazioni base: - -- Nuovi token/keyword o nuova sintassi tipo-generica in `src/lexer/token.rs` e `src/parser/expr/`. -- Nuovi nodi in `src/parser/ast/stmt.rs`, `src/parser/ast/expr.rs`, e `src/parser/ast/types.rs`: - - dichiarazione `packed class` oppure equivalente - - tipo `Buffer` / `PackedArray` - - accesso indicizzato hot-path - - eventuali builtins tipo `buffer_new($len)` - -### Type checker - -- Estendere `src/types/model.rs` con tipi nuovi, ad esempio: - - `PhpType::Packed(String)` - - `PhpType::Buffer(Box)` -- Creare metadata statici analoghi a `ExternClassInfo`, ma per tipi packed nativi. -- Validare che gli elementi siano POD e calcolare `stride` e offset. -- Integrare ownership: i buffer devono stare fuori da `Mixed/Array/AssocArray/Object` e dal normale refcount per elemento. - -### Codegen - -- Nuovo path di emissione in `src/codegen/expr.rs` e sotto-moduli dedicati, evitando `__rt_hash_get` e le semantiche COW. -- Riutilizzare il modello gia' presente per accesso a campi a offset statici in `src/codegen/expr/objects/access.rs`. -- Generare indirizzo base + `index * stride` + `field_offset`, poi `ldr/str` diretti. - -### Runtime - -- Aggiungere un runtime minimo per allocazione buffer, idealmente separato dagli array PHP: - - `__rt_buffer_new` - - `__rt_buffer_free` o integrazione controllata col heap attuale - - opzionale `__rt_buffer_bounds_fail` -- Evitare il coinvolgimento del path hash/array runtime, che oggi introduce il costo che vogliamo bypassare: `src/codegen/runtime/arrays/hash_get.rs`, `src/codegen/runtime/arrays/array_new.rs`. - -## Strategia di rollout - -### Fase 1: MVP strettissimo - -- `buffer` -- `buffer` -- `buffer` -- `buffer` -- allocazione, lettura, scrittura, `count/len` -- nessuna crescita dinamica -- nessuna interoperabilita' automatica con array PHP - -Questo basta gia' per: - -- z-buffer -- colonne raycaster -- stati compatti -- arrays paralleli SoA per enemy/player/projectiles - -### Fase 2: `packed class` - -- `packed class EnemyPod { public float $x; public float $y; public int $state; public int $hp; }` -- supporto `buffer` -- accesso `$enemies[$i]->x` compilato come load diretto a offset statico - -### Fase 3: ergonomia extra - -- helper per slice/view -- eventuale `unsafe` mode per togliere bounds checks -- memset/memcpy builtins mirati -- conversioni esplicite da/verso array PHP per tooling/debug - -## Criteri di successo per il design - -Il nuovo tipo e' corretto se: - -- l'accesso a elemento/campo non passa da hash lookup o tag dispatch -- il layout e' statico e ispezionabile -- non eredita COW/refcount per elemento dagli array PHP -- si integra bene con SDL/FFI e i pointer helpers gia' esistenti -- consente esempi di gameplay/render loop scritti in PHP con dati hot-path fuori dagli assoc array - -## Test da prevedere - -Seguire il pattern del progetto con test lexer/parser/typechecker/codegen: - -- parser del nuovo tipo e della nuova sintassi -- type errors su tipi non POD -- codegen tests per lettura/scrittura in loop -- test FFI/interoperabilita' con `ptr_cast` e layout packed -- benchmark micro per confrontare: - - assoc array - - array indicizzato PHP - - nuovo `buffer` - -## Rischi principali - -- Se si cerca di rendere `array` PHP stesso “veloce”, si entra in conflitto con hash/COW/refcount e si allarga troppo il blast radius. -- Se `packed class` accetta tipi heap-managed troppo presto, si perde il vantaggio del modello hot-path. -- Se la sintassi e' troppo ambiziosa nella v1, il costo parser/typechecker cresce senza dare subito valore al renderer. - -## Raccomandazione finale - -Puntare a un MVP con **`buffer` per POD + poi `packed class`**, costruito sopra i meccanismi gia' presenti di layout statico e accesso a offset. E' la strada che massimizza le prestazioni e minimizza il rischio architetturale, lasciando gli array PHP al ruolo di strutture di alto livello e i nuovi buffer ai path caldi. diff --git a/.plans/null-sentinel-collision.md b/.plans/null-sentinel-collision.md deleted file mode 100644 index 8072ee849d..0000000000 --- a/.plans/null-sentinel-collision.md +++ /dev/null @@ -1,118 +0,0 @@ -# Null-sentinel collision — Implementation Plan - -**Goal:** Make every valid PHP integer representable as a non-null scalar — eliminating the collision where the integer `9223372036854775806` (= `PHP_INT_MAX - 1` = the in-band null sentinel `0x7fff_ffff_ffff_fffe`) is misread as `null` — without heap-boxing every plain `int`. - -**Architecture:** elephc stores PHP `null` in an unboxed scalar slot using the magic i64 `0x7fff_ffff_ffff_fffe`. Because every i64 bit pattern is a valid PHP int, this in-band sentinel collides with the real integer. The fix introduces an inline 2-word `{tag, payload}` tagged scalar for scalar slots that can be null, reusing the existing runtime tag scheme, with no heap allocation. Designed to converge with the int-overflow→float work (same `{tag, payload}` shape, tag ∈ {int, float, null}). - -## Phases - -- **Phase 0** — Lock repros (`tests/codegen/null_sentinel/repros.rs`), decide representation (see DESIGN-NOTES), add microbench harness. -- **Phase 1** — Unify the 7 file-local `NULL_SENTINEL` consts + raw literals into one canonical constant; document the incompatibility in `docs/php/types.md`. Zero behavior change. -- **Phase 2** — Tagged representation behind `NullRepr::{Sentinel, Tagged}` flag (default `Sentinel`); audit every producer/consumer; repros pass under `Tagged` on all 3 targets. -- **Phase 3** — Narrowing: non-nullable `int` slots stay plain i64 (asm-inspection proof); only genuinely-nullable slots widen. -- **Phase 4** — Flip default to `Tagged`, update docs, converge with the overflow plan. - **DONE** except overflow convergence (tracked in the overflow plan): default flipped, - `--null-repr=sentinel` kept as documented opt-out, §0 repros un-ignored, docs updated. - Evidence: full default-mode suite green pre-flip (3979), full Tagged-forced suite green - modulo two since-fixed failures plus green focused reruns, Linux x86_64/ARM64 28/28. - -## Sentinel inventory - -**Producers (write the null sentinel):** `expr/scalars.rs` (`emit_null_literal`), `expr/objects/nullsafe.rs` (`?->`), `expr/objects/dispatch/enums.rs` (enum `tryFrom`), `expr/objects.rs` (`emit_boxed_null`), `expr/arrays/access/indexed.rs` (array miss), `abi/symbols.rs` (globals), `stmt/assignments/properties/storage.rs` (Void property), `abi/values.rs` (Void/Never local store), `runtime/arrays/array_shift.rs`, boxed-null low-word reuses in `magic_set.rs`, `dynamic_props.rs`. - -**Consumers (detect null via sentinel):** `builtins/types/is_null.rs`, `stmt/io.rs` (echo), `builtins/io/var_dump.rs`, `functions/generator/emit/stmts.rs`, `expr/compare/null_coalesce.rs` (`??`), `stmt/null_coalesce_assign.rs` + dup in `stmt/assignments/locals.rs` (`??=`), `builtins/arrays/isset.rs`, `expr/coerce.rs` (`coerce_null_to_zero`), `builtins/io/stream_get_contents.rs`. - ---- - -## DESIGN-NOTES (Phase 0) - -### Empirical findings (macOS ARM64, elephc 0.23.8, PHP 8.4.20 oracle) - -``` -echo 9223372036854775806; elephc: (empty) PHP: 9223372036854775806 -$a=[...806]; echo $a[0]; var_dump($a[0]); elephc: (empty)/NULL PHP: ...806 / int(...806) -function f():?int{return ...806;} f() elephc: NULL PHP: int(...806) -$x=...806; var_dump(is_null($x)); elephc: bool(true) PHP: bool(false) -$x=...806; echo $x ?? 0; elephc: 0 PHP: ...806 -$a=[...806]; isset($a[0]) elephc: true (correct) -``` - -Key discovery from asm inspection (`--emit-asm`): a `?int` return is **already boxed as a Mixed cell with the correct int tag** (`__rt_mixed_from_value(tag=0, payload=...806)`); the NULL output comes from the **shared int formatter** (`emit_var_dump_int`) re-checking the payload against the sentinel after unboxing. The collision therefore lives in (a) consumers that sentinel-check every `Int`-typed payload because the type `Int` is overloaded ("definitely int" vs "int-or-null"), and (b) producers that write the sentinel into `Int`-typed slots (array miss, etc.). The boxed Mixed path itself is sound. - -Unrelated pre-existing quirk found while locking repros: `var_dump(isset(...))` prints `int(1)` instead of `bool(true)` (isset result is typed int). Repro tests avoid it via ternary echo. - -### Decision: option (b) — inline 2-word tagged scalar, new internal `PhpType::TaggedScalar` variant - -1. **Carrier.** New variant `PhpType::TaggedScalar` (internal-only, like `Mixed`; the checker never produces it). It is **constructed only in codegen funnels and only when `NullRepr::Tagged` is active**. Under `NullRepr::Sentinel` (default) it is never constructed, so generated code is byte-identical and no flag plumbing is needed in `src/types/model.rs` — `stack_size`/`register_count`/`is_refcounted` for the variant are unconditional. - -2. **Layout.** - - Stack slot: 16 bytes — payload word at `offset`, tag word at `offset - 8` (mirrors `Str`: ptr at `offset`, len at `offset - 8`). - - `stack_size() = 16`, `register_count() = 2`, `is_refcounted() = false`, `is_float_reg() = false`. - - Expression result: payload in `int_result_reg` (ARM64 `x0`, x86_64 `rax`), tag in the second-word register (ARM64 `x1`, x86_64 `rdx` — same convention as `Str`'s second word). Payload-in-result-reg makes "narrow to plain int" free once non-null is proven. - - Arguments: 2 consecutive integer argument registers (payload, tag), reusing the existing 2-register argument machinery built for `Str`; stack spills follow the existing 2-word logic. - -3. **Tag domain.** Reuse `runtime_value_tag` values: `0 = int`, `2 = float` (reserved for the int-overflow plan), `3 = bool` (reserved), `8 = null`. `is_null` ⇔ `tag == 8`. The `{tag, payload}` pair is word-compatible with a Mixed cell's words 0/8, so TaggedScalar→Mixed boxing is a direct `__rt_mixed_from_value(tag, payload, 0)` and Mixed→TaggedScalar unboxing is two loads. This is the convergence point with the overflow plan. - -4. **Construction funnels (under `Tagged`).** - - Declared-type lowering (`codegen_static_type`/`codegen_declared_type` + the `Union → Mixed` collapse): `Union([Int, Void])` → `TaggedScalar` for params, returns, locals, properties. - - `array` element reads (miss-capable): hit → `{tag=int, payload}`, miss → `{tag=null}`. - - `null` literal flowing into an int context; `?->` over int members; `array_shift` empty result; `stream_get_contents` failure path. - - **Scope:** only `int|null` widens initially. `?float`/`?bool` keep today's Mixed boxing (tag values reserved; flipping them is follow-up work). `?string` stays Mixed permanently (a string is already 2 words; `{tag, ptr, len}` would need 3). Nullable objects keep their existing pointer-sentinel scheme (separate surface, out of scope). - -5. **Consumers (under `Tagged`).** `is_null`, echo suppression, `var_dump`, `??`, `??=`, `isset`, `coerce_null_to_zero` dispatch on the tag word when the operand is `TaggedScalar`, and emit **no sentinel check at all** for plain `Int` operands (plain `Int` becomes "definitely int" again — this alone fixes `echo 9223372036854775806`). - -6. **Flag.** `NullRepr::{Sentinel, Tagged}` lives in `CliConfig` → pipeline → codegen `Context`. Default `Sentinel` until Phase 4. CLI/env opt-in: `ELEPHC_NULL_REPR=tagged`. Tests force `Tagged` per-compile through an explicit parameter on the test-support compile helpers (no process-global mutation — tests run in parallel threads). - -7. **Scope refinement (Phase 2, validated empirically).** elephc's checker is flow-sensitive - for locals, and `?int` params/returns/properties already lower to boxed Mixed (null-safe). - The §0 bugs therefore need TaggedScalar **producers** only for: int-element array reads - (indexed + assoc, miss-capable), and later `array_pop`/`array_shift`/`?->`-int/globals. - Params, returns, and properties keep their Mixed boxing — for them only the **consumers** - needed fixing (the shared int formatter / sentinel checks). Null-then-int locals are - handled by flow typing today and stay as they are. - -8. **Null payload canonicalization.** A tagged null carries `NULL_SENTINEL` as its payload - word (not zero), so boxing it into a Mixed cell produces exactly the legacy - `{tag 8, sentinel}` words — `__rt_mixed_strict_eq` compares payload words even for the - null tag, and un-audited consumers that ignore the tag see precisely the legacy encoding - (graceful degradation instead of new corruption). - -9. **Rejected alternatives.** - - *(a) rarer sentinel value:* still collides — every i64 is a valid int. - - *(c) box null-capable scalars as Mixed:* already today's repr for `?int` returns yet still broken (consumer-side sentinel re-check), and boxing every `array` read would heap-allocate per access. - - *Re-repr `Union([Int,Void])` without a new variant:* every existing `Union(_) => /* boxed pointer */` match arm becomes a silent hazard under the flag; a new variant turns missed paths into compile errors (exhaustive matches) or loud crashes instead of silent miscompilation. - - *Uninit-property sentinel (`0x...fffd`):* not a value collision (separate metadata word); in scope only for the Phase 1 constant unification. - -### Phase 2 status & follow-ups - -Done under `NullRepr::Tagged` (default `Sentinel` unchanged, suite green): TaggedScalar -variant + ABI (slots, args, returns, spills mirror `Str`), `NullRepr` flag -(`--null-repr=` / `ELEPHC_NULL_REPR`), tag-aware consumers (echo, var_dump + generator dup, -is_null, `??`, `??=` both copies, isset, empty, gettype, casts, string coercion, truthiness, -numeric binops, strict `===` via the Mixed boxing path), and the int-array read producers -(indexed + assoc, hit→tag int / miss→tag null). All §0 repros pass under `Tagged` and are -covered by `tests/codegen/null_sentinel/tagged.rs` (macOS ARM64 + Linux x86_64 + Linux ARM64). - -Discovered during Phase 2 — pre-existing null-read bugs (broken in BOTH modes; Tagged fixes -most): under the legacy sentinel repr, `$a[miss] + 1`, `$a[miss] . "s"`, `"{$a[miss]}"`, -`$a[miss] * 3` all leak the sentinel digits; Tagged narrows them correctly. - -Phase 2.3 (full suite forced to Tagged) initially failed 9/3978; all four systematic causes -fixed: local inference now mirrors emission (TaggedScalar for miss-capable int reads, so -array literals box and === peeks stay runtime), untyped params widen to int|null on null -call sites (flag installed before checking), array element stores/pushes narrow the payload -word, and local assignments reinterpret into one-word slots instead of clobbering the -neighboring slot with the tag word. `array_pop`/`array_shift` empty results, unary minus, -and `abs()` are tagged/narrowed. Benches (noisy, same-load comparison): plain-int parity, -array-read loop ~45% faster under Tagged (sentinel materialization disappears), nullable -roundtrip +9%. - -Remaining follow-ups (not blockers; behavior matches the legacy mode in both representations): -- storing a null-valued tagged read into an int array keeps the in-band payload (reads - back as the sentinel integer, like the legacy mode); a runtime tag-branch into the boxed - Mixed conversion would make it PHP-exact. -- int-builtin argument narrowing beyond abs() (e.g. intdiv on a null read) follows legacy - semantics; a shared builtin-arg coercion gate would make them PHP-exact. -- undefined globals read as int 0 in both modes (PHP: null + warning) — pre-existing. -- `?->` over int members already prints correctly via the Mixed path; enum tryFrom keeps - its object-pointer sentinel scheme (no int collision). diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index ef495c00be..0000000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -./CLAUDE.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..6245315fa5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,551 @@ +# elephc — Developer Guide + +## What is this + +A PHP-to-native compiler written in Rust. Compiles a static subset of PHP to native assembly for the supported target matrix, producing standalone binaries. No interpreter, no VM, no runtime dependencies. + +## Read CONTRIBUTING.md first + +Before contributing, read `CONTRIBUTING.md` in full. It holds the complete step-by-step recipes — adding an operator, a statement type, a built-in function, an EIR optimization pass, and a crate-backed `--with-` feature — plus the assembly comment policy, the coding style, and the Pull Request workflow. This guide covers the architecture, invariants, and context that go alongside those recipes; it deliberately does not repeat them. + +## Supported target policy + +All supported targets are first-class targets. The supported target matrix is currently `macos-aarch64`, `linux-aarch64`, and `linux-x86_64`. + +Do not design or land codegen/runtime features as ARM64-first with x86_64 treated as a later port. New features, builtins, runtime helpers, optimizer assumptions that affect emitted code, ABI behavior, and ownership/GC paths must either support every supported target in the same change or clearly isolate an intentionally unsupported path with diagnostics, tests, and documentation. A feature is not considered done while any supported target has a missing runtime symbol, reduced semantics, stale documentation, or an untested target-specific lowering path. + +When examples or internals docs use ARM64 snippets for readability, treat them as examples only. Implementation work must keep the target-aware ABI/runtime boundaries authoritative. + +## Build & run + +```bash +cargo build # dev build +cargo build --release # optimized build +cargo run -- file.php # compile a PHP file +``` + +The compiler outputs a native binary next to the source file (e.g., `file.php` → `file`). + +## Test policy + +**Every feature must have tests before it's considered done.** The test suite is the primary quality gate. + +Local agent runs should stay focused. By default, run only the build checks and tests needed to validate the implementation or regression you touched, then rely on CI for the complete matrix. Do **not** run the full local suite (`cargo test`, `cargo test -- --include-ignored`, or full Linux Docker scripts) unless the user explicitly asks for it, you are doing a release/pre-release verification, CI is unavailable for the needed signal, or a broad/high-risk change cannot be validated responsibly with focused tests. + +### Running tests + +```bash +cargo test test_fizzbuzz # run a focused test by name +cargo test --test codegen_tests test_name # run a focused end-to-end codegen test +cargo nextest run --profile ci test_name # run a focused nextest test with per-test timeout +cargo test # full local suite; only when explicitly requested/necessary +cargo test -- --include-ignored # ignored tests; only when relevant or requested +``` + +Linux target-specific regressions can be checked through the Docker scripts in +`scripts/`. Prefer filters during implementation; the unfiltered commands run +full Linux suites and should be reserved for explicit requests or genuinely +necessary local target validation. + +```bash +./scripts/test-linux-x86_64.sh iterable # run tests matching a filter +./scripts/test-linux-arm64.sh test_my_feature # run tests matching a filter +./scripts/test-linux-x86_64.sh # full Linux x86_64 suite; only when necessary/requested +./scripts/test-linux-arm64.sh # full Linux ARM64 suite; only when necessary/requested +./scripts/test-linux-x86_64.sh --rebuild # rebuild the Docker image first +./scripts/test-linux-arm64.sh --rebuild # rebuild the Docker image first +``` + +Some tests are marked `#[ignore]` because they require external libraries (e.g., SDL2) not available in CI. Run ignored tests only when the change touches that surface, during explicit release verification, or when the user asks for them. + +### Test strategy during development + +The full test suite is slow because each codegen test spawns `as` + `ld` + runs the binary. CI runs the complete supported matrix (`macos-aarch64`, `linux-x86_64`, and `linux-aarch64`) with sharded codegen tests, so local implementation work should optimize for fast, relevant signal: + +1. **While developing a feature**: run only the tests for that feature or regression (`cargo test test_my_feature`). +2. **Scope to a single test binary**: prefer `cargo test --test codegen_tests ` over a bare `cargo test `. A bare filter rebuilds and links all six test binaries every cycle (~2.5s of wasted link time); `--test ` links only the one you need. Most codegen work lives in `codegen_tests`. +3. **For target-sensitive changes**: run the smallest relevant macOS/Linux focused tests locally when they materially reduce risk; otherwise let CI provide complete target coverage. +4. **For broad infrastructure changes**: validate the edited workflow/config directly (YAML parse, `cargo metadata`, `nextest` config checks, etc.) instead of running unrelated Rust suites. +5. **PHP cross-check**: opt in narrowly with `ELEPHC_PHP_CHECK=1 cargo test ` when PHP equivalence is the question. + +### Pre-commit verification + +Before committing code changes, run the smallest useful focused tests and hygiene checks that cover the implementation. Do not run full local suites by default; CI is responsible for the complete sharded matrix after push/PR. + +```bash +cargo build # for code changes that should compile warning-free +cargo test # or the narrower --test form +git diff --check +``` + +For docs-only, workflow-only, or configuration-only changes, replace Rust test runs with the relevant syntax/metadata checks. For codegen changes, also verify assembly-comment coverage/alignment for any files you touched. If the change can affect generated assembly, runtime helpers, ABI behavior, linking, ownership/GC, or target-specific libraries, run focused tests for affected supported targets when local evidence is needed; otherwise rely on CI for full Linux x86_64/Linux ARM64/macOS ARM64 coverage. + +### Test structure + +| File | What it tests | How | +|---|---|---| +| `tests/lexer_tests.rs` | Tokenization | Asserts token sequences from source strings | +| `tests/parser_tests.rs` | AST construction | Asserts AST node structure and operator precedence | +| `tests/codegen_tests.rs`, `tests/codegen/` | Full pipeline (end-to-end) | Compiles PHP → binary, runs it, asserts stdout | +| `tests/error_tests.rs`, `tests/error_tests/` | Error reporting | Asserts that invalid programs produce the right error messages | + +### Test coverage requirements + +- **New language construct** (keyword, operator, statement): needs lexer, parser, codegen, AND error tests +- **New operator**: needs a Pratt parser binding power test verifying precedence relative to adjacent operators +- **New statement type**: needs at least one codegen test showing correct output, one test for edge cases (empty body, nested), and one error test for malformed syntax +- **New built-in function**: needs codegen tests for normal use and error test for wrong argument count/types +- **Bug fix**: must include a regression test that would have caught the bug +- **Every feature also needs an example** in `examples/`. If an existing example can showcase the new feature naturally, update it. Otherwise, create a new `examples//main.php` with its own `.gitignore` (containing `*.s`, `*.o`, `main`). Examples should be small, readable programs that demonstrate real use cases — not just test cases. + +### Writing codegen tests + +Codegen tests compile inline PHP source and assert stdout: + +```rust +#[test] +fn test_my_feature() { + let out = compile_and_run("` | +| `src/parser/` | `parse()` | Tokens → `Program` (Vec of Stmt). Pratt parser for expressions | +| `src/magic_constants/`, `src/magic_constants.rs` | `substitute_file_and_scope_constants()` | Lowers PHP magic constants before resolver/name-resolver/optimizer passes | +| `src/conditional.rs` | `apply()` | Applies compiler `ifdef` conditional branches | +| `src/resolver/` | `resolve()` | Resolves `include`/`require`, discovers declarations, and tracks include-loaded function variants. Runs before namespace/name canonicalization | +| `src/name_resolver/` | `resolve()` | Applies namespace/use rules, rewrites references to canonical fully-qualified names, handles PHP-style builtin fallback, and flattens namespace-only AST nodes before type checking | +| `src/types/` | `check()` | Type checking, returns `CheckResult` with `TypeEnv`, function/class/interface/enum/FFI metadata, warnings, required libraries, and the internal `Mixed` type for heterogeneous assoc-array values | +| `src/optimize/`, `src/optimize.rs` | `fold_constants()`, `propagate_constants()`, `eliminate_dead_code()` | AST-level constant folding/propagation, control-flow pruning/normalization, DCE, and effect modeling | +| `src/ir/` | IR types / builders / validator | EIR program, function, block, instruction, terminator, value, local, effect, ownership, and textual-format definitions | +| `src/ir_lower/` | `lower_program()` | Active AST → EIR lowering, including local slot creation, hidden temporaries, ownership annotations, and PHP call semantics | +| `src/ir_passes/` | `optimize_module()` / `allocate_registers()` | EIR analyses and transformations over lowered functions: fixed-point optimization pass driver (identity folding, …; gated by `--ir-opt`) and linear-scan register allocation, before codegen | +| `src/codegen/` | `generate()` | Active EIR → target assembly backend | +| `src/codegen_support/abi/` | ABI helpers | Target-specific argument materialization, frame layout, registers, stack slots, symbols, and call helpers | +| `src/codegen_support/runtime/` | Runtime emitters | Shared `__rt_*` helpers and runtime data used by generated programs | +| `src/codegen/program_usage/` | Program scans | Collects codegen metadata such as required classes and variables before emission | +| `src/runtime_cache.rs` | `prepare_runtime_object()` | Builds/reuses the target runtime object before final linking | +| `src/errors/` | `report()` | Error formatting with line:col | +| `src/span.rs` | `Span` | Source position (line, col) attached to all AST nodes | + +### Bridge crates and `--with-` flags + +Optional, heavyweight functionality that is naturally expressed with Rust +libraries lives in a workspace crate under `crates/elephc-/`, compiled as a +`staticlib` and linked into the user program on demand. Every such bridge is +described by one entry in the `BRIDGES` table in `src/linker.rs`; `link()` and the +discovery/auto-build helpers are fully table-driven, so adding a bridge is a +single table entry rather than new linker logic. + +A bridge is linked when its `lib_name` appears in `extra_link_libs`. That set is +populated automatically by feature detection: the type checker records a needed +library via `require_builtin_library(...)` (e.g. hash builtins → `elephc_crypto`) +or through an injected `extern "elephc_" { ... }` prelude block (e.g. the +PDO/tz/image preludes), so programs that do not use a feature never link its +bridge. + +`--with-` force-enables a bridge regardless of detection. It force-links +the staticlib (whole-archived so it is not dead-stripped) and, for crates whose +PHP surface comes from a prelude (`pdo`, `tz`, `image`), force-injects that +prelude so the API is declared even when usage was not detected. The flag name is +the bridge's `flag_name` (`crate_name` minus the `elephc-` prefix): `--with-pdo`, +`--with-tls`, `--with-crypto`, `--with-phar`, `--with-tz`, `--with-image`. +`--with-web` is an alias for `--web` (the full server mode, which owns the program +entry point). An unknown `--with-` is a hard CLI error listing the valid +crates. The end-to-end wiring is CLI (`src/cli.rs`, `with_crates`) → pipeline +(`src/pipeline.rs`: force-link + prelude forcing) → linker +(`src/linker.rs`: `forced_whole_archive`). + +### Codegen layout + +- `src/ir_lower/` is the active high-level lowering layer. Add PHP-visible semantics there. +- `src/codegen/lower_inst/` and `src/codegen/lower_term.rs` are the active EIR instruction/terminator assembly lowerers. +- `src/codegen/context.rs`, `src/codegen/frame.rs`, and `src/codegen/value_placement.rs` carry active backend state, frame layout, and value placement. +- `src/codegen_support/runtime/mod.rs` emits shared runtime code (`__rt_*` routines) +- `src/codegen_support/runtime/data/` emits shared runtime `.data` / `.bss` symbols and metadata tables +- `src/codegen_support/abi/` centralizes target-specific register, stack, frame, symbol, and call mechanics. Prefer these helpers over hardcoding ARM64 or x86_64 details in feature emitters. + +### Adding a new operator + +Key invariants: + +- A new operator touches the whole pipeline: token (`src/lexer/`), Pratt binding power (`infix_bp()` in `src/parser/expr/pratt.rs`), `BinOp` variant (`src/parser/ast.rs`), type inference (`src/types/checker/`, usually `inference/ops.rs`), optimizer folding/effects (`src/optimize/`), and EIR lowering (`src/ir_lower/expr/` + `src/codegen/lower_inst/`). +- Precedence and associativity must match PHP; keep folds PHP-equivalent and cross-check edge cases with `php -r`. +- Needs a Pratt binding-power test asserting precedence relative to adjacent operators, plus tests in all 4 test files (lexer, parser, codegen, error). + +### Adding a new statement type + +Key invariants: + +- A new statement touches parser (`StmtKind` in `src/parser/ast.rs` + `src/parser/stmt.rs`), resolver/name-resolver (if it holds names, declarations, includes, function variants, or expressions), type checker (`src/types/checker/`), optimizer/effects/warnings (`src/optimize/`), and EIR lowering (`src/ir_lower/stmt/` + `src/codegen/`). +- If it introduces variables or hidden temporaries, update EIR local/temp declaration in `src/ir_lower/context.rs` and frame-layout allocation before frame sizing. +- Also audit every AST-walking pass (see "Adding or changing an AST node") — a missed pass usually causes silent miscompilation rather than a compile error. + +### Adding or changing an AST node + +When adding a new `ExprKind` or `StmtKind`, check every AST-walking pass. The compiler has many passes that deliberately recurse by variant, and missing one usually creates silent miscompilation rather than a compile error. + +Common places to audit: + +- Parser construction and lowering in `src/parser/` +- Resolver/include discovery and function-variant handling in `src/resolver/` +- Namespace/use/FQN rewriting in `src/name_resolver/` +- Type checking, inference, return analysis, warnings, and type compatibility in `src/types/` +- Optimizer folding, propagation, DCE, control-flow normalization, and effect modeling in `src/optimize/` +- Program usage scans in `src/codegen/program_usage/` +- Local/hidden-slot declaration in `src/ir_lower/context.rs` and frame placement in `src/codegen/` +- Ownership metadata in `src/ir_lower/ownership.rs`, EIR ownership lowering in `src/codegen/lower_inst/ownership.rs`, and related runtime/GC paths +- EIR lowering in `src/ir_lower/` plus EIR backend lowering in `src/codegen/` +- Lexer/parser/codegen/error/regression tests, depending on the surface area + +### Adding a new built-in function + +PHP builtins are declared **once** in the single-source registry: one home file per +builtin at `src/builtins//.rs`, declared with the `builtin!` macro and +collected via `inventory`. From that single declaration the compiler derives the +catalog name-set (`function_exists`, case-insensitive lookup, namespace fallback), the +`FunctionSig` (named args, defaults, ref params, variadic, arity), the type-check +entry, the EIR lowering dispatch (`spec.lower`), and the generated docs. Do **not** +re-add builtin names to hand-maintained tables (`catalog.rs`, `signatures.rs`, per-area +`check_builtin` arms) — they are superseded by the registry. + +Key invariants: + +- **One builtin per home file.** The `lower` hook is a thin wrapper over the real + emitter in `src/codegen/lower_inst/builtins//`; leaf emitter files hold + exactly one emitter function, and runtime data emission stays in + `src/codegen/runtime/data.rs`. +- **`returns:`/`check` are checker-only.** The EIR backend derives return types + separately in `call_return_type` (`src/ir_lower/expr/mod.rs`); a `returns: Mixed` + + precise-`check` builtin also needs a matching EIR return-type arm, or the checker and + EIR disagree on the value's type. +- **Separate surfaces still need hand-wiring** when relevant: the EIR emitter/runtime + routine, optimizer effects (`src/optimize/effects/builtins.rs`), and the + runtime-callable wrapper exclusion (`src/codegen/callable_dispatch.rs`). +- `isset`/`unset`/`empty`/`exit`/`die`/`buffer_*` are language constructs that stay + 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. + +### Adding a new EIR optimization pass + +IR-level transformations run after EIR lowering/validation through a fixed-point driver (`src/ir_passes/`), not in the AST optimizer. + +Key invariants: + +- Implement the `IrPass` trait in `src/ir_passes/.rs` and register it in `default_passes()` (`src/ir_passes/driver.rs`); the driver re-runs the whole set per function to a fixed point, capped by `MAX_PASS_ITERATIONS`. +- Reuse `src/ir_passes/rewrite.rs` (RAUW via `replace_all_uses` + shared fold helpers) instead of re-walking operands/terminators. Keep rewrites dominance-safe and PHP-equivalent; cross-check edge cases (division by zero, signed-zero/`NaN` floats) with `php -r`. +- Debug/test builds re-validate each function after every pass and panic (naming the pass) on malformed IR or non-convergence; both guards compile out of `--release`. +- Behavior must be identical with `--ir-opt` on or off except for performance (gate: `--ir-opt=on|off` / `--no-ir-opt`, env `ELEPHC_IR_OPT`, default on). Add unit tests (`src/ir_passes/tests/`) and e2e tests (`tests/codegen/optimizer/`) using runtime-unknown values (e.g. `$argc`) so the construct survives AST folding. + +### Call argument semantics + +All function-like call surfaces must share the same argument rules instead of normalizing locally in individual emitters: + +- Shared named/positional/spread semantics live in `src/types/call_args/` whenever they are not codegen-specific. +- `src/types/call_args/` owns the semantic planner (`CallArgPlan` / `plan_call_args`). The checker and EIR lowering should consume that plan; they should not rebuild named-argument matching, duplicate detection, static associative-spread expansion, spread bounds, or the regular/variadic split locally. +- If a codegen surface uses an internal signature with hidden parameters, such as closure captures, pass the caller-visible regular parameter count through `plan_call_args_with_regular_param_count()` instead of letting the planner infer it from the full internal signature. +- Type-checker validation and diagnostic mapping lives in `src/types/checker/functions/call_validation.rs`; it maps planner errors to `CompileError` diagnostics instead of owning the semantic rules. +- `src/ir_lower/expr/` owns active EIR call-argument lowering: planner consumption, source-order named/spread lowering, spread checks, and hidden-temp creation. `src/codegen/` then materializes the lowered call through target-aware ABI helpers. +- User-defined calls, builtins, and extern calls must use the same named/spread normalization rules before any callee-specific lowering runs. +- PHP call unpacking with static string keys maps to named arguments (`f(...["a" => 1])` behaves like `f(a: 1)`). Static numeric keys remain positional, and duplicate static string keys inside one unpack use PHP's last-wins behavior before planning. +- When adding or extending a builtin, verify `first_class_callable_builtin_sig()` as well as the direct builtin signature so first-class callable syntax and callable aliases stay coherent. +- PHP source evaluation order is distinct from ABI parameter order. Preserve side effects in source order, then materialize arguments in parameter/ABI order; extern calls follow the same rule before C ABI register loading. +- Spread arguments before named arguments must be evaluated once, length/overwrite checks must happen at the PHP-observable point, and later named-argument side effects must not be skipped by early codegen checks. Too-short spreads for required parameters must fail instead of reading past the array payload. +- A positional spread into a variadic callee fills visible regular parameters first; only the remaining tail becomes the variadic array. +- User-defined variadics accept unknown named arguments as string-keyed variadic entries; internal/builtin variadics reject unknown named arguments like PHP internals. +- Ref-like parameters, including mutating builtin parameters, must avoid value-temp preevaluation so the original storage is passed/mutated. +- If hidden named-argument temporaries are introduced, update `src/ir_lower/context.rs` and EIR frame placement so slots are allocated before frame-size calculation. + +### Optimizer and effects + +The optimizer assumes side effects are modeled conservatively. When changing calls, operators, expressions, statements, or builtins: + +- Update `src/optimize/effects/` if purity, variable reads/writes, call effects, filesystem/runtime state, exceptions/fatals, or by-ref mutation behavior changes. +- Do not mark a call as pure if it can read or write globals, files, environment, runtime heap state, object properties, array contents, argument storage, or can emit visible output. +- Keep constant folding in `src/optimize/fold/` limited to PHP-equivalent results. If PHP behavior is edge-case sensitive, cross-check with `php -r`. +- Add optimizer regression tests under `tests/codegen/optimizer/` when DCE, constant propagation, control-flow pruning, or folding can observe the change. +- Magic constants must be lowered before optimizer passes. Do not introduce optimizer paths that expect raw `ExprKind::MagicConstant`. +- `src/optimize/` is the AST optimizer only. IR-level (EIR) transformations live in `src/ir_passes/` behind the fixed-point pass driver; see "Adding a new EIR optimization pass". Folds that need value identity, basic blocks, or dominance belong there, not in `src/optimize/`. + +### Runtime ownership, GC, and COW + +Refcounted runtime values are not plain scalars. When changing arrays, strings, objects, `Mixed`, `Iterable`, call returns, or temporaries: + +- Preserve the boxed `Mixed` cell contract: the runtime tag and payload shape must stay consistent across codegen and runtime helpers. +- Respect copy-on-write before mutating arrays or hashes. Use the existing ensure-unique helpers instead of mutating shared storage directly. +- Track whether a value is owned, borrowed, persistent, or a temporary result. Release only values this code path owns. +- Keep cleanup paths balanced across normal returns, early exits, throws/fatals, and control-flow merges. +- Add focused tests in `tests/codegen/runtime_gc/` for ownership, aliasing, cycles, heap debug, stack args, and COW changes. + +### File size policy + +As a general rule, aim to keep source files under **500 lines of code**. This is a maintainability guideline, not a blind numeric rule. + +The real goal is to avoid files that become hard to reason about because they mix multiple responsibilities. In practice: + +- **Dispatcher/orchestration files** (`mod.rs`, top-level drivers, large checker/codegen coordinators) should stay slim. If they grow large, split them aggressively. +- **Multi-responsibility files** should be split once they start accumulating unrelated concerns, even if the line count is not yet extreme. +- **Leaf files that implement one cohesive feature** are allowed to exceed 500 lines when splitting them would create artificial fragmentation. + +Examples of files that may reasonably stay above the soft limit: + +- a single runtime emitter implementing one substantial builtin or runtime routine +- a single compiler pass file that is still clearly about one feature and one code path +- a self-contained parser/lowering/runtime leaf where splitting would only spread one mental model across several tiny files + +Examples of files that should usually be split: + +- a file that mixes dispatch, validation, data collection, and post-processing +- a file that contains several unrelated builtins or runtime helpers +- a file that acts as a “miscellaneous bucket” for code that did not get a home + +So the policy is: + +- treat **500 LOC as a warning sign** +- treat **mixed responsibilities** as the real trigger for refactoring +- do **not** split a file that owns one coherent feature just to satisfy the number + +In short: prefer **cohesion over mechanical line-count compliance**. A 650-line mono-feature leaf is acceptable; a 350-line multi-purpose orchestrator is already a refactor candidate. + +### Rust module preamble policy + +Every repo-owned Rust source file (`*.rs`) must start with a module-level Rustdoc preamble before any `use`, `mod`, item, or test helper code. Use `//!` comments so the explanation is attached to the module in rustdoc. + +The preamble is mandatory for all new Rust files and must be added or preserved when touching existing Rust files. Release verification should report any Rust file that is missing it. + +Standard format: + +```rust +//! Purpose: +//! Explain what this file owns in 2-4 lines. +//! +//! Called from: +//! - `crate::path::caller()` or the relevant test/module entry point. +//! +//! Key details: +//! - Important invariants, ordering constraints, ownership/ABI/runtime rules, or coupling. +``` + +For test files, use the same structure but describe the test surface instead of production callers: + +```rust +//! Purpose: +//! Integration or regression tests for the relevant feature area. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Fixture layout, platform assumptions, ignored-test requirements, or why edge cases exist. +``` + +Keep preambles concise and factual. Do not include refactor history, stale line numbers, or broad architecture prose that belongs in `docs/internals/`. + +### Rust function docblock policy + +Every explicit Rust function in repo-owned Rust source files must have a concise docblock explaining what that function does. This applies equally to public functions, restricted-visibility functions (`pub(crate)`, `pub(super)`, etc.), private helper functions, impl methods, trait methods, and test functions. + +Use `///` Rustdoc comments immediately before the function item or its item attributes. Keep the docblock specific to the function's actual responsibility, inputs, outputs, side effects, ownership/ABI/runtime constraints, and failure behavior when those details matter. Do not use vague filler such as "handles logic" or "processes data". + +When documenting test functions, describe the behavior or regression being verified and any important fixture/platform assumptions. For explanatory comments inside a function body, use normal `//` comments, not `///`; Rustdoc comments inside function bodies produce warnings or errors because they do not document an item. + +Adding or updating function docblocks must not change code behavior. Do not alter function signatures, visibility, attributes, derives, module declarations, control-flow braces, strings, assembly instructions, or instruction-comment alignment while adding documentation. If a doc-only change causes `cargo check`, `cargo check --tests`, or `git diff --check` to fail, fix the documentation placement or comment style rather than changing code to fit the comment. + +### Codegen conventions (target-aware) + +- Prefer helpers from `src/codegen_support/abi/` for registers, stack slots, frame layout, argument materialization, symbol addresses, and calls. +- New feature emitters belong in `src/codegen/`. +- New feature emitters must support every supported target through `emitter.target` or clearly isolate target-specific code behind existing target helpers with explicit tests and diagnostics. +- Avoid hardcoding ARM64 register names, x86_64 register names, syscall numbers, object formats, or stack alignment rules in shared lowering code. +- Do not add an ARM64-only runtime helper, builtin emitter, ABI path, or ownership cleanup path unless the feature is intentionally target-gated and documented as unsupported elsewhere. +- Target-sensitive changes need coverage for every supported target they can affect. During local implementation, run focused target checks only when they are needed for confidence; rely on CI for the complete supported-target matrix unless the user requests local Docker runs or CI cannot provide the needed signal. + +### ARM64 quick reference + +- **Integers**: result in `x0` +- **Floats**: result in `d0` +- **Strings**: pointer in `x1`, length in `x2` +- **Function args**: `x0`-`x7` (int = 1 reg, string = 2 regs), `d0`-`d7` (floats) +- **Return value**: same as expression result (`x0`, `d0`, or `x1`/`x2`) +- **Stack frame**: `x29` = frame pointer, `x30` = link register, locals at negative offsets from `x29` +- **ABI helpers**: `src/codegen_support/abi/` centralizes load/store/write per type +- **Labels**: use `ctx.next_label("prefix")` — global counter prevents collisions across functions +- **Mixed values**: `PhpType::Mixed` is an internal boxed runtime shape used for heterogeneous associative-array values; codegen/runtime must preserve the boxed cell contract instead of treating it like a plain scalar + +### Assembly comment policy + +Every `emitter.instruction(...)` call must have an inline `//` comment aligned to +column 81, with `// -- description --` block comments before related groups. + +## Examples + +Each example lives in `examples//main.php` with its own `.gitignore`. To run: + +```bash +cargo run -- examples/fizzbuzz/main.php +./examples/fizzbuzz/main +``` + +## PHP compatibility + +**PHP-derived syntax must be 100% compatible with PHP.** When elephc implements a PHP construct (variables, operators, keywords, built-ins), it must behave identically to PHP. This means: + +- Variable names, keywords, operators, and built-in function names must match PHP exactly +- Superglobals (`$argc`, `$argv`) must use PHP's syntax (e.g., `$argv[0]`, not `argv(0)`) +- Operator precedence and associativity must match PHP +- String escape sequences must match PHP behavior +- Built-in function signatures must match PHP (argument count, order, types) + +When in doubt, test with `php -r '...'` to verify behavior. + +**elephc also provides compiler-specific extensions** beyond standard PHP (e.g., `ptr`, `extern`, `buffer`, `packed class`). These features have no PHP equivalent and are not expected to run under the PHP interpreter. They are clearly distinguishable from PHP syntax and exist to enable use cases (FFI, game development, low-level memory access) that PHP cannot address. + +## Documentation + +The `docs/` directory is the project's complete documentation, organized into the following sections: + +``` +docs/ +├── README.md # Main index +├── getting-started/ # Installation and first program +│ ├── installation.md +│ └── your-first-program.md +├── compiling/ # The compiler CLI: flags and the full compilation process +│ ├── overview.md +│ ├── compilation-pipeline.md +│ ├── cli-reference.md +│ ├── targets.md +│ ├── optimization.md +│ ├── output-and-diagnostics.md +│ └── linking-and-conditional-compilation.md +├── php/ # PHP syntax (standard PHP features) +│ ├── types.md +│ ├── operators.md +│ ├── control-structures.md +│ ├── functions.md +│ ├── strings.md +│ ├── arrays.md +│ ├── math.md +│ ├── classes.md +│ ├── namespaces.md +│ ├── magic-constants.md +│ └── system-and-io.md +├── beyond-php/ # Compiler extensions (not valid PHP) +│ ├── pointers.md +│ ├── buffers.md +│ ├── packed-classes.md +│ ├── extern.md +│ └── ifdef.md +└── internals/ # Compiler internals + ├── what-is-a-compiler.md + ├── how-elephc-works.md + ├── the-lexer.md + ├── the-parser.md + ├── the-type-checker.md + ├── the-optimizer.md + ├── the-codegen.md + ├── the-runtime.md + ├── memory-model.md + ├── architecture.md + ├── arm64-assembly.md + └── arm64-instructions.md +``` + +### Astro compatibility + +All docs files are Markdown with YAML frontmatter compatible with Astro content collections. Every `.md` file **must** have this frontmatter format: + +```yaml +--- +title: "Page Title" +description: "One-line description of the page." +sidebar: + order: N +--- +``` + +- `title` replaces the `# Heading` — do **not** add a top-level `# Title` in the body (Astro renders it from frontmatter) +- `sidebar.order` controls page ordering within its section +- No navigation links (`[← Back]`, `Next:`, etc.) — Astro handles navigation +- Use standard Markdown (CommonMark). No custom shortcodes or Astro components inside docs + +### Keeping docs up to date + +**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. +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`. +5. If a feature was previously listed as "not supported", remove that note. +6. If there are known incompatibilities with PHP, document them in `docs/php/types.md` (incompatibilities section). +7. Update `docs/README.md` index if adding a new page. + +## Roadmap management + +`ROADMAP.md` tracks all planned and completed work, organized by version. + +- **Never remove completed items** from a version section. Mark them as `[x]` and leave them under the version they belong to. This preserves the history of what was delivered in each release. +- New work items go under the appropriate future version. +- When all items in a version are completed, the version is considered done — do not move items elsewhere. + +## Changelog management + +`CHANGELOG.md` records every released version, newest first, in *Keep a Changelog* style. + +When cutting a release: + +- Add a new section at the top (under the header), above the previous version: + + ``` + ## [X.Y.Z] - YYYY-MM-DD + - One terse, user-facing bullet per notable change. + ``` + + Keep entries concise (usually one or two bullets), describe what shipped — not the implementation — and use the absolute release date. +- Add a matching compare link at the **bottom** of the file, also newest first, immediately above the previous version's link: + + ``` + [X.Y.Z]: https://github.com/illegalstudio/elephc/compare/v...vX.Y.Z + ``` + + The first-ever release uses the `releases/tag/v0.1.0` form instead of a compare range. Every version section must have its link; do not leave the link out. +- Never change elephc's Cargo package version in `Cargo.toml` or `Cargo.lock`. Release automation in CI owns Cargo version bumps; agent changes should leave those files' version numbers untouched unless the user explicitly overrides this policy. + +## Conventions + +- No `Co-Authored-By` lines in commits +- Use commit message prefixes such as `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, or `test:` +- Keep commit messages concise +- Run the focused pre-commit verification above before committing code changes. Do not knowingly commit with relevant focused tests failing; the full suite must pass in CI. +- Zero compiler warnings policy (`cargo build` must be clean) +- Never run `cargo fmt` in this repo. Use targeted manual edits only; global formatting creates noisy churn here. diff --git a/CHANGELOG.md b/CHANGELOG.md index c42753a1cb..60c45752db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,55 @@ All notable changes to elephc, a PHP-to-native compiler written in Rust. Releases are listed newest first. ## [Unreleased] +- Fixed name resolution inside named-argument values (issue #495): an imported alias or namespace-relative name nested in a named argument's value — such as `Url` in `new self(url: new Url('/'))` — is now rewritten to its canonical fully-qualified form like positional arguments, instead of failing with "Undefined class: Url". The name resolver's expression walk previously had no `NamedArg` arm, so the value expression escaped rewriting entirely. +- Removed the legacy direct AST → ASM backend completely. EIR is now the only + codegen implementation path. +- `in_array()` now honors its optional third `$strict` argument: omitted/false + uses PHP loose membership for supported scalar/string paths, including + numeric-string coercion, string loose equality, and bool/int truthiness, while + `true` uses strict type-identical membership. +- Add an experimental Windows x86_64 (PE32+) cross-compilation target (`--target windows-x86_64`, alias `x86_64-pc-windows-gnu`): produces a GNU/MinGW-ABI `.exe` (`.dll` for `--emit cdylib`) via `x86_64-w64-mingw32-gcc`, and requires the MinGW-w64 cross toolchain on the host doing the build. Runtime shim coverage and end-to-end execution testing are still catching up to macOS/Linux, so treat it as cross-compilation support rather than a fully verified target. +- Add the `random_bytes(int $length): string` builtin: a cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or a length below 1. +- Int-backed enum `from()` / `tryFrom()` now accept a dynamically-typed (`mixed`) argument (issue #449): a `foreach` value over a heterogeneous array, an untyped parameter, etc. are coerced on their runtime type before the enum lookup — integer/numeric-string resolve (or throw `ValueError`), float truncates, bool/null coerce, and array/object/resource/closure throw `TypeError` naming the given type. Previously any `mixed` argument was rejected at compile time. Target-aware on every supported backend. +- Int-backed enum `from()` / `tryFrom()` now accept a numeric string (issue #349): `Level::from("1")` coerces the string to the integer backing value (as a distinct EIR coercion lowered before the enum call) and returns the matching case, instead of being rejected at compile time. A numeric string with no matching case throws `ValueError`; a non-numeric string (e.g. `"x"`) throws `TypeError` with PHP's exact argument-type message — matching PHP's coercive typing on every supported target, including PHP-rejected libc `strtod` extensions such as hexadecimal `"0x1"`, `"INF"`, and `"NAN"`. +- Fixed an enum `from()` / `tryFrom()` refcount bug (surfaced while fixing #349): the returned case singleton was under-retained, so storing the result into a reassigned variable inside a loop drove the persistent singleton's refcount to zero and freed it — producing garbage reads or a heap crash after a few iterations. `from()`/`tryFrom()` now retain the matched singleton, keeping it alive like direct case access. Affected both backed-enum backings. +- Fixed integer-arithmetic overflow parity (issue #369): runtime `int + int`, `int - int`, and `int * int` now promote to `float` on 64-bit overflow instead of wrapping, clamping, or staying statically `int`, while non-overflowing runtime arithmetic remains `integer`. The EIR backend now lowers checked integer arithmetic through target-aware runtime helpers, folds constant checked operations back to scalar `int`/`float` values under `--ir-opt`, and preserves PHP behavior through chained arithmetic plus prefix/postfix increment overflow. The ownership cleanup around widened `Mixed` results, statics, ordinary globals, returned locals, and `--web` request resets was tightened so the extra boxed values introduced by checked arithmetic are released exactly once. +- Fixed a constant-propagation miscompile with by-reference calls in a `match` subject (issue #384): `echo match(bump($i)) { ... } . "|" . $i` kept the pre-call constant for `$i` and printed the stale value, because a call's unknown write set was treated as "no writes" instead of forcing conservative invalidation. A read sequenced after any by-reference-mutating call in the same expression now observes the post-call value, matching PHP; pure calls (`gettype`, `strlen`, …) keep their operands foldable. +- Fixed PHP parser/codegen parity for parenthesis-free object instantiation (issue #371): `new Foo`, `new self`, `new static`, `new parent`, and `new $class` now compile with empty constructor arguments, while immediate postfix forms such as `new Foo->bar`, `new Foo::bar()`, `new Foo?->bar`, and `new Foo[0]` are rejected instead of being misparsed. +- Fixed three PHP parity regressions in the EIR backend: indexed array elements such as `$a[0]` can now be passed to by-reference parameters with copy-on-write storage split before mutation (issue #360); `IteratorAggregate::getIterator()` may declare the marker `Traversable` return type while `foreach` still dispatches through the returned object's concrete `Iterator` methods (issue #385); and catchable private/protected method access plus readonly-property write errors now preserve PHP's receiver/RHS evaluation order before throwing `Error` (issue #383). The merge also removes a duplicate `_spl_error_class_id` data symbol that could make post-merge user assembly fail to assemble. +- Fixed EIR parity for two PHP runtime edge cases: `foreach` by reference now observes elements appended during iteration instead of using the by-value snapshot length, while by-value indexed and mixed-array iteration still stops at the original array length; and reading an uninitialized typed instance or static property now emits PHP's specific fatal message when uncaught while still throwing a catchable `Error` when an exception handler is active. +- Fixed missing-key reads on indexed arrays: null coalescing now suppresses undefined-key warnings for missing integer, string, mixed, and `null` keys while direct reads still emit PHP-compatible `Warning: Undefined array key ...` diagnostics and return the correct null fallback. The EIR/runtime paths now handle mixed-key indexed reads consistently across macOS ARM64, Linux x86_64, and Linux ARM64, including string-key warnings and mixed integer-key misses. +- Fixed static-member and integer-division parity (issues #336, #372, #356): `static::CONST` now late-binds to the runtime class and falls back to the declaring-class value when not overridden; prefix/postfix `++` and `--` now work on static properties through `ClassName::$x`, `self::$x`, `static::$x`, and `parent::$x`; and `intdiv(PHP_INT_MIN, -1)` now throws catchable `ArithmeticError` with PHP's message instead of wrapping or trapping on every supported backend target. +- Fixed mixed float loose equality and `switch` comparisons (issue #397): `Mixed` float operands now compare numerically instead of truncating through integers, and numeric loose equality dispatches through the boxed runtime tag so numeric strings, booleans, non-scalars, and NaN/unordered float comparisons follow PHP semantics on every supported backend target. +- Fixed undefined-variable compound assignments (issue #370): `$x += 1`, `$x -= 1`, `$x *= 5`, and `$y .= "..."` now treat the missing target as PHP `null`/`0`/`""` with a single warning instead of failing during type checking or reading uninitialized stack storage. Null-coalescing assignment (`??=`) remains warning-free and now also works correctly when used as an expression, such as `echo ($z ??= 42)`. +- Fixed enum type resolution in class member positions: enum names can now be used as declared property types and constructor-promoted property types without failing early with "Unknown type" during the class schema pass. +- Fixed the Linux x86_64 `strtotime()` weekday-modifier scanner: `next Mon`, `last Fri`, and similar modifier + weekday forms now pass the remaining input length capped at 16 bytes to the keyword matcher, matching the ARM64 path and avoiding a fragile fixed-width scan into the zero-padded lowercase buffer tail. + +## [0.26.0] +- Runtime dead stripping: compiled executables now link only the runtime helpers the program actually reaches and drop the rest, shrinking binaries without changing behavior. Works on every supported target — Linux via per-symbol sections and `--gc-sections`, macOS via `.subsections_via_symbols` atoms and `-dead_strip`. Shared libraries (`--emit cdylib`) keep the full runtime. - Closures can be rebound to a new receiver: `Closure::bind()`, `bindTo()`, and `Closure::call()` are supported, and a top-level closure that captures `$this` now binds it correctly instead of losing the receiver. A by-reference `Closure::bind` stored in a variable and called later is tracked as a static callable, so the call carries the bound cell directly rather than going through the generic descriptor invoker. - New magic methods `__callStatic`, `__isset`, and `__unset`: a static call to an undeclared method dispatches to `__callStatic`, `isset()`/`empty()` on an undeclared property route through `__isset` (and only read `__get` when `__isset` is truthy, so an unset virtual property is empty without ever being read), and `unset($obj->prop)` on a virtual property calls `__unset`. - Reflection over functions: `ReflectionFunction` (name and parameter counts), `getParameters()`, `ReflectionParameter`, `ReflectionParameter::getType()`, and `ReflectionNamedType`. Attribute arguments are now exposed in reflection metadata, including float, positional-array, named-argument and associative-array values, references to global and class constants, and enum-case references. - References to object properties: `$x = &$obj->prop` aliases the property with write-through in both directions, and a by-reference function/method return can be captured with `$x = &f()`. By-reference returns also work for `string`- and `float`-typed properties. Reassigning an array reference to a non-empty literal of a different type boxes the literal's elements to match the property's element type. - `unset()` on array elements: `unset($hash[$key])` removes an associative entry and `unset($arr[$key])` removes a packed indexed element with sparse semantics. `array_map()` now works over heterogeneous (mixed-element) arrays. +- Added 15 array builtins on the EIR backend: `array_is_list()`, `array_key_first()`/`array_key_last()`, `array_replace()`/`array_replace_recursive()`, `array_diff_assoc()`/`array_intersect_assoc()`, `array_merge_recursive()`, `array_walk_recursive()`, `array_find()`/`array_any()`/`array_all()` (PHP 8.4), `array_udiff()`/`array_uintersect()`, and `array_multisort()`. The hash-based set operations accept associative arrays and scalar-element indexed arrays (converted to integer-keyed hashes, with result keys/values widening to `mixed` for heterogeneous inputs); the predicate/comparator builtins accept string, function, and non-capturing closure callbacks. All are target-aware (macOS ARM64, Linux x86_64, Linux ARM64) and documented in `docs/php/arrays.md`. - Added the enum case `->name` property (issue #330): every enum case, pure or backed, now exposes the read-only `name` string holding the case identifier (`E::A->name` is `"A"`), matching PHP's `UnitEnum::$name`. Previously this property access was rejected at compile time with "Undefined property". Backed cases keep `->value`, and `$this->name` is now readable inside enum methods. Access works through direct case access, an aliasing variable, `cases()`, and string interpolation. - Generators now run on stackful coroutines (issue #329): a generator body is compiled by the normal backend and runs on its own coroutine stack, so `Generator::throw()` raises the exception at the suspended `yield` and a `try`/`catch` *inside* the generator body handles it and resumes — instead of always terminating the generator and propagating to the caller. In-generator method calls, arbitrary control flow, and `try`/`finally` around `yield` work like ordinary functions; `yield from` (over generators and arrays), `send()`, and `getReturn()` are preserved. Generator parameters passed on the caller stack (e.g. a 7th integer parameter under the x86_64 SysV ABI) are now forwarded correctly instead of arriving as zero, and a generator declaring more than 7 parameters (counting closure captures) is rejected with a clear diagnostic rather than corrupting coroutine state. - Fixed associative-array method parameters (issue #406): an `array`-typed parameter of an instance or static method now preserves the associative shape known at the call site, exactly like a free-function `array` parameter. String-key access (`$d['a']`) type-checks instead of failing with "Array index must be integer", and `json_encode()` of the parameter emits a JSON object instead of a JSON list with garbage values. A declared generic `array` parameter is sharpened from the call-site argument type during method-call inference, and the sharpened shape is used when checking the method body. - Fixed associative-array property defaults (issue #407): a typed `array` (or untyped) property initialized with an associative literal such as `['a' => 1]` is now stored as associative (hash) storage, so string-key reads and writes (`$this->data['a']`, `$this->data[$key]`) type-check and run instead of failing with "Array index must be integer". The EIR backend also lowers string-keyed associative literal defaults for instance and static properties instead of rejecting them as an unsupported `object_new` feature. +- Fixed heterogeneous associative-array property defaults (issue #413): a typed `array` or untyped property initialized with an associative literal whose values have different types — such as `public array $data = ['n' => 1, 's' => 'hi'];` — now compiles, inferring a boxed `mixed` value slot instead of being rejected with an `unsupported EIR backend feature: prop_set` error. Previously the value type was over-widened to a single scalar (`int` + `string` → `string`), which diverged from the array's actual heterogeneous shape. +- `serialize()` / `unserialize()` builtins covering scalars, nested arrays, and objects — including the `__serialize`/`__unserialize`/`__sleep`/`__wakeup` magic methods and `r:`/`R:` object back-references (repeated objects rebuild as one shared instance) — byte-for-byte compatible with PHP's wire format. +- `Phar` / `PharData` global metadata and stub now persist into the archive (native PHAR, tar, and zip) and round-trip across objects and processes via `setMetadata()`/`getMetadata()`/`hasMetadata()`/`delMetadata()` and `setStub()`/`getStub()`. +- `PharFileInfo` per-file metadata (`setMetadata()`/`getMetadata()`/`hasMetadata()`/`delMetadata()` on `$phar["entry"]`) now persists per-entry into the archive for native PHAR, tar, and zip, round-tripping across objects and the PHP interpreter. +- `PharData::compress(Phar::GZ|Phar::BZ2)` / `decompress()` now perform whole-archive tar compression, writing a sibling `.tar.gz` / `.tar.bz2` (or plain `.tar`); compressed archives are read transparently and are interchangeable with the PHP interpreter. +- `Phar::setSignatureAlgorithm()` / `getSignature()` now sign native PHAR, tar, and zip phars, including `Phar::OPENSSL` RSA-SHA1 signing with a PEM private key (verifiable by the PHP interpreter) alongside the MD5/SHA1/SHA256/SHA512 hash algorithms. Tar/zip signatures use a `.phar/signature.bin` control entry. +- `phar://` zip readers now accept entries written with a streaming data descriptor (general-purpose flag bit 3), reading the authoritative central-directory sizes instead of rejecting the archive. +- ZIP64 phar archives (over 65535 entries, or sizes/offsets over 4 GiB) are now read and written, interchangeable with the PHP interpreter and other ZIP64 tools. +- `Phar`/`PharData::setZipPassword()` (a compiler extension) reads and writes traditional-PKWARE (ZipCrypto) encrypted ZIP entries: with a password set, zip entries (the stub included) are encrypted on write and decrypted on read, while the `.phar/signature.bin` entry stays in the clear. The cipher is cryptographically weak and kept only for compatibility with legacy archives. +- EIR backend correctness: method calls on `mixed` values and on object-iterator `foreach` values (e.g. `DirectoryIterator`, `FilesystemIterator`) now dispatch synthetic SPL methods instead of crashing on an unemitted vtable slot, and `fopen("compress.zlib://…")` / `fopen("compress.bzip2://…")` read wrappers now decompress on the EIR backend. +- Type-checker correctness: a method call on a `mixed` receiver now infers the union of the declared return types across the classes that declare the method (mirroring the runtime class-id dispatch) instead of falling back to `int`. An un-annotated function such as `function f($x) { return $x->name(); }` now returns the method's actual value — previously the inferred `int` return type coerced a returned string to `0`. +- Fixed `foreach`-key array rebuilds: `foreach ($src as $k => $v) $dst[$k] = $v` now preserves every entry instead of collapsing all string keys onto index `0` (which dropped all but the last). Sparse and negative integer keys are kept with PHP semantics — the destination promotes to hash storage instead of zero-filling gaps or silently dropping negative-index writes — while contiguous integer-key rebuilds stay on indexed storage so `implode()` and other indexed consumers are unaffected. A `foreach` key variable no longer leaks its key classification into other functions that reuse the same name, and reassigning the key variable to a string inside the loop routes the write as a string key. +- Resource scope-cleanup: an `fopen()` stream, `popen()` pipe, `opendir()` directory handle, or `hash_init()` hashing context that leaves scope without an explicit `fclose()`/`pclose()`/`closedir()`/`hash_final()` is now released automatically at scope exit (closing the fd, reaping the `popen` child, freeing the context) instead of leaking until the process exits. Aliasing (`$b = $a`) is reference-counted so the handle is freed exactly once; finalizing a hash context and then dropping it is a single safe free; and an explicit close is never doubled even if the descriptor number is later reused. ## [0.25.2] - 2026-06-26 - `--web`: compile a PHP program into a standalone prefork HTTP server binary with per-request top-level execution, `echo`/`print` response bodies, `$_SERVER`/`$_GET`/`$_POST` and `php://input` request input, PHP-compatible `http_response_code()`/`header()` handling, configurable listen address/workers/body limit, clean signal shutdown, worker respawn, bounded keep-alive handling, fixed-heap request cleanup, and full sharded CI coverage across macOS ARM64, Linux x86_64, and Linux ARM64. @@ -413,7 +453,8 @@ 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.25.2...HEAD +[Unreleased]: https://github.com/illegalstudio/elephc/compare/v0.26.0...HEAD +[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 [0.25.1]: https://github.com/illegalstudio/elephc/compare/v0.25.0...v0.25.1 [0.25.0]: https://github.com/illegalstudio/elephc/compare/v0.24.3...v0.25.0 diff --git a/CLAUDE.md b/CLAUDE.md index 08d0d5b38a..d8247be8b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,559 +1 @@ -# elephc — Developer Guide - -## What is this - -A PHP-to-native compiler written in Rust. Compiles a static subset of PHP to native assembly for the supported target matrix, producing standalone binaries. No interpreter, no VM, no runtime dependencies. - -## Supported target policy - -All supported targets are first-class targets. The supported target matrix is currently `macos-aarch64`, `linux-aarch64`, and `linux-x86_64`. - -Do not design or land codegen/runtime features as ARM64-first with x86_64 treated as a later port. New features, builtins, runtime helpers, optimizer assumptions that affect emitted code, ABI behavior, and ownership/GC paths must either support every supported target in the same change or clearly isolate an intentionally unsupported path with diagnostics, tests, and documentation. A feature is not considered done while any supported target has a missing runtime symbol, reduced semantics, stale documentation, or an untested target-specific lowering path. - -When examples or internals docs use ARM64 snippets for readability, treat them as examples only. Implementation work must keep the target-aware ABI/runtime boundaries authoritative. - -## Build & run - -```bash -cargo build # dev build -cargo build --release # optimized build -cargo run -- file.php # compile a PHP file -``` - -The compiler outputs a native binary next to the source file (e.g., `file.php` → `file`). - -## Test policy - -**Every feature must have tests before it's considered done.** The test suite is the primary quality gate. - -Local agent runs should stay focused. By default, run only the build checks and tests needed to validate the implementation or regression you touched, then rely on CI for the complete matrix. Do **not** run the full local suite (`cargo test`, `cargo test -- --include-ignored`, or full Linux Docker scripts) unless the user explicitly asks for it, you are doing a release/pre-release verification, CI is unavailable for the needed signal, or a broad/high-risk change cannot be validated responsibly with focused tests. - -### Running tests - -```bash -cargo test test_fizzbuzz # run a focused test by name -cargo test --test codegen_tests test_name # run a focused end-to-end codegen test -cargo nextest run --profile ci test_name # run a focused nextest test with per-test timeout -cargo test # full local suite; only when explicitly requested/necessary -cargo test -- --include-ignored # ignored tests; only when relevant or requested -``` - -Linux target-specific regressions can be checked through the Docker scripts in -`scripts/`. Prefer filters during implementation; the unfiltered commands run -full Linux suites and should be reserved for explicit requests or genuinely -necessary local target validation. - -```bash -./scripts/test-linux-x86_64.sh iterable # run tests matching a filter -./scripts/test-linux-arm64.sh test_my_feature # run tests matching a filter -./scripts/test-linux-x86_64.sh # full Linux x86_64 suite; only when necessary/requested -./scripts/test-linux-arm64.sh # full Linux ARM64 suite; only when necessary/requested -./scripts/test-linux-x86_64.sh --rebuild # rebuild the Docker image first -./scripts/test-linux-arm64.sh --rebuild # rebuild the Docker image first -``` - -Some tests are marked `#[ignore]` because they require external libraries (e.g., SDL2) not available in CI. Run ignored tests only when the change touches that surface, during explicit release verification, or when the user asks for them. - -### Test strategy during development - -The full test suite is slow because each codegen test spawns `as` + `ld` + runs the binary. CI runs the complete supported matrix (`macos-aarch64`, `linux-x86_64`, and `linux-aarch64`) with sharded codegen tests, so local implementation work should optimize for fast, relevant signal: - -1. **While developing a feature**: run only the tests for that feature or regression (`cargo test test_my_feature`). -2. **Scope to a single test binary**: prefer `cargo test --test codegen_tests ` over a bare `cargo test `. A bare filter rebuilds and links all six test binaries every cycle (~2.5s of wasted link time); `--test ` links only the one you need. Most codegen work lives in `codegen_tests`. -3. **For target-sensitive changes**: run the smallest relevant macOS/Linux focused tests locally when they materially reduce risk; otherwise let CI provide complete target coverage. -4. **For broad infrastructure changes**: validate the edited workflow/config directly (YAML parse, `cargo metadata`, `nextest` config checks, etc.) instead of running unrelated Rust suites. -5. **PHP cross-check**: opt in narrowly with `ELEPHC_PHP_CHECK=1 cargo test ` when PHP equivalence is the question. - -### Pre-commit verification - -Before committing code changes, run the smallest useful focused tests and hygiene checks that cover the implementation. Do not run full local suites by default; CI is responsible for the complete sharded matrix after push/PR. - -```bash -cargo build # for code changes that should compile warning-free -cargo test # or the narrower --test form -git diff --check -``` - -For docs-only, workflow-only, or configuration-only changes, replace Rust test runs with the relevant syntax/metadata checks. For codegen changes, also verify assembly-comment coverage/alignment for any files you touched. If the change can affect generated assembly, runtime helpers, ABI behavior, linking, ownership/GC, or target-specific libraries, run focused tests for affected supported targets when local evidence is needed; otherwise rely on CI for full Linux x86_64/Linux ARM64/macOS ARM64 coverage. - -### Test structure - -| File | What it tests | How | -|---|---|---| -| `tests/lexer_tests.rs` | Tokenization | Asserts token sequences from source strings | -| `tests/parser_tests.rs` | AST construction | Asserts AST node structure and operator precedence | -| `tests/codegen_tests.rs`, `tests/codegen/` | Full pipeline (end-to-end) | Compiles PHP → binary, runs it, asserts stdout | -| `tests/error_tests.rs`, `tests/error_tests/` | Error reporting | Asserts that invalid programs produce the right error messages | - -### Test coverage requirements - -- **New language construct** (keyword, operator, statement): needs lexer, parser, codegen, AND error tests -- **New operator**: needs a Pratt parser binding power test verifying precedence relative to adjacent operators -- **New statement type**: needs at least one codegen test showing correct output, one test for edge cases (empty body, nested), and one error test for malformed syntax -- **New built-in function**: needs codegen tests for normal use and error test for wrong argument count/types -- **Bug fix**: must include a regression test that would have caught the bug -- **Every feature also needs an example** in `examples/`. If an existing example can showcase the new feature naturally, update it. Otherwise, create a new `examples//main.php` with its own `.gitignore` (containing `*.s`, `*.o`, `main`). Examples should be small, readable programs that demonstrate real use cases — not just test cases. - -### Writing codegen tests - -Codegen tests compile inline PHP source and assert stdout: - -```rust -#[test] -fn test_my_feature() { - let out = compile_and_run("` | -| `src/parser/` | `parse()` | Tokens → `Program` (Vec of Stmt). Pratt parser for expressions | -| `src/magic_constants/`, `src/magic_constants.rs` | `substitute_file_and_scope_constants()` | Lowers PHP magic constants before resolver/name-resolver/optimizer passes | -| `src/conditional.rs` | `apply()` | Applies compiler `ifdef` conditional branches | -| `src/resolver/` | `resolve()` | Resolves `include`/`require`, discovers declarations, and tracks include-loaded function variants. Runs before namespace/name canonicalization | -| `src/name_resolver/` | `resolve()` | Applies namespace/use rules, rewrites references to canonical fully-qualified names, handles PHP-style builtin fallback, and flattens namespace-only AST nodes before type checking | -| `src/types/` | `check()` | Type checking, returns `CheckResult` with `TypeEnv`, function/class/interface/enum/FFI metadata, warnings, required libraries, and the internal `Mixed` type for heterogeneous assoc-array values | -| `src/optimize/`, `src/optimize.rs` | `fold_constants()`, `propagate_constants()`, `eliminate_dead_code()` | AST-level constant folding/propagation, control-flow pruning/normalization, DCE, and effect modeling | -| `src/ir/` | IR types / builders / validator | EIR program, function, block, instruction, terminator, value, local, effect, ownership, and textual-format definitions | -| `src/ir_lower/` | `lower_program()` | Active AST → EIR lowering, including local slot creation, hidden temporaries, ownership annotations, and PHP call semantics | -| `src/ir_passes/` | `optimize_module()` / `allocate_registers()` | EIR analyses and transformations over lowered functions: fixed-point optimization pass driver (identity folding, …; gated by `--ir-opt`) and linear-scan register allocation, before codegen | -| `src/codegen_ir/` | `generate()` | Active EIR → target assembly backend | -| `src/codegen/` | `generate()` / shared emitters | Frozen legacy direct AST backend plus shared ABI/runtime/target helpers still consumed by EIR | -| `src/codegen/abi/` | ABI helpers | Target-specific argument materialization, frame layout, registers, stack slots, symbols, and call helpers | -| `src/codegen/program_usage/` | Program scans | Collects codegen metadata such as required classes and variables before emission | -| `src/runtime_cache.rs` | `prepare_runtime_object()` | Builds/reuses the target runtime object before final linking | -| `src/errors/` | `report()` | Error formatting with line:col | -| `src/span.rs` | `Span` | Source position (line, col) attached to all AST nodes | - -### Codegen layout - -- `src/ir_lower/` is the active high-level lowering layer. Add PHP-visible semantics there, not in legacy direct AST emitters. -- `src/codegen_ir/lower_inst/` and `src/codegen_ir/lower_term.rs` are the active EIR instruction/terminator assembly lowerers. -- `src/codegen_ir/context.rs`, `src/codegen_ir/frame.rs`, and `src/codegen_ir/value_placement.rs` carry active backend state, frame layout, and value placement. -- `src/codegen/expr.rs`, `src/codegen/stmt.rs`, and their focused legacy helper modules are frozen direct AST backend dispatchers. Do not extend them for new features. -- `src/codegen/runtime/mod.rs` emits shared runtime code (`__rt_*` routines) -- `src/codegen/runtime/data.rs` emits shared runtime `.data` / `.bss` symbols and metadata tables -- `src/codegen/abi/` centralizes target-specific register, stack, frame, symbol, and call mechanics. Prefer these helpers over hardcoding ARM64 or x86_64 details in feature emitters. - -### Adding a new operator - -1. Add token to `src/lexer/token.rs` -2. Add scanning logic to `src/lexer/scan.rs` -3. Add `BinOp` variant to `src/parser/ast.rs` -4. Add one line to `infix_bp()` in `src/parser/expr/pratt.rs` (the Pratt parser binding power table) -5. Add type checking/inference in the relevant `src/types/checker/` file, usually under `inference/ops.rs` or expression inference -6. Add optimizer/effect handling when the operator can be folded, propagated, pruned, or has side effects -7. Add EIR lowering in the relevant `src/ir_lower/expr/` path and target-aware EIR codegen under `src/codegen_ir/lower_inst/` when the operator needs a new IR instruction or lowering path. Do not extend the frozen legacy direct AST emitter. -8. Add tests in all 4 test files - -### Adding a new statement type - -1. Add `StmtKind` variant to `src/parser/ast.rs` -2. Add parser logic in `src/parser/stmt.rs` -3. Add resolver/name-resolver handling if the statement can contain names, declarations, includes, function variants, or expressions -4. Add type checking in the relevant `src/types/checker/` module -5. Add optimizer/effects/warnings handling if the statement can be folded, pruned, read variables, write variables, or alter control flow -6. Add EIR lowering in `src/ir_lower/stmt/` and target-aware EIR codegen under `src/codegen_ir/` when the statement needs new instruction or terminator support. Do not extend the frozen legacy direct AST emitter. -7. If it introduces variables or hidden temporaries, update EIR local/temp declaration in `src/ir_lower/context.rs` and any frame-layout allocation needed before frame sizing -8. Add tests - -### Adding or changing an AST node - -When adding a new `ExprKind` or `StmtKind`, check every AST-walking pass. The compiler has many passes that deliberately recurse by variant, and missing one usually creates silent miscompilation rather than a compile error. - -Common places to audit: - -- Parser construction and lowering in `src/parser/` -- Resolver/include discovery and function-variant handling in `src/resolver/` -- Namespace/use/FQN rewriting in `src/name_resolver/` -- Type checking, inference, return analysis, warnings, and type compatibility in `src/types/` -- Optimizer folding, propagation, DCE, control-flow normalization, and effect modeling in `src/optimize/` -- Program usage scans in `src/codegen/program_usage/` -- Local/hidden-slot declaration in `src/ir_lower/context.rs` and frame placement in `src/codegen_ir/` -- Ownership metadata in `src/ir_lower/ownership.rs`, EIR ownership lowering in `src/codegen_ir/lower_inst/ownership.rs`, and related runtime/GC paths -- EIR lowering in `src/ir_lower/` plus EIR backend lowering in `src/codegen_ir/` -- Lexer/parser/codegen/error/regression tests, depending on the surface area - -### Adding a new built-in function - -1. Add the function to `src/types/checker/builtins/catalog.rs`. This is mandatory: it drives PHP-style case-insensitive builtin lookup, namespace fallback, redeclaration checks, and `name_resolver` behavior. -2. Confirm `function_exists("...")` recognizes the function. The implementation delegates to the canonical catalog; do not add a second builtin-name table without keeping it in lockstep. -3. Add or update the call signature in `src/types/signatures.rs`. This is the contract for named arguments: parameter names, default values, variadic name, by-ref/ref-like params, and arity must match PHP. Mark mutating parameters in `ref_params`; named-argument lowering and hidden-temp allocation depend on it. -4. Add type signature handling in the appropriate `src/types/checker/builtins/.rs` file (argument count, value types, return type, warnings, required Linux libraries). -5. Add first-class callable support in `first_class_callable_builtin_sig()` if the builtin should work through first-class callable syntax or callable aliases. -6. Add optimizer effect modeling in `src/optimize/effects/builtins.rs` when purity, reads, writes, or thrown/fatal behavior matters for DCE/constant propagation. -7. Add or update EIR call/lowering support in `src/ir_lower/expr/` when argument materialization, hidden temporaries, or ownership differ from ordinary calls. -8. Add target-aware EIR backend support in `src/codegen_ir/lower_inst/builtins/.rs` or the closest focused EIR lowering module. -9. If the function needs a runtime routine, create it under `src/codegen/runtime//`. -10. Add module/re-export wiring in the relevant `runtime//mod.rs`, then call it from the runtime emitter orchestration. -11. Leave the frozen legacy `src/codegen/builtins/` path untouched unless a narrow shared-API build fix is required. -12. Update docs and add codegen/error tests. New PHP-visible builtins should include at least one case-insensitive or namespaced call test when relevant. - -Do not stop after wiring only the checker and EIR backend dispatcher. A builtin is not complete until the catalog, `function_exists()`, case-insensitive lookup, and namespace fallback all see it. New builtins should include at least one case-insensitive or namespaced call test when the feature is PHP-visible. - -Leaf builtin/runtime files contain exactly **one emitter function**. Keep dispatcher/re-export files (`mod.rs`) as orchestration-only files, and keep runtime data emission in `src/codegen/runtime/data.rs`. - -Do not list every builtin in this guide. `src/types/checker/builtins/catalog.rs` and `src/types/signatures.rs` are the canonical sources; update those instead of maintaining parallel lists. - -### Adding a new EIR optimization pass - -IR-level transformations run after EIR lowering/validation through a fixed-point driver, not in the AST optimizer. - -1. Implement the `IrPass` trait (`name()`, `run(&mut Function, &mut DataPool) -> bool`) in a new `src/ir_passes/.rs`; `run` mutates the function in place and returns whether it changed anything. The `DataPool` is the module's shared literal pool for passes that intern new constants (e.g. peephole string-literal concat folding); ignore it (`_data`) otherwise. -2. Register the pass in `default_passes()` in `src/ir_passes/driver.rs`. Order matters: the driver re-runs the whole set per function until none reports a change, capped by `MAX_PASS_ITERATIONS`. -3. Reuse `src/ir_passes/rewrite.rs` for value redirection (`replace_all_uses` for RAUW) and the shared fold helpers (`resolve_chains`, `neutralize_to_nop`, `defining_instruction`, `count_value_uses`) instead of re-walking operands/terminators. Keep rewrites dominance-safe and PHP-equivalent; cross-check edge cases (division by zero, signed-zero/`NaN` floats) with `php -r`. -4. The driver re-validates each function with `validate_function` after every pass in debug/test builds and panics (naming the pass) on malformed IR or non-convergence; both guards compile out of `--release`. Rely on this during development. -5. Add unit tests under `src/ir_passes/tests/` (hand-built EIR via `crate::ir::Builder`) and end-to-end tests under `tests/codegen/optimizer/`. In e2e fixtures, use runtime-unknown values (e.g. `$argc`) so the targeted IR construct survives AST-level folding and actually reaches EIR. -6. Passes are gated by `--ir-opt=on|off` / `--no-ir-opt` (env `ELEPHC_IR_OPT`), default on. Behavior must be identical with the flag on or off except for performance; verify with `--emit-ir` and `--emit-ir --no-ir-opt`. - -### Call argument semantics - -All function-like call surfaces must share the same argument rules instead of normalizing locally in individual emitters: - -- Shared named/positional/spread semantics live in `src/types/call_args/` whenever they are not codegen-specific. -- `src/types/call_args/` owns the semantic planner (`CallArgPlan` / `plan_call_args`). The checker and EIR lowering should consume that plan; they should not rebuild named-argument matching, duplicate detection, static associative-spread expansion, spread bounds, or the regular/variadic split locally. -- If a codegen surface uses an internal signature with hidden parameters, such as closure captures, pass the caller-visible regular parameter count through `plan_call_args_with_regular_param_count()` instead of letting the planner infer it from the full internal signature. -- Type-checker validation and diagnostic mapping lives in `src/types/checker/functions/call_validation.rs`; it maps planner errors to `CompileError` diagnostics instead of owning the semantic rules. -- `src/ir_lower/expr/` owns active EIR call-argument lowering: planner consumption, source-order named/spread lowering, spread checks, and hidden-temp creation. `src/codegen_ir/` then materializes the lowered call through target-aware ABI helpers. The legacy `src/codegen/expr/calls/` path is frozen. -- User-defined calls, builtins, and extern calls must use the same named/spread normalization rules before any callee-specific lowering runs. -- PHP call unpacking with static string keys maps to named arguments (`f(...["a" => 1])` behaves like `f(a: 1)`). Static numeric keys remain positional, and duplicate static string keys inside one unpack use PHP's last-wins behavior before planning. -- When adding or extending a builtin, verify `first_class_callable_builtin_sig()` as well as the direct builtin signature so first-class callable syntax and callable aliases stay coherent. -- PHP source evaluation order is distinct from ABI parameter order. Preserve side effects in source order, then materialize arguments in parameter/ABI order; extern calls follow the same rule before C ABI register loading. -- Spread arguments before named arguments must be evaluated once, length/overwrite checks must happen at the PHP-observable point, and later named-argument side effects must not be skipped by early codegen checks. Too-short spreads for required parameters must fail instead of reading past the array payload. -- A positional spread into a variadic callee fills visible regular parameters first; only the remaining tail becomes the variadic array. -- User-defined variadics accept unknown named arguments as string-keyed variadic entries; internal/builtin variadics reject unknown named arguments like PHP internals. -- Ref-like parameters, including mutating builtin parameters, must avoid value-temp preevaluation so the original storage is passed/mutated. -- If hidden named-argument temporaries are introduced, update `src/ir_lower/context.rs` and EIR frame placement so slots are allocated before frame-size calculation. - -### Optimizer and effects - -The optimizer assumes side effects are modeled conservatively. When changing calls, operators, expressions, statements, or builtins: - -- Update `src/optimize/effects/` if purity, variable reads/writes, call effects, filesystem/runtime state, exceptions/fatals, or by-ref mutation behavior changes. -- Do not mark a call as pure if it can read or write globals, files, environment, runtime heap state, object properties, array contents, argument storage, or can emit visible output. -- Keep constant folding in `src/optimize/fold/` limited to PHP-equivalent results. If PHP behavior is edge-case sensitive, cross-check with `php -r`. -- Add optimizer regression tests under `tests/codegen/optimizer/` when DCE, constant propagation, control-flow pruning, or folding can observe the change. -- Magic constants must be lowered before optimizer passes. Do not introduce optimizer paths that expect raw `ExprKind::MagicConstant`. -- `src/optimize/` is the AST optimizer only. IR-level (EIR) transformations live in `src/ir_passes/` behind the fixed-point pass driver; see "Adding a new EIR optimization pass". Folds that need value identity, basic blocks, or dominance belong there, not in `src/optimize/`. - -### Runtime ownership, GC, and COW - -Refcounted runtime values are not plain scalars. When changing arrays, strings, objects, `Mixed`, `Iterable`, call returns, or temporaries: - -- Preserve the boxed `Mixed` cell contract: the runtime tag and payload shape must stay consistent across codegen and runtime helpers. -- Respect copy-on-write before mutating arrays or hashes. Use the existing ensure-unique helpers instead of mutating shared storage directly. -- Track whether a value is owned, borrowed, persistent, or a temporary result. Release only values this code path owns. -- Keep cleanup paths balanced across normal returns, early exits, throws/fatals, and control-flow merges. -- Add focused tests in `tests/codegen/runtime_gc/` for ownership, aliasing, cycles, heap debug, stack args, and COW changes. - -### File size policy - -As a general rule, aim to keep source files under **500 lines of code**. This is a maintainability guideline, not a blind numeric rule. - -The real goal is to avoid files that become hard to reason about because they mix multiple responsibilities. In practice: - -- **Dispatcher/orchestration files** (`mod.rs`, top-level drivers, large checker/codegen coordinators) should stay slim. If they grow large, split them aggressively. -- **Multi-responsibility files** should be split once they start accumulating unrelated concerns, even if the line count is not yet extreme. -- **Leaf files that implement one cohesive feature** are allowed to exceed 500 lines when splitting them would create artificial fragmentation. - -Examples of files that may reasonably stay above the soft limit: - -- a single runtime emitter implementing one substantial builtin or runtime routine -- a single compiler pass file that is still clearly about one feature and one code path -- a self-contained parser/lowering/runtime leaf where splitting would only spread one mental model across several tiny files - -Examples of files that should usually be split: - -- a file that mixes dispatch, validation, data collection, and post-processing -- a file that contains several unrelated builtins or runtime helpers -- a file that acts as a “miscellaneous bucket” for code that did not get a home - -So the policy is: - -- treat **500 LOC as a warning sign** -- treat **mixed responsibilities** as the real trigger for refactoring -- do **not** split a file that owns one coherent feature just to satisfy the number - -In short: prefer **cohesion over mechanical line-count compliance**. A 650-line mono-feature leaf is acceptable; a 350-line multi-purpose orchestrator is already a refactor candidate. - -### Rust module preamble policy - -Every repo-owned Rust source file (`*.rs`) must start with a module-level Rustdoc preamble before any `use`, `mod`, item, or test helper code. Use `//!` comments so the explanation is attached to the module in rustdoc. - -The preamble is mandatory for all new Rust files and must be added or preserved when touching existing Rust files. Release verification should report any Rust file that is missing it. - -Standard format: - -```rust -//! Purpose: -//! Explain what this file owns in 2-4 lines. -//! -//! Called from: -//! - `crate::path::caller()` or the relevant test/module entry point. -//! -//! Key details: -//! - Important invariants, ordering constraints, ownership/ABI/runtime rules, or coupling. -``` - -For test files, use the same structure but describe the test surface instead of production callers: - -```rust -//! Purpose: -//! Integration or regression tests for the relevant feature area. -//! -//! Called from: -//! - `cargo test` through Rust's test harness. -//! -//! Key details: -//! - Fixture layout, platform assumptions, ignored-test requirements, or why edge cases exist. -``` - -Keep preambles concise and factual. Do not include refactor history, stale line numbers, or broad architecture prose that belongs in `docs/internals/`. - -### Rust function docblock policy - -Every explicit Rust function in repo-owned Rust source files must have a concise docblock explaining what that function does. This applies equally to public functions, restricted-visibility functions (`pub(crate)`, `pub(super)`, etc.), private helper functions, impl methods, trait methods, and test functions. - -Use `///` Rustdoc comments immediately before the function item or its item attributes. Keep the docblock specific to the function's actual responsibility, inputs, outputs, side effects, ownership/ABI/runtime constraints, and failure behavior when those details matter. Do not use vague filler such as "handles logic" or "processes data". - -When documenting test functions, describe the behavior or regression being verified and any important fixture/platform assumptions. For explanatory comments inside a function body, use normal `//` comments, not `///`; Rustdoc comments inside function bodies produce warnings or errors because they do not document an item. - -Adding or updating function docblocks must not change code behavior. Do not alter function signatures, visibility, attributes, derives, module declarations, control-flow braces, strings, assembly instructions, or instruction-comment alignment while adding documentation. If a doc-only change causes `cargo check`, `cargo check --tests`, or `git diff --check` to fail, fix the documentation placement or comment style rather than changing code to fit the comment. - -### Codegen conventions (target-aware) - -- Prefer helpers from `src/codegen/abi/` for registers, stack slots, frame layout, argument materialization, symbol addresses, and calls. -- New feature emitters belong in `src/codegen_ir/`; the legacy direct AST emitters under `src/codegen/expr/`, `src/codegen/stmt/`, and `src/codegen/builtins/` are frozen. -- New feature emitters must support every supported target through `emitter.target` or clearly isolate target-specific code behind existing target helpers with explicit tests and diagnostics. -- Avoid hardcoding ARM64 register names, x86_64 register names, syscall numbers, object formats, or stack alignment rules in shared lowering code. -- Do not add an ARM64-only runtime helper, builtin emitter, ABI path, or ownership cleanup path unless the feature is intentionally target-gated and documented as unsupported elsewhere. -- Target-sensitive changes need coverage for every supported target they can affect. During local implementation, run focused target checks only when they are needed for confidence; rely on CI for the complete supported-target matrix unless the user requests local Docker runs or CI cannot provide the needed signal. - -### ARM64 quick reference - -- **Integers**: result in `x0` -- **Floats**: result in `d0` -- **Strings**: pointer in `x1`, length in `x2` -- **Function args**: `x0`-`x7` (int = 1 reg, string = 2 regs), `d0`-`d7` (floats) -- **Return value**: same as expression result (`x0`, `d0`, or `x1`/`x2`) -- **Stack frame**: `x29` = frame pointer, `x30` = link register, locals at negative offsets from `x29` -- **ABI helpers**: `src/codegen/abi/` centralizes load/store/write per type -- **Labels**: use `ctx.next_label("prefix")` — global counter prevents collisions across functions -- **Mixed values**: `PhpType::Mixed` is an internal boxed runtime shape used for heterogeneous associative-array values; codegen/runtime must preserve the boxed cell contract instead of treating it like a plain scalar - -### Assembly comment policy - -**Every `emitter.instruction(...)` call MUST have an inline `//` comment** explaining what the assembly instruction does. This is mandatory — the generated assembly is meant to be read, and every assembly line must be understandable by someone learning how compilers work. - -Rules: - -1. **Every instruction line gets a comment.** No exceptions. If you add a new `emitter.instruction(...)`, it must have a `// comment`. -2. **Alignment: `//` starts at column 81.** Pad with spaces so the `//` is at the 81st character position (1-indexed). If the code itself is >= 80 characters, add exactly one space before `//`. -3. **Block comments before related groups.** Use `// -- description --` on a standalone line before a block of related instructions (e.g., `// -- set up stack frame --`, `// -- copy bytes from source --`). -4. **Comments explain intent, not mnemonics.** Write "store argc from OS" not "store x0 to memory". The reader can see the instruction — explain *why* it's there. - -Example of correct formatting: - -```rust - // -- set up stack frame -- - emitter.instruction("sub sp, sp, #32"); // allocate 32 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #16"); // set new frame pointer - - // -- convert integer to string and write to stdout -- - emitter.instruction("bl __rt_itoa"); // convert x0 to decimal string → x1=ptr, x2=len - emitter.instruction("mov x0, #1"); // fd = stdout - emitter.instruction("mov x16, #4"); // syscall 4 = sys_write - emitter.instruction("svc #0x80"); // invoke macOS kernel -``` - -To verify alignment, run: -```bash -python3 -c " -with open('path/to/file.rs') as f: - for i, line in enumerate(f, 1): - if 'emitter.instruction' in line and '//' in line: - pos = line.rstrip().index('//') - if pos != 80 and len(line[:pos].rstrip()) < 80: - print(f'Line {i}: // at col {pos+1}') -" -``` - -## Examples - -Each example lives in `examples//main.php` with its own `.gitignore`. To run: - -```bash -cargo run -- examples/fizzbuzz/main.php -./examples/fizzbuzz/main -``` - -## PHP compatibility - -**PHP-derived syntax must be 100% compatible with PHP.** When elephc implements a PHP construct (variables, operators, keywords, built-ins), it must behave identically to PHP. This means: - -- Variable names, keywords, operators, and built-in function names must match PHP exactly -- Superglobals (`$argc`, `$argv`) must use PHP's syntax (e.g., `$argv[0]`, not `argv(0)`) -- Operator precedence and associativity must match PHP -- String escape sequences must match PHP behavior -- Built-in function signatures must match PHP (argument count, order, types) - -When in doubt, test with `php -r '...'` to verify behavior. - -**elephc also provides compiler-specific extensions** beyond standard PHP (e.g., `ptr`, `extern`, `buffer`, `packed class`). These features have no PHP equivalent and are not expected to run under the PHP interpreter. They are clearly distinguishable from PHP syntax and exist to enable use cases (FFI, game development, low-level memory access) that PHP cannot address. - -## Documentation - -The `docs/` directory is the project's complete documentation, organized into the following sections: - -``` -docs/ -├── README.md # Main index -├── getting-started/ # Installation and first program -│ ├── installation.md -│ └── your-first-program.md -├── compiling/ # The compiler CLI: flags and the full compilation process -│ ├── overview.md -│ ├── compilation-pipeline.md -│ ├── cli-reference.md -│ ├── targets.md -│ ├── optimization.md -│ ├── output-and-diagnostics.md -│ └── linking-and-conditional-compilation.md -├── php/ # PHP syntax (standard PHP features) -│ ├── types.md -│ ├── operators.md -│ ├── control-structures.md -│ ├── functions.md -│ ├── strings.md -│ ├── arrays.md -│ ├── math.md -│ ├── classes.md -│ ├── namespaces.md -│ ├── magic-constants.md -│ └── system-and-io.md -├── beyond-php/ # Compiler extensions (not valid PHP) -│ ├── pointers.md -│ ├── buffers.md -│ ├── packed-classes.md -│ ├── extern.md -│ └── ifdef.md -└── internals/ # Compiler internals - ├── what-is-a-compiler.md - ├── how-elephc-works.md - ├── the-lexer.md - ├── the-parser.md - ├── the-type-checker.md - ├── the-optimizer.md - ├── the-codegen.md - ├── the-runtime.md - ├── memory-model.md - ├── architecture.md - ├── arm64-assembly.md - └── arm64-instructions.md -``` - -### Astro compatibility - -All docs files are Markdown with YAML frontmatter compatible with Astro content collections. Every `.md` file **must** have this frontmatter format: - -```yaml ---- -title: "Page Title" -description: "One-line description of the page." -sidebar: - order: N ---- -``` - -- `title` replaces the `# Heading` — do **not** add a top-level `# Title` in the body (Astro renders it from frontmatter) -- `sidebar.order` controls page ordering within its section -- No navigation links (`[← Back]`, `Next:`, etc.) — Astro handles navigation -- Use standard Markdown (CommonMark). No custom shortcodes or Astro components inside docs - -### Keeping docs up to date - -**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. -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`. -5. If a feature was previously listed as "not supported", remove that note. -6. If there are known incompatibilities with PHP, document them in `docs/php/types.md` (incompatibilities section). -7. Update `docs/README.md` index if adding a new page. - -## Roadmap management - -`ROADMAP.md` tracks all planned and completed work, organized by version. - -- **Never remove completed items** from a version section. Mark them as `[x]` and leave them under the version they belong to. This preserves the history of what was delivered in each release. -- New work items go under the appropriate future version. -- When all items in a version are completed, the version is considered done — do not move items elsewhere. - -## Changelog management - -`CHANGELOG.md` records every released version, newest first, in *Keep a Changelog* style. - -When cutting a release: - -- Add a new section at the top (under the header), above the previous version: - - ``` - ## [X.Y.Z] - YYYY-MM-DD - - One terse, user-facing bullet per notable change. - ``` - - Keep entries concise (usually one or two bullets), describe what shipped — not the implementation — and use the absolute release date. -- Add a matching compare link at the **bottom** of the file, also newest first, immediately above the previous version's link: - - ``` - [X.Y.Z]: https://github.com/illegalstudio/elephc/compare/v...vX.Y.Z - ``` - - The first-ever release uses the `releases/tag/v0.1.0` form instead of a compare range. Every version section must have its link; do not leave the link out. -- Never change elephc's Cargo package version in `Cargo.toml` or `Cargo.lock`. Release automation in CI owns Cargo version bumps; agent changes should leave those files' version numbers untouched unless the user explicitly overrides this policy. - -## Conventions - -- No `Co-Authored-By` lines in commits -- Use commit message prefixes such as `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, or `test:` -- Keep commit messages concise -- Run the focused pre-commit verification above before committing code changes. Do not knowingly commit with relevant focused tests failing; the full suite must pass in CI. -- Zero compiler warnings policy (`cargo build` must be clean) -- Never run `cargo fmt` in this repo. Use targeted manual edits only; global formatting creates noisy churn here. +Refer to @AGENTS.md. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..fd338d3f72 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,560 @@ +# Contributing to Elephc + +First of all, thank you for considering contributing to Elephc! ❤️ + +Every contribution matters, whether it's fixing a typo, improving documentation, reporting a bug, or implementing a new feature. + +## Before You Start + +If you're planning to work on a significant feature or architectural change, please open an issue first so we can discuss the design before implementation. + +This helps avoid duplicated work and ensures that the proposed solution aligns with the project's long-term direction. + +## AI-Assisted Contributions + +Contributions created with the help of AI tools are welcome. What matters to us is the quality and correctness of the result, not how it was produced — so the usual expectations still apply: the code must build, be covered by tests, follow the surrounding style, and come with a clear description. Please review anything an AI helps you write as carefully as you would your own work, since you remain responsible for whatever you submit. + +## 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. + +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. + +Leave your plan in the repository until the work it describes is entirely finished. You don't have to land everything in one Pull Request: a plan may be split across several PRs, as long as the test suite stays green at every step. Once a plan is complete, the maintainers will clear it out during periodic cleanups, so there's no need to remove it yourself. + +## Reporting Bugs + +When reporting a bug, please include as much information as possible: + +* Operating system +* CPU architecture +* Elephc version +* Steps to reproduce +* Expected behavior +* Actual behavior +* Relevant code snippets or logs + +A minimal reproducible example is always appreciated. + +## Working locally + +Do your work on a dedicated branch — one branch per feature or fix — and prefer a +separate **git worktree** per branch so your changes stay isolated from `main` and +from any other work in progress. Worktrees let you keep several branches checked +out at once without stashing or switching back and forth in a single checkout. + +Name the branch with a short, descriptive slug behind a type prefix that mirrors +the commit-message prefixes used in this project: + +- `feat/` — a new feature +- `fix/` — a bug fix +- `docs/` — documentation only +- `refactor/` — internal restructuring with no behavior change +- `chore/` — tooling, CI, or housekeeping +- `test/` — tests only + +When the work tracks a GitHub issue, include the issue number, e.g. +`fix/369-tier2-range-analysis`. + +Worktrees can be managed by hand (`git worktree add`), but a small helper makes it +painless. We recommend [`ggw`](https://github.com/illegalstudio/ggw), which +creates, navigates, and pushes worktree-backed branches for you: + +```bash +ggw create feat/my-feature # create the branch and its worktree +ggw cd feat/my-feature # switch into the worktree +# ... implement your change ... +git commit -m "feat: add my feature" +ggw push # push the branch and set its upstream +``` + +`ggw push` is equivalent to `git push -u feat/my-feature` — use whichever +you prefer. Once the branch is pushed, open your Pull Request as described below. + +## Pull Requests + +Before opening a Pull Request, please ensure that: + +* Your code builds successfully. +* Existing tests continue to pass. +* New functionality includes tests whenever possible. +* Documentation is updated when appropriate. +* Commits are reasonably organized and have meaningful commit messages. +* When the Pull Request addresses an existing issue, reference it in the description. + +Please keep Pull Requests focused and self-contained. A Pull Request that solves a single, well-defined problem is far easier to review than a large one that bundles several unrelated changes together. + +### Draft until it's ready + +Open your Pull Request as a **draft** while you are still iterating, and keep it +in draft until you are confident the implementation is complete and correct. + +Once you switch it to **ready for review**, please leave the Pull Request +untouched — do not push further changes, and do not rebase or merge `main` into it +to keep it aligned. From that point on the maintainers take over the Pull Request +and will handle reviewing, updating, and integrating it. + +## Coding Style + +Try to follow the style already used throughout the codebase. + +Consistency is generally more important than personal preference. + +### Assembly comment alignment + +The assembly elephc emits is meant to be read and understood by someone learning +how compilers work, so **every `emitter.instruction(...)` call must carry an inline +`//` comment** explaining what the instruction does — and those comments are aligned +to a fixed column. A few rules keep them consistent: + +1. **Every instruction line gets a comment.** No exceptions: if you add an + `emitter.instruction(...)`, it gets a `// comment`. +2. **The `//` starts at column 81.** Pad the line with spaces so the `//` sits at the + 81st character (1-indexed). If the code itself already reaches 80 characters or + more, put exactly one space before the `//`. +3. **Group related instructions under a block comment.** Put a standalone + `// -- description --` line before a block of related instructions (e.g. + `// -- set up stack frame --`). +4. **Explain intent, not the mnemonic.** Write "store argc from OS", not "store x0 to + memory" — the reader can already see the instruction; the comment should say *why* + it's there. + +For example: + +```rust + // -- set up stack frame -- + emitter.instruction("sub sp, sp, #32"); // allocate 32 bytes on the stack + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // set new frame pointer +``` + +To verify alignment, run `./scripts/check_asm_comments.py` against any codegen file you +touch before opening a Pull Request. It flags every `emitter.instruction(...)` whose +`//` comment is misaligned and exits non-zero if it finds any, so it also works in a +pre-commit hook or CI: + +```bash +./scripts/check_asm_comments.py src/codegen/lower_inst/your_file.rs +``` + +It accepts multiple files at once, e.g. `./scripts/check_asm_comments.py src/codegen/lower_inst/*.rs`. + +## Adding a new operator + +elephc parses expressions with a Pratt parser, so a new binary operator flows +through the whole pipeline — lexer, parser, type checker, optimizer, EIR +lowering, and target-aware codegen. Implement it end-to-end: + +1. Add the token to `src/lexer/token.rs`. +2. Add scanning logic to `src/lexer/scan.rs`. +3. Add the `BinOp` variant to `src/parser/ast.rs`. +4. Add one line to `infix_bp()` in `src/parser/expr/pratt.rs` (the Pratt parser + binding-power table) so precedence and associativity match PHP. +5. Add type checking/inference in the relevant `src/types/checker/` file, usually + under `inference/ops.rs` or expression inference. +6. Add optimizer/effect handling when the operator can be folded, propagated, + pruned, or has side effects (`src/optimize/`). Keep folds PHP-equivalent — + cross-check edge cases with `php -r`. +7. Add EIR lowering in the relevant `src/ir_lower/expr/` path and target-aware EIR + codegen under `src/codegen/lower_inst/` when the operator needs a new IR + instruction or lowering path. +8. Add tests in all four test files (lexer, parser, codegen, error), including a + Pratt binding-power test that asserts precedence relative to adjacent operators. + +## Adding a new statement type + +A new statement kind must be threaded through parsing, the frontend passes, the +type checker, the optimizer, and EIR lowering. Missing a pass usually produces +silent miscompilation rather than a compile error, so also audit every +AST-walking pass (see "Adding or changing an AST node" in `CLAUDE.md`): + +1. Add the `StmtKind` variant to `src/parser/ast.rs`. +2. Add parser logic in `src/parser/stmt.rs`. +3. Add resolver/name-resolver handling if the statement can contain names, + declarations, includes, function variants, or expressions. +4. Add type checking in the relevant `src/types/checker/` module. +5. Add optimizer/effects/warnings handling if the statement can be folded, pruned, + read variables, write variables, or alter control flow (`src/optimize/`). +6. Add EIR lowering in `src/ir_lower/stmt/` and target-aware EIR codegen under + `src/codegen/` when the statement needs new instruction or terminator + support. +7. If it introduces variables or hidden temporaries, update EIR local/temp + declaration in `src/ir_lower/context.rs` and any frame-layout allocation needed + before frame sizing. +8. Add tests: at least one codegen test showing correct output, one for edge cases + (empty body, nested), and one error test for malformed syntax. + +## Adding a new EIR optimization pass + +IR-level (EIR) transformations run after EIR lowering/validation through a +fixed-point driver, not in the AST optimizer (`src/optimize/`). Folds that need +value identity, basic blocks, or dominance belong here. + +1. Implement the `IrPass` trait (`name()`, `run(&mut Function, &mut DataPool) -> + bool`) in a new `src/ir_passes/.rs`; `run` mutates the function in place + and returns whether it changed anything. The `DataPool` is the module's shared + literal pool for passes that intern new constants (e.g. peephole string-literal + concat folding); ignore it (`_data`) otherwise. +2. Register the pass in `default_passes()` in `src/ir_passes/driver.rs`. Order + matters: the driver re-runs the whole set per function until none reports a + change, capped by `MAX_PASS_ITERATIONS`. +3. Reuse `src/ir_passes/rewrite.rs` for value redirection (`replace_all_uses` for + RAUW) and the shared fold helpers (`resolve_chains`, `neutralize_to_nop`, + `defining_instruction`, `count_value_uses`) instead of re-walking + operands/terminators. Keep rewrites dominance-safe and PHP-equivalent; + cross-check edge cases (division by zero, signed-zero/`NaN` floats) with `php -r`. +4. The driver re-validates each function with `validate_function` after every pass + in debug/test builds and panics (naming the pass) on malformed IR or + non-convergence; both guards compile out of `--release`. Rely on this during + development. +5. Add unit tests under `src/ir_passes/tests/` (hand-built EIR via + `crate::ir::Builder`) and end-to-end tests under `tests/codegen/optimizer/`. In + e2e fixtures, use runtime-unknown values (e.g. `$argc`) so the targeted IR + construct survives AST-level folding and actually reaches EIR. +6. Passes are gated by `--ir-opt=on|off` / `--no-ir-opt` (env `ELEPHC_IR_OPT`), + default on. Behavior must be identical with the flag on or off except for + performance; verify with `--emit-ir` and `--emit-ir --no-ir-opt`. + +## Adding a built-in function + +elephc's PHP built-in functions are declared **once** in a single-source registry. +Each builtin has one *home file* at `src/builtins//.rs` that declares it +with the `builtin!` macro; all declarations are collected at link time through the +`inventory` crate. From that single declaration the compiler derives the catalog +name-set (case-insensitive lookup, `function_exists`, namespace fallback, +redeclaration checks), the call signature (named arguments, defaults, by-ref params, +variadic, arity), the type-check entry, the EIR lowering dispatch, and the generated +documentation. + +Do **not** re-add builtin names to the old hand-maintained tables (`catalog.rs`, +`signatures.rs`, per-area `check_builtin` arms). They are superseded by the registry; +a builtin is fully wired the moment its home file compiles. + +### 1. Create the home file + +Add `src/builtins//.rs` and register it in `src/builtins//mod.rs` +with `pub mod ;` (keep the list alphabetical). Areas are `string`, `array`, +`math`, `io`, `system`, `types`, `callables`, `spl`, `pointers` (plus `internal` for +compiler-internal builtins). One builtin per home file; the file owns its declaration +plus its `check`/`lower` hooks. Start with the mandatory `//!` module preamble. + +### 2. Declare it with `builtin!` + +```rust +builtin! { + name: "strlen", + area: String, + params: [string: Str], + returns: Int, + check: check, + lazy_check: true, + lower: lower, + summary: "Returns the length of a string.", + php_manual: "function.strlen", +} +``` + +Fields must appear in this canonical order; optional fields (marked `?`) may be +omitted: + +`name`, `area`, `params`, `variadic?`, `min_args?`, `max_args?`, `arity_error?`, +`returns`, `by_ref_return?`, `check?`, `lazy_check?`, `lower`, `summary`, `examples?`, +`php_manual?`, `deprecation?`, `internal?`. + +- **`params`** — `[name: TypeSpec, name: TypeSpec = DefaultSpec::Variant, ...]`. A + parameter with `= DefaultSpec::…` is optional; without it, required. Prefix a + parameter with `ref` to pass it by reference (mutating builtins): + `params: [ref array: Mixed, offset: Int]`. Parameter names become PHP's + named-argument keys and must match PHP exactly (Rust keywords work as names via raw + identifiers, e.g. `r#type`). +- **`returns` and param `TypeSpec`** — written as a bare scalar type ident: `Int`, + `Float`, `Str`, `Bool`, `Mixed`, `Null`, `Void`. Non-scalar shapes (arrays, unions, + resources) are declared as `Mixed`; supply the precise type from a `check` hook when + it matters (see the note in step 3). +- **`DefaultSpec`** — full path form: `DefaultSpec::Null`, `DefaultSpec::Int(0)`, + `DefaultSpec::Bool(false)`, `DefaultSpec::Float(1.5)`, `DefaultSpec::Str("…")`, + `DefaultSpec::IntMax`, `DefaultSpec::IntMin`, `DefaultSpec::EmptyArray`. +- **`variadic`** — the PHP name of the trailing variadic parameter, e.g. + `variadic: "values"`. +- **`min_args` / `max_args` / `arity_error`** — override only the arity check (not the + derived signature or the parity gate). Use when a builtin's PHP arity is + tighter/looser than its declared parameter list, or needs a verbatim error message. +- **`summary` / `examples` / `php_manual` / `deprecation`** — documentation metadata + surfaced by the `gen_builtins` exporter. +- **`internal: true`** — a compiler-internal builtin that is not PHP-visible and is + excluded from catalogs and docs. + +A builtin whose return type does not depend on its arguments and needs no extra +validation can omit `check` entirely — `returns:` is then authoritative for the +checker. + +### 3. The `check` hook (type checking) + +Add a `check` hook when the return type depends on argument types/values, or when the +call needs validation beyond arity and the parameter list: + +```rust +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Str | PhpType::Mixed | PhpType::Union(_)) { + return Err(CompileError::new(cx.span, "strlen() argument must be string")); + } + Ok(PhpType::Int) +} +``` + +The hook receives `BuiltinCheckCtx { checker, name, args, span, env }` and returns the +call's `PhpType` (or a diagnostic). Its returned type overrides `returns:` for the +checker. + +For a normal builtin the registry already checks arity and infers every argument once +(for side effects such as variable narrowing and undefined-variable diagnostics) +before calling the hook. Set **`lazy_check: true`** when the hook must control +inference order — most importantly when it injects element/parameter type hints into +an unannotated closure argument *before* that closure is inferred (e.g. `usort`, +`array_map` with a callback). With `lazy_check: true` the hook is responsible for +inferring each argument itself. + +> **Return typing is a checker-only contract.** The `returns:` field and the `check` +> hook drive the **type checker** only. The EIR backend derives call return types +> independently in `call_return_type` (`src/ir_lower/expr/mod.rs`). If you declare +> `returns: Mixed` + a precise `check` hook (the standard pattern for non-scalar +> returns), you must also add a matching arm to the EIR return-type derivation, or the +> checker and EIR will disagree on the value's type. This caveat is documented on the +> `returns`/`check` fields in `src/builtins/spec.rs`. + +### 4. The `lower` hook (EIR codegen) + +`lower` is mandatory — it is the builtin's EIR lowering entry point. Keep it a thin +wrapper that dispatches to the actual emitter: + +```rust +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_strlen(ctx, inst) +} +``` + +Write the emitter itself under `src/codegen/lower_inst/builtins//`, following +the target-aware codegen conventions in `CLAUDE.md` (support every target through +`emitter.target`, one emitter per leaf file, an inline `//` comment on every +`emitter.instruction(...)`). If the builtin needs a runtime routine, add it under +`src/codegen_support/runtime//`. The registry dispatches `spec.lower` first, so no +match arm needs editing. + +### 5. What derives automatically + +Once the home file compiles, all of the following see the builtin with no further +edits: `function_exists()` and case-insensitive/namespaced lookup, the named-argument +`FunctionSig`, first-class-callable syntax (`strlen(...)`), the arity check and its +error message, and the `gen_builtins` JSON docs export. + +### 6. Surfaces you still wire by hand + +The registry single-sources the declaration, signature, checker entry, lowering +*dispatch*, and docs. These related surfaces are **not** derived and must be updated +when relevant: + +- **The EIR emitter** the `lower` hook calls (and any runtime routine it needs). +- **EIR return typing** — see the note in step 3. +- **Optimizer effects** in `src/optimize/effects/builtins.rs` when purity, reads/writes, + or thrown/fatal behavior matter for DCE and constant propagation. Never mark a call + pure if it can read/write globals, files, the environment, heap state, or emit output. +- **Runtime-callable wrapper exclusion** — if the builtin cannot be dispatched through + the dynamic string-callable wrapper, add it to `runtime_builtin_wrapper_excluded()` + in `src/codegen/callable_dispatch.rs`. + +### 7. Tests, examples, and docs + +- Add codegen tests for normal use (plus at least one case-insensitive or namespaced + call for a PHP-visible builtin), and error tests for wrong argument count/types. +- Add or update an example under `examples/` when the builtin is a notable user-facing + feature. +- Document the PHP surface (signature, parameters, return type, a short example) on the + relevant `docs/php/` page. +- The signature/arity parity gates in `src/builtins/parity_tests.rs` must stay green. + +### 8. Not every "builtin" is a function + +A small set of PHP language constructs — `isset`, `unset`, `empty`, `exit`, `die`, plus +the `buffer_*` intrinsics — are l-value/lazy constructs with dedicated EIR paths and are +intentionally kept in the checker (`numeric`/`arrays` `check_builtin`), not in the +registry. Do not migrate those into `builtin!`. + +## Adding functionality via a Rust crate (bridge crates) + +elephc compiles a static subset of PHP straight to native code, so most features +are implemented directly in the compiler (lexer → parser → type checker → EIR → +codegen). But some functionality is heavy, well-served by an existing Rust +library, or simply not worth re-implementing by hand — TLS, PDO database drivers, +image codecs, hashing, timezone tables, Phar archives, the `--web` server. + +**If the functionality you want to add can be realized through Rust libraries, +implement it as a bridge crate and register a `--with-` flag** instead of +hand-writing it in the runtime. A *bridge crate* is a `staticlib` under +`crates/elephc-/` that elephc links into compiled PHP programs on demand. +The whole linking model is table-driven from the `BRIDGES` table in +`src/linker.rs`, so wiring a new bridge is a single table entry plus the PHP-facing +surface. + +Follow these technical specifications in full. + +### 1. Decide it belongs in a crate + +Use a bridge crate when the feature (a) maps cleanly onto a maintained Rust crate, +(b) is optional (programs that do not use it must not pay for it), and (c) exposes +a small, stable C ABI surface. Do **not** use a crate for core language semantics, +ownership/GC, or anything that must be understood line-by-line in the generated +assembly — that belongs in the compiler proper (`AGENTS.md`/`CLAUDE.md`). + +### 2. Create the crate + +Create `crates/elephc-/` as a workspace member: + +```toml +# crates/elephc-/Cargo.toml +[package] +name = "elephc-" +version = "0.1.0" +edition = "2021" +license = "MIT" +publish = false + +# staticlib: linked into compiled PHP programs. rlib: lets the bridge be +# unit-tested via `cargo test -p elephc-`. +[lib] +crate-type = ["staticlib", "rlib"] + +[dependencies] +# Prefer pure-Rust, musl-friendly crates so the Linux Docker test images link. +``` + +Add the crate to **both** `members` and `default-members` in the root +`Cargo.toml`, and an entry under `[workspace.dependencies]`. Being a default +member is what makes a plain `cargo build` materialize `target//libelephc_.a`. + +Expose a **stable C ABI**: every entry point is `#[no_mangle] pub extern "C"` and +must be panic-free across the boundary (catch/encode errors, return error codes or +null; never unwind into generated code). Keep the surface small and explicit — +pass pointers + lengths for strings/buffers, return primitive status values. Name +exports `elephc__*` so they are easy to find and namespace-clean. + +Every supported target must build and link the crate: `macos-aarch64`, +`linux-aarch64`, `linux-x86_64`. A bridge that only works on one target is not +acceptable (see the supported-target policy in `CLAUDE.md`). + +### 3. Register the bridge in `BRIDGES` (`src/linker.rs`) + +Add one `BridgeStaticlib` entry. This is the only linker change required — +discovery, on-demand build, search paths, whole-archiving, and macOS frameworks +are all driven from the table: + +```rust +BridgeStaticlib { + lib_name: "elephc_", // `-l` name → links lib.a + env_var: "ELEPHC__LIB_DIR", // dir override for prebuilt staticlibs + crate_name: "elephc-", // cargo package (auto-build + workspace) + flag_name: "", // user-facing `--with-` flag + whole_archive: false, // true if link-time side effects / owns entry + macos_frameworks: &[], // transitive native deps' frameworks + needs_libdl: true, // Rust runtime/unwinder needs -ldl on Linux +}, +``` + +Set `whole_archive: true` only when the staticlib has link-time side effects that +must survive (e.g. a provider registration) or owns the program entry point (like +`elephc_web`). Otherwise leave it `false`; `--with-` force-loads it anyway. + +### 4. Expose the PHP-visible surface + +Pick one of two paths so PHP code can actually call into the crate. Both make the +type checker record `elephc_` as a required library, which is what links the +bridge automatically when the feature is used. + +- **Core builtins** — when the feature is a set of PHP built-in functions + (`md5()`, `hash()`, …). Follow "Adding a built-in function" above (declare each + builtin in `src/builtins//` with its `check`/`lower` hooks), and call + `Checker::require_builtin_library("elephc_")` from the `check` hook when a + builtin that needs the crate is used. The PHP names are always available, so no + prelude is needed. + +- **A prelude** — when the feature is a set of classes/functions written in + elephc-PHP that wrap the crate (PDO, timezone introspection, image). Add + `src/_prelude.rs`: + - a static elephc-PHP source string declaring `extern "elephc_" { ... }` + plus the wrapper classes/functions; + - `pub fn inject_if_used(program: Program, force: bool) -> Program` that returns + `program` unchanged when `!force && !detect::program_uses_(&program)`, + and otherwise tokenizes/parses the prelude and prepends it (declarations are + hoisted, so prepending does not change execution order); + - a `detect` submodule that scans the AST for the feature's symbols. + Wire the call into `src/pipeline.rs` after include resolution, mirroring the PDO + block. The injected `extern "elephc_"` block is what adds the bridge to + `required_libraries`, so usage auto-links it. + +### 5. Register the `--with-` flag + +Registering the bridge in `BRIDGES` with a `flag_name` is what enables the flag — +`src/cli.rs` parses `--with-` generically against the table (an unknown +crate is rejected, listing the valid ones), and `src/pipeline.rs` force-links the +bridge (whole-archived via `forced_bridge_libs`) for every crate named in +`with_crates`. + +If your crate uses a **prelude**, also thread the force flag into its +`inject_if_used` call in `src/pipeline.rs`, mirroring pdo/tz/image: + +```rust +let ast = _prelude::inject_if_used(ast, with_crates.contains("")); +``` + +so `--with-` declares the PHP surface even when auto-detection would not. +Core-builtin crates need nothing extra here — force-linking the staticlib is +enough because their PHP names are always available. + +`--with-` semantics: it guarantees the crate is compiled in (whole-archived, +not dead-stripped) and, for prelude crates, that its API is declared — useful when +detection cannot see indirect usage. It is additive and never disables +auto-detection. Note that it increases binary size by force-including the whole +crate. + +### 6. Tests + +- Unit-test the `BRIDGES`/flag mapping in `src/linker.rs` and the CLI parsing in + `src/cli.rs` (both are fast, no assembler/linker). +- Add codegen/end-to-end tests that exercise the feature, and error tests for + argument-count/usage diagnostics, per the test-coverage rules in `CLAUDE.md`. +- The crate itself should have `cargo test -p elephc-` unit tests (that is + what the `rlib` crate-type is for). +- Run focused tests locally; CI runs the full supported-target matrix. + +### 7. Documentation + +- `docs/compiling/cli-reference.md` — add the `--with-` flag. +- `docs/compiling/linking-and-conditional-compilation.md` — describe the bridge + and its auto-link trigger. +- The relevant `docs/php/` or `docs/beyond-php/` page — document the PHP surface. +- Update `CLAUDE.md` only if you changed the bridge/flag mechanism itself. + +## Contributor Certification + +By submitting a contribution to this repository, you represent and warrant that: + +* you have the legal right to submit the contribution; +* the contribution is your original work, or you have sufficient rights to submit it; +* to the best of your knowledge, the contribution does not knowingly infringe the intellectual property rights of any third party; +* you agree that your contribution will be distributed under the same license as the Elephc project. + +You retain the copyright to your contributions. + +## Code of Conduct + +Please be respectful and constructive. + +Healthy technical discussions are encouraged. Personal attacks, harassment, or disrespectful behavior will not be tolerated. + +## Questions + +If you're unsure about anything, feel free to open an issue or start a discussion. + +Contributions of all sizes are welcome. + +Happy hacking! 🐘 diff --git a/Cargo.lock b/Cargo.lock index f120da1c41..6718c4a830 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bindgen" version = "0.72.1" @@ -355,6 +361,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.2" @@ -493,6 +505,17 @@ dependencies = [ "cmov", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "derive_utils" version = "0.15.1" @@ -511,6 +534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.6", "crypto-common 0.1.7", ] @@ -521,7 +545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", - "const-oid", + "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", ] @@ -545,7 +569,7 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elephc" -version = "0.25.2" +version = "0.26.0" dependencies = [ "bitflags 2.13.0", "bzip2-rs", @@ -557,6 +581,7 @@ dependencies = [ "elephc-tz", "elephc-web", "flate2", + "inventory", "rustls", "rustls-pemfile", "serde_json", @@ -610,7 +635,10 @@ dependencies = [ "bzip2", "bzip2-rs", "flate2", + "md-5 0.10.6", + "rsa", "sha1", + "sha2 0.10.9", ] [[package]] @@ -1111,6 +1139,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "io-enum" version = "1.2.1" @@ -1179,6 +1216,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -1208,6 +1248,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.17" @@ -1431,6 +1477,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1440,6 +1502,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1447,6 +1520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1506,6 +1580,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1537,6 +1620,27 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -1867,6 +1971,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha1", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust_decimal" version = "1.42.0" @@ -2062,6 +2188,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -2112,6 +2248,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 217bdfc475..95c054243f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "elephc" -version = "0.25.2" +version = "0.26.0" edition = "2021" +default-run = "elephc" description = "A PHP-to-native AOT compiler" license = "MIT" repository = "https://github.com/illegalstudio/elephc" @@ -27,7 +28,12 @@ resolver = "2" name = "elephc" path = "src/main.rs" +[[bin]] +name = "gen_builtins" +path = "src/bin/gen_builtins.rs" + [dependencies] +inventory = "0.3" bitflags = "2" serde_json = "1" # flate2 (default miniz_oxide backend = pure Rust, no system zlib) is used by the diff --git a/README.md b/README.md index 9b7ed97e88..f297ccf0b5 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@

- 3 native targets · no Zend Engine · zero runtime dependencies · single standalone binary + 4 native targets · no Zend Engine · zero runtime dependencies · 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, plus an experimental Windows x86_64 cross-compilation target. No opcode fallback, just real machine code.

@@ -42,6 +42,18 @@ elephc is built and maintained independently. You can support the project by eit - ⭐ **[Starring the repo](https://github.com/illegalstudio/elephc/stargazers)** — it helps others discover it and keeps the project going. - 💜 **[Sponsoring on GitHub](https://github.com/sponsors/nahime0)** — every contribution, big or small, makes a real difference. +## Core Contributors + +

+ Vincenzo Petrucci +  Vincenzo Petrucci +

+ +

+ Guillaume Loulier +  Guillaume Loulier +

+ ## An async HTTP server in PHP An asynchronous HTTP/1.1 server — a non-blocking `poll()` event loop, one Fiber per connection, raw TCP sockets through `extern` FFI, plus an HTTP parser and a router — written entirely in PHP and compiled to a single native binary. No interpreter, no PHP-FPM, no Nginx. @@ -101,7 +113,7 @@ elephc takes a narrower but cleaner route: it is a from-scratch compiler for a s That tradeoff is intentional: -- **Less legacy compatibility** than a VM-backed PHP implementation. +- **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. - **Native-oriented extensions**: `extern`, `ptr`, `buffer`, and `packed class` let PHP-shaped code cross into systems, FFI, game, and performance-sensitive workloads. @@ -145,11 +157,8 @@ xattr -cr elephc ## Usage -> **Important:** Starting with v0.23.10, elephc uses the new EIR backend by default. -> The legacy AST backend is frozen: it will not receive new language or runtime -> features, and it is scheduled for complete removal in v0.26.0. If you need to -> compare behavior with the old backend during the transition, compile with -> `--ast-backend`. +> **Important:** elephc lowers every build through the EIR pipeline and the +> target-aware assembly emitter. ```bash # Compile a PHP file to a native binary @@ -186,10 +195,14 @@ 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 +elephc app.php --with-pdo --with-crypto + # Explicit target selection -# Supported targets today: macos-aarch64, linux-aarch64, linux-x86_64 +# Supported targets today: macos-aarch64, linux-aarch64, linux-x86_64, windows-x86_64 (experimental) elephc --target linux-aarch64 hello.php elephc --target linux-x86_64 hello.php +elephc --target windows-x86_64 hello.php # experimental cross-compilation, requires MinGW-w64 # Compile a standalone prefork HTTP server binary elephc --web app.php @@ -320,26 +333,26 @@ The full list of supported constructs, operators, and control structures is in t -### Built-in functions (380+) +### Built-in functions (420+) -Over 380 PHP built-ins are implemented natively, grouped here by category — strings, arrays, math, I/O, streams/sockets, system, and more. +Over 420 PHP built-ins are implemented natively, grouped here by category — strings, arrays, math, I/O, streams/sockets, system, and more.
Show all built-in functions by category **Strings:** `strlen`, `substr`, `strpos`, `strrpos`, `strstr`, `str_replace`, `str_ireplace`, `substr_replace`, `strtolower`, `strtoupper`, `ucfirst`, `lcfirst`, `ucwords`, `trim`, `ltrim`, `rtrim`, `str_repeat`, `str_pad`, `strrev`, `chop`, `grapheme_strrev`, `str_split`, `strcmp`, `strcasecmp`, `str_contains`, `str_starts_with`, `str_ends_with`, `ord`, `chr`, `explode`, `implode`, `sprintf`, `printf`, `vprintf`, `vsprintf`, `sscanf`, `md5`, `sha1`, `hash`, `hash_algos`, `hash_equals`, `hash_hmac`, `hash_init`, `hash_update`, `hash_final`, `hash_copy`, `crc32`, `number_format`, `addslashes`, `stripslashes`, `nl2br`, `wordwrap`, `bin2hex`, `hex2bin`, `htmlspecialchars`, `htmlentities`, `html_entity_decode`, `urlencode`, `urldecode`, `rawurlencode`, `rawurldecode`, `base64_encode`, `base64_decode`, `gzcompress`, `gzdeflate`, `gzinflate`, `gzuncompress`, `ip2long`, `long2ip`, `inet_ntop`, `inet_pton`, `ctype_alpha`, `ctype_digit`, `ctype_alnum`, `ctype_space` -**Arrays:** `count`, `array_push`, `array_pop`, `in_array`, `array_keys`, `array_values`, `sort`, `rsort`, `isset`, `array_key_exists`, `array_search`, `array_merge`, `array_slice`, `array_splice`, `array_combine`, `array_flip`, `array_reverse`, `array_unique`, `array_sum`, `array_product`, `array_chunk`, `array_pad`, `array_fill`, `array_fill_keys`, `array_diff`, `array_intersect`, `array_diff_key`, `array_intersect_key`, `array_unshift`, `array_shift`, `asort`, `arsort`, `ksort`, `krsort`, `natsort`, `natcasesort`, `shuffle`, `array_rand`, `array_column`, `range`, `array_map`, `array_filter`, `array_reduce`, `array_walk`, `usort`, `uksort`, `uasort`, `call_user_func`, `call_user_func_array`, `function_exists` +**Arrays:** `count`, `array_push`, `array_pop`, `in_array`, `array_keys`, `array_values`, `sort`, `rsort`, `isset`, `array_key_exists`, `array_search`, `array_merge`, `array_slice`, `array_splice`, `array_combine`, `array_flip`, `array_reverse`, `array_unique`, `array_sum`, `array_product`, `array_chunk`, `array_pad`, `array_fill`, `array_fill_keys`, `array_diff`, `array_intersect`, `array_diff_key`, `array_intersect_key`, `array_unshift`, `array_shift`, `asort`, `arsort`, `ksort`, `krsort`, `natsort`, `natcasesort`, `shuffle`, `array_rand`, `array_column`, `range`, `array_map`, `array_filter`, `array_reduce`, `array_walk`, `array_walk_recursive`, `array_is_list`, `array_key_first`, `array_key_last`, `array_replace`, `array_replace_recursive`, `array_merge_recursive`, `array_diff_assoc`, `array_intersect_assoc`, `array_udiff`, `array_uintersect`, `array_find`, `array_any`, `array_all`, `array_multisort`, `usort`, `uksort`, `uasort`, `call_user_func`, `call_user_func_array`, `function_exists` **Math:** `abs`, `floor`, `ceil`, `round`, `sqrt`, `pow`, `min`, `max`, `clamp`, `intdiv`, `fmod`, `fdiv`, `rand`, `mt_rand`, `random_int`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sinh`, `cosh`, `tanh`, `log`, `log2`, `log10`, `exp`, `hypot`, `deg2rad`, `rad2deg`, `pi` -**Types and class introspection:** `gettype`, `settype`, `empty`, `unset`, `is_int`, `is_float`, `is_string`, `is_bool`, `is_null`, `is_numeric`, `is_nan`, `is_finite`, `is_infinite`, `is_iterable`, `is_callable`, `is_resource`, `boolval`, `floatval`, `intval`, `get_resource_type`, `get_resource_id`, `class_exists`, `interface_exists`, `trait_exists`, `enum_exists`, `class_alias`, `get_class`, `get_parent_class`, `get_declared_classes`, `get_declared_interfaces`, `get_declared_traits`, `is_a`, `is_subclass_of`, `class_implements`, `class_parents`, `class_uses` +**Types and class introspection:** `gettype`, `settype`, `empty`, `unset`, `is_int`, `is_float`, `is_string`, `is_bool`, `is_null`, `is_numeric`, `is_nan`, `is_finite`, `is_infinite`, `is_iterable`, `is_callable`, `is_resource`, `is_array`, `is_object`, `is_scalar`, `boolval`, `floatval`, `intval`, `get_resource_type`, `get_resource_id`, `class_exists`, `interface_exists`, `trait_exists`, `enum_exists`, `class_alias`, `get_class`, `get_parent_class`, `get_declared_classes`, `get_declared_interfaces`, `get_declared_traits`, `is_a`, `is_subclass_of`, `class_implements`, `class_parents`, `class_uses` -**I/O:** `fopen`, `fclose`, `fread`, `fwrite`, `fprintf`, `vfprintf`, `fscanf`, `fgets`, `fgetc`, `fpassthru`, `flock`, `tmpfile`, `readfile`, `feof`, `readline`, `fseek`, `ftell`, `rewind`, `file_get_contents`, `file_put_contents`, `file`, `hash_file`, `fgetcsv`, `fputcsv`, `file_exists`, `is_file`, `is_dir`, `is_readable`, `is_writable`, `is_writeable`, `is_executable`, `is_link`, `symlink`, `link`, `readlink`, `linkinfo`, `filesize`, `filemtime`, `fileatime`, `filectime`, `fileperms`, `fileowner`, `filegroup`, `fileinode`, `filetype`, `stat`, `lstat`, `fstat`, `clearstatcache`, `disk_free_space`, `disk_total_space`, `basename`, `dirname`, `pathinfo`, `realpath`, `fnmatch`, `touch`, `chmod`, `chown`, `chgrp`, `lchown`, `lchgrp`, `umask`, `ftruncate`, `fflush`, `fsync`, `fdatasync`, `copy`, `rename`, `unlink`, `mkdir`, `rmdir`, `opendir`, `readdir`, `rewinddir`, `closedir`, `scandir`, `glob`, `getcwd`, `chdir`, `tempnam`, `sys_get_temp_dir`, `var_dump`, `print_r` +**I/O:** `fopen`, `fclose`, `fread`, `fwrite`, `fprintf`, `vfprintf`, `fscanf`, `fgets`, `fgetc`, `fpassthru`, `flock`, `tmpfile`, `readfile`, `feof`, `readline`, `fseek`, `ftell`, `rewind`, `file_get_contents`, `file_put_contents`, `file`, `hash_file`, `fgetcsv`, `fputcsv`, `file_exists`, `is_file`, `is_dir`, `is_readable`, `is_writable`, `is_writeable`, `is_executable`, `is_link`, `symlink`, `link`, `readlink`, `linkinfo`, `filesize`, `filemtime`, `fileatime`, `filectime`, `fileperms`, `fileowner`, `filegroup`, `fileinode`, `filetype`, `stat`, `lstat`, `fstat`, `clearstatcache`, `disk_free_space`, `disk_total_space`, `basename`, `dirname`, `pathinfo`, `realpath`, `realpath_cache_get`, `realpath_cache_size`, `fnmatch`, `touch`, `chmod`, `chown`, `chgrp`, `lchown`, `lchgrp`, `umask`, `ftruncate`, `fflush`, `fsync`, `fdatasync`, `copy`, `rename`, `unlink`, `mkdir`, `rmdir`, `opendir`, `readdir`, `rewinddir`, `closedir`, `scandir`, `glob`, `getcwd`, `chdir`, `tempnam`, `sys_get_temp_dir`, `var_dump`, `print_r` **Streams and sockets:** `stream_isatty`, `stream_is_local`, `stream_supports_lock`, `stream_get_wrappers`, `stream_get_transports`, `stream_get_filters`, `stream_context_create`, `stream_context_get_default`, `stream_context_set_default`, `stream_context_set_option`, `stream_context_set_params`, `stream_context_get_options`, `stream_context_get_params`, `stream_resolve_include_path`, `stream_get_contents`, `stream_copy_to_stream`, `stream_get_line`, `stream_get_meta_data`, `stream_set_chunk_size`, `stream_set_read_buffer`, `stream_set_write_buffer`, `stream_set_blocking`, `stream_set_timeout`, `stream_select`, `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_wrapper_register`, `stream_wrapper_unregister`, `stream_wrapper_restore`, `stream_socket_server`, `stream_socket_client`, `stream_socket_accept`, `stream_socket_enable_crypto`, `stream_socket_shutdown`, `stream_socket_sendto`, `stream_socket_recvfrom`, `stream_socket_get_name`, `stream_socket_pair`, `fsockopen`, `pfsockopen`, `popen`, `pclose`, `gethostname`, `gethostbyname`, `gethostbyaddr`, `getprotobyname`, `getprotobynumber`, `getservbyname`, `getservbyport` -**System:** `exit`, `die`, `time`, `microtime`, `hrtime`, `date`, `gmdate`, `mktime`, `gmmktime`, `checkdate`, `getdate`, `localtime`, `strtotime`, `date_default_timezone_get`, `date_default_timezone_set`, `sleep`, `usleep`, `getenv`, `putenv`, `php_uname`, `phpversion`, `exec`, `shell_exec`, `system`, `passthru`, `json_encode`, `json_decode`, `json_last_error`, `json_last_error_msg`, `json_validate`, `preg_match`, `preg_match_all`, `preg_replace_callback`, `preg_replace`, `preg_split`, `define`, `defined`, `class_attribute_names`, `class_attribute_args`, `class_get_attributes` +**System:** `exit`, `die`, `time`, `microtime`, `hrtime`, `date`, `gmdate`, `mktime`, `gmmktime`, `checkdate`, `getdate`, `localtime`, `strtotime`, `date_default_timezone_get`, `date_default_timezone_set`, `sleep`, `usleep`, `getenv`, `putenv`, `php_uname`, `phpversion`, `exec`, `shell_exec`, `system`, `passthru`, `json_encode`, `json_decode`, `json_last_error`, `json_last_error_msg`, `json_validate`, `preg_match`, `preg_match_all`, `preg_replace_callback`, `preg_replace`, `preg_split`, `define`, `defined`, `class_attribute_names`, `class_attribute_args`, `class_get_attributes`, `serialize`, `unserialize`, `header`, `http_response_code` **SPL/autoload:** `spl_autoload_register`, `spl_autoload_unregister`, `spl_autoload_functions`, `spl_autoload_extensions`, `spl_autoload_call`, `spl_autoload`, `spl_classes`, `spl_object_id`, `spl_object_hash`, `iterator_to_array`, `iterator_count`, `iterator_apply` @@ -509,21 +522,18 @@ src/ │ ├── ir/ # EIR data model, builder, validator, and printer ├── ir_lower/ # Active AST → EIR lowering -├── codegen_ir/ # Active EIR → target assembly backend -├── codegen/ # Frozen legacy AST backend plus shared ABI/runtime/target helpers -│ ├── mod.rs # Pipeline entry, main/global codegen orchestration -│ ├── driver_support.rs # Pipeline glue and orchestration helpers -│ ├── prescan.rs # Pre-pass collecting program-wide codegen metadata -│ ├── program_usage.rs # Usage analysis feeding metadata emission +├── codegen/ # Active EIR → target assembly backend +├── codegen_support/ # Shared ABI/runtime/target helpers used by codegen +│ ├── mod.rs # Shared metadata registries and support re-exports +│ ├── driver_support.rs # Runtime object, deferred callable, boxing, and hash-key helpers +│ ├── prescan.rs # Constant pre-scan feeding EIR lowering +│ ├── program_usage.rs # Required-class usage analysis feeding metadata emission │ ├── expr.rs # Expression codegen dispatcher │ ├── expr/ # Focused expression helpers (arrays, calls, objects, binops, ...) │ ├── stmt.rs # Statement codegen dispatcher │ ├── stmt/ # Focused statement helpers (arrays, control_flow, io, storage, ...) │ ├── abi/ # Target-aware calling-convention, frame, and value helpers -│ ├── functions/ # User function emission, wrappers, and epilogue cleanup -│ ├── main_emission.rs # Top-level program emission -│ ├── class_methods.rs # Class/static method emission orchestration -│ ├── function_variants.rs # Include-loaded function dispatchers +│ ├── functions/ # Closure/FCC wrapper emission and epilogue cleanup │ ├── interface_wrappers.rs # Interface dispatch return-shape adapters │ ├── callables.rs # Top-level callable metadata and indirect-call helpers │ ├── ffi.rs # Extern function/global/class codegen @@ -579,7 +589,7 @@ ELEPHC_PHP_CHECK=1 cargo test # cross-check output with PHP interpreter The **[docs/](docs/)** directory is a complete wiki covering every aspect of the compiler. Inside you'll find: -- **PHP syntax reference** — types, operators, control structures, functions, classes, namespaces, and all 380+ built-in functions with signatures and examples +- **PHP syntax reference** — types, operators, control structures, functions, classes, namespaces, and all 420+ built-in functions with signatures and examples - **Compiler extensions** — pointers, `buffer`, `packed class`, FFI with `extern`, and conditional compilation with `ifdef` — the features that take PHP beyond the web - **Compiler internals** — a step-by-step walkthrough of the full pipeline, from lexing to Pratt parsing to type checking to code generation and runtime structure - **ARM64 primer** — an introduction to ARM64 assembly for people who've never seen it, plus a quick reference of the ARM64 instruction set used by elephc's AArch64 backend @@ -598,3 +608,13 @@ MIT [![Nuno Maduro: PHP Is Getting a Compiler?](https://img.youtube.com/vi/x06307Ui3uY/maxresdefault.jpg)](https://www.youtube.com/watch?v=x06307Ui3uY) **[Nuno Maduro: PHP Is Getting a Compiler?](https://www.youtube.com/watch?v=x06307Ui3uY)** + +## Star History + + + + + + Star History Chart + + diff --git a/ROADMAP.md b/ROADMAP.md index 3951e81cf4..54637b0721 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -575,7 +575,7 @@ PHP-visible surface, not the internal commits. - [x] Phase 47 — runtime-built `phar://` write streams for `fopen()`: non-literal `fopen($path, $mode)` now publishes the PHAR URL writer bridge alongside the dynamic reader bridge. `__rt_fopen_maybe_phar` still routes `r*` modes to `__rt_phar_read_entry`, but `w`/`a`/`c`/`x` modes now tail-call `__rt_phar_write_open_url`, which persists the full runtime URL with `__rt_str_persist` so `fclose()` can finalize through `elephc_phar_put_url`. Dynamic stream writes therefore preserve sibling entries in native PHAR archives just like literal streams and dynamic `file_put_contents()` writes. Test: `test_fopen_dynamic_phar_write_preserves_existing_entries`. Follow-up phases cover compressed-entry controls and concurrent PHAR write streams; private-key signing remains deferred. ARM64 + x86_64 (both Docker-verified) - [x] Phase 48 — tar/zip `phar://` writes: the `elephc-phar` bridge now preserves the archive family for existing native PHAR, tar, and ZIP containers, and missing `.tar` / `.zip` archive paths are created in that family instead of native PHAR. Literal write splitting recognizes `.phar/`, `.tar/`, and `.zip/` boundaries; runtime-built URLs use the same suffix-aware split. ZIP output preserves stored/deflated entries, tar output is POSIX ustar, and native PHAR gzip/bzip2 entries keep their compression when replaced. Tests: `writes_tar_entries`, `writes_zip_entries`, `writes_preserve_gzip_native_phar_entries`, `writes_preserve_bzip2_native_phar_entries`, `test_file_put_contents_phar_tar_archive_runtime_readback`, `test_file_put_contents_phar_zip_archive_runtime_readback`. Follow-up phases cover compression controls and concurrent PHAR write streams; private-key signing remains deferred. ARM64 + x86_64 (both Docker-verified) - [x] Phase 49 — concurrent `phar://` write streams: write-mode `fopen()` now publishes buffered `elephc-phar` stream entrypoints, so literal and runtime-built PHAR URLs receive real synthetic descriptors in the `0x50000000..0x50000020` range instead of sharing one global `0x50000000` stream. `fwrite()` and `fclose()` dispatch that whole range, and the bridge owns per-descriptor payload/target state until finalization; the old assembly single-stream writer remains as an unlinked-bridge fallback. Tests: `concurrent_phar_write_streams_preserve_distinct_entries`, `test_fopen_concurrent_phar_write_streams_preserve_entries`. Deferred: private-key signing. ARM64 + x86_64 (both Docker-verified) -- [x] Phase 50 — `Phar` / `PharData` OOP baseline: the checker now injects builtin `Phar`, `PharData`, and `PharFileInfo` classes with PHP-facing format/compression/signature constants, constructors that store the archive path, object-local mixed metadata/string stub state, archive-scanned plus object-local entry iteration state, `addFromString()`, `delete()`, `compressFiles()`, `decompressFiles()`, path helpers, entry `getContent()`, and ArrayAccess methods (`offsetGet`, `offsetSet`, `offsetExists`, `offsetUnset`) lowered as synthetic PHP bodies over the existing `phar://` `file_get_contents()` / `file_put_contents()` / `unlink()` runtime paths plus the elephc-phar compression/listing bridge. This gives `$phar->addFromString("entry", "data")`, `$phar->delete("entry")`, `setMetadata()` / `getMetadata()` / `hasMetadata()` / `delMetadata()` for strings, arrays, ints, and null, `setStub()` / `getStub()`, native-PHAR `Phar::GZ` / `Phar::BZ2` / `Phar::NONE` compression control, ZIP `Phar::GZ` / `Phar::NONE` compression control, `$phar["entry"]->getContent()` reads through `PharFileInfo`, `foreach ($phar as $name => $info)` for entries scanned from existing native PHAR/tar/ZIP archives and entries written through that object, `isset($phar["entry"])`, and `unset($phar["entry"])` coverage for native PHAR plus tar/ZIP containers without new target-specific assembly. Tests: `test_phar_oop_array_access_read_write`, `test_phar_oop_add_from_string_writes_entries`, `test_phar_oop_metadata_stub_and_path_helpers`, `test_phar_oop_iteration_tracks_written_entries`, `test_phar_oop_iteration_scans_existing_archives`, `test_phar_oop_array_access_unset_deletes_entry`, `test_phar_oop_delete_method_removes_entries`, `test_phar_oop_compress_and_decompress_files`. Deferred: persisted metadata/stub serialization, tar compression controls, and private-key signing. +- [x] Phase 50 — `Phar` / `PharData` OOP baseline: the checker now injects builtin `Phar`, `PharData`, and `PharFileInfo` classes with PHP-facing format/compression/signature constants, constructors that store the archive path, object-local mixed metadata/string stub state, archive-scanned plus object-local entry iteration state, `addFromString()`, `delete()`, `compressFiles()`, `decompressFiles()`, path helpers, entry `getContent()`, and ArrayAccess methods (`offsetGet`, `offsetSet`, `offsetExists`, `offsetUnset`) lowered as synthetic PHP bodies over the existing `phar://` `file_get_contents()` / `file_put_contents()` / `unlink()` runtime paths plus the elephc-phar compression/listing bridge. This gives `$phar->addFromString("entry", "data")`, `$phar->delete("entry")`, `setMetadata()` / `getMetadata()` / `hasMetadata()` / `delMetadata()` for strings, arrays, ints, and null, `setStub()` / `getStub()`, native-PHAR `Phar::GZ` / `Phar::BZ2` / `Phar::NONE` compression control, ZIP `Phar::GZ` / `Phar::NONE` compression control, `$phar["entry"]->getContent()` reads through `PharFileInfo`, `foreach ($phar as $name => $info)` for entries scanned from existing native PHAR/tar/ZIP archives and entries written through that object, `isset($phar["entry"])`, and `unset($phar["entry"])` coverage for native PHAR plus tar/ZIP containers without new target-specific assembly. Tests: `test_phar_oop_array_access_read_write`, `test_phar_oop_add_from_string_writes_entries`, `test_phar_oop_metadata_stub_and_path_helpers`, `test_phar_oop_iteration_tracks_written_entries`, `test_phar_oop_iteration_scans_existing_archives`, `test_phar_oop_array_access_unset_deletes_entry`, `test_phar_oop_delete_method_removes_entries`, `test_phar_oop_compress_and_decompress_files`. Persisted metadata/stub serialization landed in a later change; tar archive-wide compression controls and private-key signing remain deferred. ### Streams — remaining work (subsystem considered *partially complete*; merged as-is) @@ -610,18 +610,61 @@ none are needed for typical stream usage. rewrites and OpenSSL/private-key signing. - [x] **`phar://` tar/zip variants** — native PHAR, tar-based PHAR, and zip-based PHAR containers are readable and writable through literal and - runtime PHAR URLs. ZIP64, encrypted ZIP entries, and ZIP data descriptors - remain deferred. + runtime PHAR URLs. ZIP entries written with a streaming data descriptor + (general-purpose flag bit 3) are read via the authoritative + central-directory sizes. ZIP64 archives (over 65535 entries, or sizes/offsets + over 4 GiB) are read and written — verified interchangeable with PHP and + Python. Traditional-PKWARE (ZipCrypto) encrypted ZIP entries are read and + written with a password set via the `Phar`/`PharData::setZipPassword()` compiler + extension (when set, zip entries — the stub included — are encrypted on write and + decrypted on read; the `.phar/signature.bin` entry stays in the clear). ZipCrypto + is cryptographically weak and kept only for compatibility with legacy archives. - [x] **`Phar` / `PharData` OOP API** — a baseline constructor/constants, `addFromString()`, `delete()`, native-PHAR `compressFiles()` / `decompressFiles()` (native PHAR plus ZIP `Phar::GZ` / `Phar::NONE`), - object-local mixed metadata/string stub accessors, path helpers, `PharFileInfo` + path helpers, `PharFileInfo` `getContent()`, archive-scanned and object-local entry iteration, and ArrayAccess read/write/isset surface is implemented (Phase 50). `offsetUnset()` - deletes archive entries through the PHAR-aware `unlink()` path. The closed - scope is the baseline OOP archive surface; persisted metadata/stub - serialization, tar compression controls, and OpenSSL/private-key signing remain - future work. + deletes archive entries through the PHAR-aware `unlink()` path. +- [x] **`Phar` persisted global metadata and stub** — `setMetadata()` / + `getMetadata()` / `hasMetadata()` / `delMetadata()` and `setStub()` / `getStub()` + persist into the archive file and round-trip across fresh objects and processes + (and the PHP interpreter) for native PHAR (manifest field + byte-prefix stub), + tar (`.phar/.metadata.bin` + `.phar/stub.php`), and zip (EOCD comment + + `.phar/stub.php`); reserved `.phar/*` control entries are hidden from listings. + Backed by new public `serialize()` / `unserialize()` builtins (scalar + nested + array subset, byte-for-byte PHP-compatible). +- [x] **`serialize()` / `unserialize()` objects + references** — objects serialize as + `O::""::{...}` with PHP's exact public/protected/private key + mangling, honour the `__serialize`/`__unserialize`/`__sleep`/`__wakeup` magic methods, + and emit `r:;` back-references for repeated objects (PHP's global value counter); + `unserialize()` rebuilds shared objects as one instance (`===` identity preserved). + Byte-for-byte PHP-compatible; 3-target verified. Deferred: cyclic references inside an + object's own properties resolve to `null` on read, and the deprecated `Serializable` + (`C:`) interface is unsupported. +- [x] **`PharData` whole-archive (tar) compression** — `compress(Phar::GZ)` / + `compress(Phar::BZ2)` write a sibling `.tar.gz` / `.tar.bz2` and return a fresh + `PharData`; `decompress()` writes the plain `.tar` back. Compressed archives are read + transparently (the bridge detects the gzip/bzip2 wrapper) and are interchangeable with + the PHP interpreter. Per-entry native/zip compression stays on `compressFiles()`. +- [x] **`Phar` signatures incl. OpenSSL (RSA)** — `setSignatureAlgorithm()` / + `getSignature()` across native PHAR, tar, and zip phars. Hash algorithms + (MD5/SHA1/SHA256/SHA512) and `setSignatureAlgorithm(Phar::OPENSSL, $privateKey)` + (RSA-SHA1, PEM PKCS#1/PKCS#8 key via the pure-Rust `rsa` crate). Native PHARs use the + `digest/sig ++ flag ++ "GBMB"` trailer; tar and zip phars use a `.phar/signature.bin` + control entry (`LE32(flag) ++ LE32(len) ++ signature`) appended last, with the + signature computed over the data records (tar) or local entries + central directory + + comment (zip), matching php-src. `getSignature()` returns `['hash' => , + 'hash_type' => ...]`. The PHP interpreter verifies elephc-written signatures across all + three families (OpenSSL against the matching `.pubkey`). +- [x] **`PharFileInfo` persisted per-file metadata** — `setMetadata()` / + `getMetadata()` / `hasMetadata()` / `delMetadata()` on the `PharFileInfo` objects + returned by ArrayAccess persist per-entry metadata into the archive file and + round-trip across fresh objects and the PHP interpreter for native PHAR (per-entry + manifest field), tar (`.phar/.metadata//.metadata.bin` side entry), and zip + (per-entry central-directory file comment). Same `serialize()` scalar + array + subset as global metadata; verified byte-compatible by reading elephc-written + archives back with the PHP interpreter (including nested entry paths). - [x] **TLS `ciphers` / `security_level`** — accepted without error but *not honored*: rustls has no OpenSSL-cipher-string equivalent and selects TLS 1.2/1.3 automatically. Honest no-op by design (upstream limitation), not a @@ -713,9 +756,9 @@ imposed. See `docs/internals/the-ir.md`. - [x] Register-pressure mitigations: caller-saved reuse for non-call-crossing intervals; better spill heuristic. The linear-scan allocator now classifies each live interval as call-free (never crosses a clobber point — an instruction/terminator whose lowering emits a call or touches a caller-saved register, per the safe-by-default allowlist in `src/ir_passes/clobber.rs`) and assigns call-free intervals from caller-saved pools that need no prologue save/restore (`x12`–`x15`/`d16`–`d23` on aarch64, `rsi`/`rdi`/`r8`/`r9`/`xmm2`–`xmm7` on x86_64), falling back to callee-saved (`x21`–`x28`/`d8`–`d14`/`rbx`) for cross-call values. This notably unlocks register allocation for x86_64 floats (no callee-saved XMM) and integers (callee pool is only `rbx`). The spill heuristic is now use-weighted: under pressure the rarely-used, furthest-reaching interval is evicted first, keeping hot values in registers Expected outcome: EIR is the default and only active implementation backend in -v0.24.x. The legacy AST backend is frozen behind `--ast-backend` for diagnostics -and removal work only, and ≥15% performance improvement on compute benchmarks -after Phase 06 by end of v0.24.x. +v0.24.x. The legacy AST backend is frozen for diagnostics and removal work only, +and ≥15% performance improvement on compute benchmarks after Phase 06 by end of +v0.24.x. ## v0.25.x — EIR optimization passes and Image support @@ -868,32 +911,57 @@ tested, diagnostic-emitting gaps. for the procedural PHP layer, and `program_uses_image` now detects the `cairo_` prefix so pure-procedural programs pull in the prelude +### Array builtin parity (key/list helpers, associative set-ops, recursive merge/walk) + +Well-bounded PHP-visible array builtins implemented on the EIR backend. All +target-aware (ARM64 + x86_64), with codegen and error tests; the shared `__rt_*` +runtime helpers are reused and driven through EIR lowering. + +- [x] `array_key_first()` / `array_key_last()` (PHP 7.3) — first/last key in insertion order, boxed as `Mixed`, `null` for empty arrays +- [x] `array_is_list()` (PHP 8.1) — sequential `0..n-1` key check (indexed arrays are lists by construction; associative arrays walk the insertion-order chain) +- [x] `array_replace()` / `array_replace_recursive()` — right-wins key merge over associative arrays (recursive variant merges when both values at a key are associative arrays) +- [x] `array_diff_assoc()` / `array_intersect_assoc()` — key + string-cast-value comparison via the unified `__rt_assoc_diff_intersect` helper +- [x] `array_merge_recursive()` — integer-key renumbering, string-key collisions recurse (both arrays) or combine into a list (scalars) +- [x] `array_walk_recursive()` — invokes the callback on each non-array leaf, recursing through nested indexed/associative arrays +- [x] `array_find()` / `array_any()` / `array_all()` (PHP 8.4) — predicate callbacks; find returns the first match or `null`, any/all return booleans +- [x] `array_udiff()` / `array_uintersect()` — difference/intersection with a user comparator (`$cmp($a, $b) === 0`) +- [x] `array_multisort()` — sort the first indexed array ascending (stable) and reorder a second array in tandem, both by reference (two scalar-element arrays; flags/descending/multi-key are follow-ups) +- [x] Scalar indexed-array inputs for the hash-based functions converted to integer-keyed hashes via `__rt_array_to_hash`; result key/value widen to `Mixed` for heterogeneous inputs so `foreach` dispatches keys correctly. Callback/comparator builtins reuse the EIR descriptor-callback machinery (string, function, and non-capturing closure callbacks). Hash-based functions accept associative arrays and scalar-element indexed arrays; string/heap-element indexed inputs and the callback/sort element-type limits are documented in `docs/php/arrays.md` + ## v0.26.x — Performance closure, legacy cleanup, and 0.x stabilization Optimization work should now be driven by benchmarks, generated assembly size, and 0.x validation rather than by speculative pass work. -- [x] Generators reimplemented on stackful coroutines (issue #329) — a generator body is compiled by the normal EIR backend and runs on its own coroutine stack (reusing the Fiber runtime), replacing the v1 state-machine lowering on the EIR path. `Generator::throw()` now raises the exception at the suspended `yield`, so a `try`/`catch` inside the generator body handles it and resumes instead of always terminating the generator and propagating to the caller; in-generator method calls, arbitrary control flow, and `try`/`finally` around `yield` work like ordinary functions. `yield from` over generators delegates through `__rt_gen_delegate` (forwarding sent values and returning the inner `getReturn()`) and over arrays desugars into an iterator loop; `send()`/`getReturn()`/closure captures preserved; Generator GC frees the coroutine stack and boxed key/value/return cells. The frozen legacy AST backend keeps its own `GeneratorFrame` state machine. +- [x] Generators reimplemented on stackful coroutines (issue #329) — a generator body is compiled by the normal EIR backend and runs on its own coroutine stack (reusing the Fiber runtime), replacing the v1 state-machine lowering on the EIR path. `Generator::throw()` now raises the exception at the suspended `yield`, so a `try`/`catch` inside the generator body handles it and resumes instead of always terminating the generator and propagating to the caller; in-generator method calls, arbitrary control flow, and `try`/`finally` around `yield` work like ordinary functions. `yield from` over generators delegates through `__rt_gen_delegate` (forwarding sent values and returning the inner `getReturn()`) and over arrays desugars into an iterator loop; `send()`/`getReturn()`/closure captures preserved; Generator GC frees the coroutine stack and boxed key/value/return cells. +- [x] Closure rebinding — `Closure::bind()`, `bindTo()`, and `Closure::call()` rebind a closure to a new receiver; a top-level closure that captures `$this` now binds it correctly instead of losing the receiver, and a by-reference `Closure::bind` stored in a variable and called later is tracked as a static callable so the call carries the bound cell directly (`__rt_closure_bind`) rather than going through the generic descriptor invoker +- [x] New magic methods `__callStatic`, `__isset`, and `__unset` — a static call to an undeclared method dispatches to `__callStatic`; `isset()`/`empty()` on an undeclared property route through `__isset` (and only read `__get` when `__isset` is truthy, so an unset virtual property is empty without ever being read); and `unset($obj->prop)` on a virtual property calls `__unset` +- [x] Reflection over functions — `ReflectionFunction` (name and parameter counts), `getParameters()`, `ReflectionParameter`, `ReflectionParameter::getType()`, and `ReflectionNamedType`; attribute arguments are exposed in reflection metadata, including float, positional-array, named-argument and associative-array values, references to global and class constants, and enum-case references +- [x] References to object properties — `$x = &$obj->prop` aliases the property with write-through in both directions, and a by-reference function/method return can be captured with `$x = &f()` (including `string`- and `float`-typed properties); lowered via the `LoadPropRefCell` / `BindRefCellPtr` IR ops. Reassigning an array reference to a non-empty literal of a different type boxes the literal's elements to match the property's element type +- [x] Enum case `->name` property (issue #330) — every enum case, pure or backed, exposes the read-only `name` string holding the case identifier (`E::A->name` is `"A"`), matching PHP's `UnitEnum::$name`; backed cases keep `->value`, `$this->name` is readable inside enum methods, and access works through direct case access, an aliasing variable, `cases()`, and string interpolation - [ ] Source maps v2 — richer mappings for functions / expressions / labels and a more stable machine-readable schema for external tooling - [ ] Memory-model-aware propagation for heap-backed locals and targeted runtime invalidations beyond `unset($var)` and the currently modeled local writes -- [ ] Resource scope-cleanup — auto-free tag-9 resource handles that leave scope without their explicit close (today an unclosed `fopen()` leaks its fd and an unfinalized `hash_init()` context leaks its heap state until process exit; `functions/cleanup.rs` skips `Resource`s by design). Prerequisites: a resource-kind subtype in the Mixed cell so the cleanup pass can pick the right destructor (fd → `close()`, HashContext → `elephc_crypto_free`, …), and aliasing safety (resources have no refcount; `$b = $a` would double-free under naive scope-free). Includes wiring the currently-uncalled `elephc_crypto_free` (`_elephc_crypto_free_fn` slot + publish entry + a `__rt_hash_ctx_free` helper) and nulling the Mixed payload in `hash_final` so finalized contexts are skipped — which also defuses the double-final UB documented in `src/codegen/runtime/strings/hash_context.rs` +- [x] Resource scope-cleanup — auto-free tag-9 resource handles that leave scope without their explicit close (today an unclosed `fopen()` leaks its fd and an unfinalized `hash_init()` context leaks its heap state until process exit; `functions/cleanup.rs` skips `Resource`s by design). Prerequisites: a resource-kind subtype in the Mixed cell so the cleanup pass can pick the right destructor (fd → `close()`, HashContext → `elephc_crypto_free`, …), and aliasing safety (resources have no refcount; `$b = $a` would double-free under naive scope-free). Includes wiring the currently-uncalled `elephc_crypto_free` (`_elephc_crypto_free_fn` slot + publish entry + a `__rt_hash_ctx_free` helper) as the single HashContext destructor and making `hash_final` finalize a *clone* (leaving the original owned by its Mixed box) so a finalized context that later leaves scope is freed exactly once — closing the double-final/use-after-free hole documented in `src/codegen/runtime/strings/hash_context.rs`. `popen` pipes (kind 3 → `__rt_pclose`) and `opendir` streams (kind 4 → `__rt_closedir`) are released the same way, and an explicit `fclose`/`pclose`/`closedir` stamps a `-1` sentinel into the box so a descriptor (whose fd number may be reused) is never closed twice - [ ] Purity / may-throw v2 for dynamic instance dispatch, richer property/array reads, and less pessimistic builtin modeling (feeds the EIR effects table) - [ ] Guard reasoning v2 for dead-code elimination — broader range reasoning and multi-variable facts beyond current strict-scalar, boolean, loose-comparison, and safe relational-complement guards - [ ] Exception-aware DCE v2 — exact thrown-type / handler reachability, nested try rethrow modeling, and less conservative finally-path invalidation - [ ] Control-flow normalization v2 — broader canonicalization of nested block/control shells before CFG-aware optimization passes - [ ] Composite conditional include function variants — extend include-graph exclusivity from one direct `if` / `elseif` / `else` chain to nested/composed conditional paths where declarations are pairwise exclusive only after combining multiple branch decisions - [ ] Switch-aware conditional include function variants — extend include-graph exclusivity beyond `if` / `elseif` / `else` to `switch` cases once fall-through, `break`, and terminating case bodies are modeled precisely; revisit `match` only if include-like statement lowering ever appears inside match arms -- [ ] Runtime routine dead stripping — include or link only runtime helpers reachable from the generated program instead of carrying the whole target runtime slice +- [x] Runtime routine dead stripping — include or link only runtime helpers reachable from the generated program instead of carrying the whole target runtime slice +- [x] Windows x86_64 (PE32+) cross-compilation target (experimental, newly added) — `--target windows-x86_64` (alias `x86_64-pc-windows-gnu`) cross-compiles to a GNU/MinGW-ABI binary via `x86_64-w64-mingw32-gcc` (`msvcrt`), producing `.exe` (`.dll` for `--emit cdylib`); requires the MinGW-w64 cross toolchain (`x86_64-w64-mingw32-as`, `x86_64-w64-mingw32-gcc`) on the host doing the build. CI cross-compiles and validates PE32+ structure (assemble + link) with MinGW-w64, and additionally executes the cross-compiled binaries under Wine (`wine64`/`wine`) to assert stdout for echo, arithmetic, string concatenation, loops, and function calls — closing the prior compile-only testing gap; broader runtime shim coverage (files, sockets, process control, …) is still not at parity with macOS/Linux. +- [x] `random_bytes(int $length): string` — cryptographically secure random byte string on every supported target (arc4random_buf / getrandom / BCryptGenRandom), fatal on entropy failure or length below 1 +- [x] Statically-known catchable `Error` conditions (issue #383) — private/protected method access from an inaccessible scope and readonly property writes outside the declaring constructor raise a catchable `Error` at runtime instead of being rejected at compile time, matching PHP - [ ] Tail-call optimization — direct tail self- and mutual-recursion lowering on top of EIR (`Br` to function entry with parameter rebinding) - [ ] Performance within 2x of C -O0 on compute benchmarks -- [ ] DOOM showcase performance gate after EIR optimizations — build and run a reproducible SDL benchmark for `showcases/doom`, track EIR FPS / generated assembly size / runtime helper counts, optionally compare against the last known legacy baseline when available, and require no large real-world regression before deleting the frozen legacy backend +- [ ] DOOM showcase performance gate after EIR optimizations — build and run a reproducible SDL benchmark for `showcases/doom`, track EIR FPS / generated assembly size / runtime helper counts, optionally compare against the last known legacy baseline when available, and require no large real-world regression before release - [ ] Real-world CLI tools compiled as validation -- [ ] Audit remaining references to `--ast-backend` and legacy AST emitters so docs, help text, and release notes present them as frozen diagnostic-only fallback before removal -- [ ] Remove the deprecated `--ast-backend` CLI flag once diagnostic fallback is no longer needed; report it as unsupported -- [ ] Delete frozen legacy AST → ASM emitter modules after shared ABI/runtime dependencies are disentangled -- [ ] Rename `src/codegen_ir/` to `src/codegen/` -- [ ] Move historical codegen doc to `docs/internals/legacy-codegen.md`; refresh `docs/internals/the-codegen.md` to describe the IR pipeline -- [ ] Refresh `docs/internals/the-ir.md` as the canonical, non-preview IR contract for v1.0 +- [x] Audit remaining references to `--ast-backend` and legacy AST emitters so docs, help text, and release notes no longer present a selectable fallback +- [x] Remove the deprecated `--ast-backend` CLI flag once diagnostic fallback is no longer needed; report it as unsupported +- [x] Delete frozen legacy AST → ASM emitter modules after shared ABI/runtime dependencies are disentangled +- [x] Rename `src/codegen_ir/` to `src/codegen/` +- [x] Move historical codegen doc to `docs/internals/legacy-codegen.md`; refresh `docs/internals/the-codegen.md` to describe the IR pipeline +- [x] Refresh `docs/internals/the-ir.md` as the canonical, non-preview IR contract for v1.0 - [ ] Apple notarization for direct downloads (codesign + notarytool) - [ ] Installation / packaging documentation for the supported host platforms diff --git a/crates/elephc-crypto/src/lib.rs b/crates/elephc-crypto/src/lib.rs index 328bdcc259..c18cc48dc6 100644 --- a/crates/elephc-crypto/src/lib.rs +++ b/crates/elephc-crypto/src/lib.rs @@ -11,7 +11,11 @@ //! Key details: //! - All ABI functions are `#[no_mangle] pub extern "C"`; raw digests are written //! into a caller-provided 64-byte buffer (max digest size across supported algos). -//! - `ctx` handles are thin pointers to a boxed `HashCtx`; `final`/`free` own them. +//! - `ctx` handles are thin pointers to a boxed `HashCtx`. The boxed `Mixed` +//! resource cell owns the handle for its whole lifetime: `free` is the sole +//! destructor (driven by compiler scope-cleanup), while `final` finalizes a +//! *clone* and leaves the original handle live. This keeps a context that is +//! finalized and then dropped at scope exit a single, safe free. mod algos; mod hmac; @@ -191,18 +195,26 @@ pub unsafe extern "C" fn elephc_crypto_update( ctx.update(slice(data_ptr, data_len)); } -/// Finalizes the context into `out`, consuming and freeing it. Returns the -/// digest length, or -1 for a null handle. +/// Finalizes a *clone* of the context into `out`, leaving the original handle +/// live and owned by its boxed `Mixed` resource cell. Returns the digest length, +/// or -1 for a null handle. +/// +/// The handle is intentionally NOT freed here: ownership stays with the boxed +/// resource cell, whose scope-cleanup destructor calls `elephc_crypto_free` +/// exactly once when the box is released. Finalizing a clone keeps the handle +/// valid afterwards, so a redundant `hash_final()`/`hash_update()` on the same +/// handle (which PHP rejects) stays memory-safe instead of being a use-after-free +/// or a double-free against scope cleanup. /// /// # Safety -/// `ctx` must be a live handle (invalid after this call); `out` must hold 64 bytes. +/// `ctx` must be a live handle; `out` must hold 64 bytes. #[no_mangle] pub unsafe extern "C" fn elephc_crypto_final(ctx: *mut c_void, out_ptr: *mut u8) -> isize { if ctx.is_null() { return -1; } - let ctx = Box::from_raw(ctx as *mut HashCtx); - let digest = ctx.finalize(); + let ctx = &*(ctx as *mut HashCtx); + let digest = ctx.clone_box().finalize(); std::ptr::copy_nonoverlapping(digest.as_ptr(), out_ptr, digest.len()); digest.len() as isize } @@ -220,12 +232,14 @@ pub unsafe extern "C" fn elephc_crypto_clone(ctx: *mut c_void) -> *mut c_void { Box::into_raw(Box::new(ctx.clone_box())) as *mut c_void } -/// Frees a context without finalizing (scope-exit / error cleanup). +/// Frees a context (scope-exit / error cleanup) without finalizing. /// -/// Currently UNWIRED on the compiler side by design: elephc has no Resource -/// scope-cleanup yet, so nothing calls this and an unfinalized context leaks -/// until process exit (like an unclosed `fopen()` stream). Kept in the ABI for -/// the planned cleanup pass — see ROADMAP.md (v0.26.x, "Resource scope-cleanup"). +/// This is the single owner that frees a `HashContext`: the compiler's Resource +/// scope-cleanup boxes the handle as a `Mixed` resource cell (tag 9, kind 2) and +/// releases it here through `__rt_mixed_free_deep` → `__rt_hash_ctx_free` when the +/// owning variable leaves scope. Because `elephc_crypto_final` no longer frees, +/// finalizing a context and then dropping it at scope exit is a single free with +/// no double-free. /// /// # Safety /// `ctx` must be a live handle and must not be used afterwards. diff --git a/crates/elephc-phar/Cargo.toml b/crates/elephc-phar/Cargo.toml index 197021c4d4..e415fbed71 100644 --- a/crates/elephc-phar/Cargo.toml +++ b/crates/elephc-phar/Cargo.toml @@ -14,3 +14,6 @@ flate2 = "1" bzip2 = "0.6" bzip2-rs = "0.1" sha1 = "0.10" +sha2 = "0.10" +md-5 = "0.10" +rsa = { version = "0.9", features = ["sha1", "sha2", "pem"] } diff --git a/crates/elephc-phar/src/lib.rs b/crates/elephc-phar/src/lib.rs index fbcc51ed15..a2f2256024 100644 --- a/crates/elephc-phar/src/lib.rs +++ b/crates/elephc-phar/src/lib.rs @@ -15,26 +15,49 @@ //! until the next `elephc_phar_extract_url` or `elephc_phar_list_entries` call. //! - Writes preserve the archive family for existing native PHAR, tar, and ZIP //! archives. Native PHAR gzip/bzip2 entries and ZIP deflate entries keep their -//! compression when replaced. ZIP64, encrypted ZIP entries, ZIP data -//! descriptors, tar archive compression, and private-key signing are -//! intentionally unsupported. +//! compression when replaced. ZIP64 archives are read and written (entry counts +//! over 65535 or sizes/offsets over 4 GiB). Traditional-PKWARE (ZipCrypto) +//! encrypted entries are read and written, using a password set via +//! `elephc_phar_set_zip_password` (ZipCrypto is cryptographically weak — kept for +//! compatibility, not as a real confidentiality mechanism). use std::io::{Read, Write}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; +use std::time::{SystemTime, UNIX_EPOCH}; const PHAR_FLAG_GZIP: u32 = 0x0000_1000; const PHAR_FLAG_BZIP2: u32 = 0x0000_2000; const PHAR_HDR_SIGNATURE: u32 = 0x0001_0000; const PHAR_FILE_MODE_0644: u32 = 0x0000_01a4; const PHAR_SHA1_SIGNATURE_TYPE: u32 = 0x0000_0002; +const PHAR_OPENSSL_SIGNATURE_TYPE: u32 = 0x0000_0010; const ZIP_METHOD_STORE: u16 = 0; const ZIP_METHOD_DEFLATE: u16 = 8; +/// ZIP general-purpose flag bit 0: the entry is encrypted (traditional ZipCrypto). +const ZIP_FLAG_ENCRYPTED: u16 = 0x0001; +/// ZIP general-purpose flag bit 3: sizes/CRC are in a trailing data descriptor. const ZIP_FLAG_DATA_DESCRIPTOR: u16 = 0x0008; +/// ZIP64 extended-information extra-field tag. +const ZIP64_EXTRA_TAG: u16 = 0x0001; +/// 32-bit field value meaning "real value is in the ZIP64 extra field / EOCD64". +const ZIP32_SENTINEL: u32 = 0xFFFF_FFFF; +/// 16-bit entry-count field value meaning "real count is in the EOCD64". +const ZIP16_SENTINEL: u16 = 0xFFFF; const PHAR_WRITE_FD_BASE: usize = 0x5000_0000; const PHAR_WRITE_STREAM_LIMIT: usize = 32; static EXTRACT_BUFFER: OnceLock>> = OnceLock::new(); static WRITE_STREAMS: OnceLock>>> = OnceLock::new(); +thread_local! { + /// Password used to read and write traditional-PKWARE (ZipCrypto) encrypted ZIP + /// entries, set through [`elephc_phar_set_zip_password`]; `None` until provided. + /// When set, zip phars are written with their entries encrypted. Thread-local: + /// it is set and consumed on the same (single) runtime thread, which also keeps + /// parallel unit tests from clobbering each other's password state. + static ZIP_PASSWORD: std::cell::RefCell>> = + const { std::cell::RefCell::new(None) }; +} #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum PharCompression { @@ -48,6 +71,11 @@ struct ArchiveEntry { name: Vec, payload: Vec, compression: PharCompression, + /// PHP-`serialize()`d per-file metadata blob (empty when the entry has none). + /// Stored in the native manifest's per-entry metadata field, a tar + /// `.phar/.metadata//.metadata.bin` side entry, or a zip central-directory + /// file comment, depending on the archive family. + metadata: Vec, } #[derive(Clone, Copy)] @@ -57,6 +85,41 @@ enum ArchiveFormat { Zip, } +/// Internal entry name holding a tar/zip-based phar's executable stub. +const PHAR_STUB_ENTRY: &[u8] = b".phar/stub.php"; +/// Internal entry name holding a tar/zip-based phar's serialized global metadata. +const PHAR_METADATA_ENTRY: &[u8] = b".phar/.metadata.bin"; +/// Internal entry name holding a tar/zip-based phar's signature. Its payload is +/// `LE32(sig_flag) ++ LE32(sig_len) ++ signature`, and it is always the archive's +/// last entry so the signed range is everything that precedes it. +const PHAR_SIGNATURE_ENTRY: &[u8] = b".phar/signature.bin"; +/// Prefix of the tar side entry holding one file's serialized metadata. The full +/// name is `.phar/.metadata//.metadata.bin` (matching php-src). +const PHAR_FILE_METADATA_PREFIX: &[u8] = b".phar/.metadata/"; +/// Suffix of the tar per-file metadata side entry (see [`PHAR_FILE_METADATA_PREFIX`]). +const PHAR_FILE_METADATA_SUFFIX: &[u8] = b"/.metadata.bin"; +/// Default native-PHAR stub emitted when no custom stub has been set. +const PHAR_DEFAULT_STUB: &[u8] = b"\r\n"; + +/// A parsed archive plus its archive-level global metadata and stub. +/// +/// `metadata` holds the PHP-`serialize()`d global metadata blob (empty when none); +/// `stub` holds the executable stub bytes (empty when none/default). Both are +/// preserved across read-modify-write cycles and re-emitted by [`build_archive`]. +#[derive(Clone)] +struct Archive { + entries: Vec, + format: ArchiveFormat, + metadata: Vec, + stub: Vec, +} + +/// Returns true for the reserved `.phar/*` control entries that phars hide from +/// their public entry listing (stub, metadata, alias, signature, per-file metadata). +fn is_phar_control_entry(name: &[u8]) -> bool { + name.starts_with(b".phar/") +} + enum WriteStreamTarget { Entry { archive: Vec, entry: Vec }, Url(Vec), @@ -87,6 +150,13 @@ pub fn extract_url_bytes(url: &[u8]) -> Option> { /// arbitrary stubs before the payload. Plain ZIP and TAR containers are then /// tried by signature/layout. pub fn extract_entry_bytes(archive: &[u8], entry: &[u8]) -> Option> { + // Whole-archive gzip/bzip2 wrappers are decoded transparently before extraction. + if archive.starts_with(b"\x1f\x8b") { + return extract_entry_bytes(&decompress_gzip_stream(archive)?, entry); + } + if archive.starts_with(b"BZh") { + return extract_entry_bytes(&decompress_bzip2_stream(archive)?, entry); + } parse_native_phar_entry(archive, entry) .or_else(|| parse_zip_entry(archive, entry)) .or_else(|| parse_tar_entry(archive, entry)) @@ -125,15 +195,20 @@ pub fn put_entry_bytes( } let archive_path = std::str::from_utf8(archive_path).ok()?; let path = std::path::Path::new(archive_path); - let (mut entries, format) = if path.exists() { - let archive = std::fs::read(path).ok()?; - parse_archive_entries(&archive)? + let mut archive = if path.exists() { + let bytes = std::fs::read(path).ok()?; + parse_archive(&bytes)? } else { - (Vec::new(), format_for_new_archive_path(path)) + Archive { + entries: Vec::new(), + format: format_for_new_archive_path(path), + metadata: Vec::new(), + stub: Vec::new(), + } }; - upsert_entry(&mut entries, entry_name, payload); - let archive = build_archive(&entries, format)?; - std::fs::write(path, archive).ok()?; + upsert_entry(&mut archive.entries, entry_name, payload); + let out = build_archive_value(&archive)?; + std::fs::write(path, out).ok()?; Some(payload.len()) } @@ -159,11 +234,11 @@ pub fn delete_entry_bytes(archive_path: &[u8], entry_name: &[u8]) -> Option<()> } let archive_path = std::str::from_utf8(archive_path).ok()?; let path = std::path::Path::new(archive_path); - let archive = std::fs::read(path).ok()?; - let (mut entries, format) = parse_archive_entries(&archive)?; - remove_entry(&mut entries, entry_name)?; - let archive = build_archive(&entries, format)?; - std::fs::write(path, archive).ok()?; + let bytes = std::fs::read(path).ok()?; + let mut archive = parse_archive(&bytes)?; + remove_entry(&mut archive.entries, entry_name)?; + let out = build_archive_value(&archive)?; + std::fs::write(path, out).ok()?; Some(()) } @@ -184,22 +259,80 @@ pub fn set_archive_compression(archive_path: &[u8], compression_code: usize) -> let compression = compression_from_php_constant(compression_code)?; let archive_path = std::str::from_utf8(archive_path).ok()?; let path = std::path::Path::new(archive_path); - let archive = std::fs::read(path).ok()?; - let (mut entries, format) = parse_archive_entries(&archive)?; - if matches!(format, ArchiveFormat::Tar) { + let bytes = std::fs::read(path).ok()?; + let mut archive = parse_archive(&bytes)?; + if matches!(archive.format, ArchiveFormat::Tar) { return None; } - if matches!(format, ArchiveFormat::Zip) && matches!(compression, PharCompression::Bzip2) { + if matches!(archive.format, ArchiveFormat::Zip) + && matches!(compression, PharCompression::Bzip2) + { return None; } - for entry in &mut entries { + for entry in &mut archive.entries { entry.compression = compression; } - let archive = build_archive(&entries, format)?; - std::fs::write(path, archive).ok()?; + let out = build_archive_value(&archive)?; + std::fs::write(path, out).ok()?; + Some(()) +} + +/// Reads an archive's serialized global metadata blob (empty when unset). +fn get_metadata_bytes(archive_path: &[u8]) -> Option> { + let path = std::str::from_utf8(archive_path).ok()?; + let bytes = std::fs::read(path).ok()?; + Some(parse_archive(&bytes)?.metadata) +} + +/// Reads an archive's stub bytes (empty when unset / default). +fn get_stub_bytes(archive_path: &[u8]) -> Option> { + let path = std::str::from_utf8(archive_path).ok()?; + let bytes = std::fs::read(path).ok()?; + Some(parse_archive(&bytes)?.stub) +} + +/// Sets an archive's global metadata, preserving all entries and the stub. +/// +/// Creates the archive (format chosen by extension) when it does not yet exist. +fn set_metadata_bytes(archive_path: &[u8], metadata: &[u8]) -> Option<()> { + let path_str = std::str::from_utf8(archive_path).ok()?; + let path = std::path::Path::new(path_str); + let mut archive = read_or_new_archive(path)?; + archive.metadata = metadata.to_vec(); + std::fs::write(path, build_archive_value(&archive)?).ok()?; + Some(()) +} + +/// Sets an archive's stub, preserving all entries and global metadata. +/// +/// The stub must contain `__HALT_COMPILER();` (matching PHP); creates the archive +/// (format chosen by extension) when it does not yet exist. +fn set_stub_bytes(archive_path: &[u8], stub: &[u8]) -> Option<()> { + if find_subslice(stub, b"__HALT_COMPILER();").is_none() { + return None; + } + let path_str = std::str::from_utf8(archive_path).ok()?; + let path = std::path::Path::new(path_str); + let mut archive = read_or_new_archive(path)?; + archive.stub = stub.to_vec(); + std::fs::write(path, build_archive_value(&archive)?).ok()?; Some(()) } +/// Parses an existing archive, or builds an empty one whose format follows the path. +fn read_or_new_archive(path: &std::path::Path) -> Option { + if path.exists() { + parse_archive(&std::fs::read(path).ok()?) + } else { + Some(Archive { + entries: Vec::new(), + format: format_for_new_archive_path(path), + metadata: Vec::new(), + stub: Vec::new(), + }) + } +} + /// C ABI wrapper around [`extract_url_bytes`]. /// /// Returns a pointer to a stable process-global buffer and writes the byte @@ -319,6 +452,22 @@ pub unsafe extern "C" fn elephc_phar_delete_url( } } +/// C ABI wrapper around [`set_zip_password`]. +/// +/// Sets the password used to read and write traditional-PKWARE (ZipCrypto) +/// encrypted ZIP entries; an empty password clears it. Always returns `1`. +/// +/// # Safety +/// `password_ptr` must be valid for `password_len` bytes unless `password_len` is zero. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_set_zip_password( + password_ptr: *const u8, + password_len: usize, +) -> usize { + let _ = std::panic::catch_unwind(|| set_zip_password(slice(password_ptr, password_len))); + 1 +} + /// C ABI wrapper around [`set_archive_compression`]. /// /// Returns `1` when the native PHAR archive was rewritten, or `0` for invalid @@ -341,6 +490,297 @@ pub unsafe extern "C" fn elephc_phar_set_compression( } } +/// C ABI wrapper around [`get_metadata_bytes`]. +/// +/// Returns a pointer to the serialized global metadata buffer and writes its byte +/// length into `out_len`. Returns null and writes zero when there is no metadata or +/// the archive cannot be read. +/// +/// # Safety +/// `path_ptr` must be valid for `path_len` bytes unless `path_len` is zero. +/// `out_len` may be null; when non-null it must be writable. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_get_metadata( + path_ptr: *const u8, + path_len: usize, + out_len: *mut usize, +) -> *const u8 { + match std::panic::catch_unwind(|| get_metadata_bytes(slice(path_ptr, path_len))) { + Ok(Some(bytes)) if !bytes.is_empty() => publish_result(bytes, out_len), + _ => { + write_len(out_len, 0); + std::ptr::null() + } + } +} + +/// C ABI wrapper around [`get_stub_bytes`]. +/// +/// Returns a pointer to the stub buffer and writes its byte length into `out_len`. +/// Returns null and writes zero when there is no stub or the archive cannot be read. +/// +/// # Safety +/// `path_ptr` must be valid for `path_len` bytes unless `path_len` is zero. +/// `out_len` may be null; when non-null it must be writable. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_get_stub( + path_ptr: *const u8, + path_len: usize, + out_len: *mut usize, +) -> *const u8 { + match std::panic::catch_unwind(|| get_stub_bytes(slice(path_ptr, path_len))) { + Ok(Some(bytes)) if !bytes.is_empty() => publish_result(bytes, out_len), + _ => { + write_len(out_len, 0); + std::ptr::null() + } + } +} + +/// C ABI wrapper around [`set_metadata_bytes`]. +/// +/// Returns `1` when the archive was rewritten with the new global metadata, or `0` +/// on any failure. +/// +/// # Safety +/// Each pointer must be valid for its paired byte length unless that length is zero. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_set_metadata( + path_ptr: *const u8, + path_len: usize, + data_ptr: *const u8, + data_len: usize, +) -> usize { + let result = std::panic::catch_unwind(|| { + set_metadata_bytes(slice(path_ptr, path_len), slice(data_ptr, data_len)) + }); + match result { + Ok(Some(())) => 1, + _ => 0, + } +} + +/// C ABI wrapper around [`set_stub_bytes`]. +/// +/// Returns `1` when the archive was rewritten with the new stub, or `0` on any +/// failure (including a stub missing the `__HALT_COMPILER();` marker). +/// +/// # Safety +/// Each pointer must be valid for its paired byte length unless that length is zero. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_set_stub( + path_ptr: *const u8, + path_len: usize, + data_ptr: *const u8, + data_len: usize, +) -> usize { + let result = std::panic::catch_unwind(|| { + set_stub_bytes(slice(path_ptr, path_len), slice(data_ptr, data_len)) + }); + match result { + Ok(Some(())) => 1, + _ => 0, + } +} + +/// C ABI wrapper around [`get_file_metadata_url`]. +/// +/// Takes a `phar://archive/entry` URL and returns a pointer to that entry's +/// serialized metadata, writing its byte length into `out_len`. Returns null and +/// writes zero when the entry has no metadata, the entry is absent, or the archive +/// cannot be read. +/// +/// # Safety +/// Each pointer must be valid for its paired byte length unless that length is zero. +/// `out_len` may be null; when non-null it must be writable. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_get_file_metadata( + url_ptr: *const u8, + url_len: usize, + out_len: *mut usize, +) -> *const u8 { + let result = std::panic::catch_unwind(|| get_file_metadata_url(slice(url_ptr, url_len))); + match result { + Ok(Some(bytes)) if !bytes.is_empty() => publish_result(bytes, out_len), + _ => { + write_len(out_len, 0); + std::ptr::null() + } + } +} + +/// C ABI wrapper around [`set_file_metadata_url`]. +/// +/// Takes a `phar://archive/entry` URL and serialized metadata, rewriting the archive +/// so the entry carries it (an empty `data` clears it). Returns `1` on success, or +/// `0` on any failure including a missing entry. +/// +/// # Safety +/// Each pointer must be valid for its paired byte length unless that length is zero. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_set_file_metadata( + url_ptr: *const u8, + url_len: usize, + data_ptr: *const u8, + data_len: usize, +) -> usize { + let result = std::panic::catch_unwind(|| { + set_file_metadata_url(slice(url_ptr, url_len), slice(data_ptr, data_len)) + }); + match result { + Ok(Some(())) => 1, + _ => 0, + } +} + +/// C ABI wrapper around [`gzip_archive`] — whole-archive gzip compression. +/// +/// Returns a pointer to the written destination path and writes its length into +/// `out_len`; returns null and writes zero on failure. +/// +/// # Safety +/// `src` must be valid for `src_len` unless zero; `out_len` must be writable when non-null. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_gzip_archive( + src_ptr: *const u8, + src_len: usize, + out_len: *mut usize, +) -> *const u8 { + publish_archive_path_result( + std::panic::catch_unwind(|| gzip_archive(slice(src_ptr, src_len))), + out_len, + ) +} + +/// C ABI wrapper around [`bzip2_archive`] — whole-archive bzip2 compression. +/// +/// Returns a pointer to the written destination path and writes its length into +/// `out_len`; returns null and writes zero on failure. +/// +/// # Safety +/// `src` must be valid for `src_len` unless zero; `out_len` must be writable when non-null. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_bzip2_archive( + src_ptr: *const u8, + src_len: usize, + out_len: *mut usize, +) -> *const u8 { + publish_archive_path_result( + std::panic::catch_unwind(|| bzip2_archive(slice(src_ptr, src_len))), + out_len, + ) +} + +/// C ABI wrapper around [`decompress_archive`] — whole-archive decompression. +/// +/// Returns a pointer to the written destination path and writes its length into +/// `out_len`; returns null and writes zero on failure (including an uncompressed src). +/// +/// # Safety +/// `src` must be valid for `src_len` unless zero; `out_len` must be writable when non-null. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_decompress_archive( + src_ptr: *const u8, + src_len: usize, + out_len: *mut usize, +) -> *const u8 { + publish_archive_path_result( + std::panic::catch_unwind(|| decompress_archive(slice(src_ptr, src_len))), + out_len, + ) +} + +/// Shared result handling for the archive (de)compression bridges: publishes a +/// non-empty destination path, or returns null + zero length on failure. +fn publish_archive_path_result( + result: std::thread::Result>>, + out_len: *mut usize, +) -> *const u8 { + match result { + Ok(Some(path)) if !path.is_empty() => publish_result(path, out_len), + _ => { + write_len(out_len, 0); + std::ptr::null() + } + } +} + +/// C ABI wrapper around [`sign_archive_openssl`] — RSA-SHA1 (OpenSSL) PHAR signing. +/// +/// Returns `1` when the archive was re-signed, `0` on any failure (bad key, unreadable +/// archive). +/// +/// # Safety +/// Each pointer must be valid for its paired byte length unless that length is zero. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_sign_openssl( + path_ptr: *const u8, + path_len: usize, + key_ptr: *const u8, + key_len: usize, +) -> usize { + let result = std::panic::catch_unwind(|| { + sign_archive_openssl(slice(path_ptr, path_len), slice(key_ptr, key_len)) + }); + usize::from(matches!(result, Ok(Some(_)))) +} + +/// C ABI wrapper around [`sign_archive_hash`] — MD5/SHA1/SHA256/SHA512 PHAR signing. +/// +/// Returns `1` when the archive was re-signed, `0` on any failure or unknown `algo`. +/// +/// # Safety +/// `path` must be valid for `path_len` unless that length is zero. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_sign_hash( + path_ptr: *const u8, + path_len: usize, + algo: usize, +) -> usize { + let result = std::panic::catch_unwind(|| { + sign_archive_hash(slice(path_ptr, path_len), algo as u32) + }); + usize::from(matches!(result, Ok(Some(())))) +} + +/// C ABI wrapper around [`signature_hash_hex`] — `Phar::getSignature()['hash']`. +/// +/// Returns the uppercase-hex signature/digest pointer and writes its length into +/// `out_len`; returns null + zero on failure. +/// +/// # Safety +/// `path` must be valid for `path_len`; `out_len` must be writable when non-null. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_get_signature_hash( + path_ptr: *const u8, + path_len: usize, + out_len: *mut usize, +) -> *const u8 { + publish_archive_path_result( + std::panic::catch_unwind(|| signature_hash_hex(slice(path_ptr, path_len))), + out_len, + ) +} + +/// C ABI wrapper around [`signature_type_name`] — `Phar::getSignature()['hash_type']`. +/// +/// Returns the type-name pointer and writes its length into `out_len`; returns null + +/// zero on failure. +/// +/// # Safety +/// `path` must be valid for `path_len`; `out_len` must be writable when non-null. +#[no_mangle] +pub unsafe extern "C" fn elephc_phar_get_signature_type( + path_ptr: *const u8, + path_len: usize, + out_len: *mut usize, +) -> *const u8 { + publish_archive_path_result( + std::panic::catch_unwind(|| signature_type_name(slice(path_ptr, path_len))), + out_len, + ) +} + /// C ABI wrapper that opens a buffered write stream for a literal PHAR entry. /// /// Returns a synthetic descriptor in the `0x50000000..0x50000020` range, or @@ -557,12 +997,120 @@ fn split_write_url_entry(rest: &[u8]) -> Option<(&[u8], &[u8])> { Some((rest.get(..idx)?, rest.get(idx + 1..)?)) } +/// Parses archive bytes into a full [`Archive`] (entries plus global metadata/stub). +/// +/// Dispatch is by container signature rather than try-each-and-fallback: tar/zip-based +/// phars embed a `.phar/stub.php` containing `__HALT_COMPILER();`, so a native-first +/// scan would mistake them for native PHARs. ZIP starts with `PK\x03\x04` (or +/// `PK\x05\x06` when empty); TAR carries the ustar magic at offset 257; everything +/// else (a ` Option { + // A whole-archive gzip/bzip2 wrapper (e.g. `.tar.gz` / `.tar.bz2`) is decoded + // transparently, then the inner archive is parsed normally. + if data.starts_with(b"\x1f\x8b") { + return parse_archive(&decompress_gzip_stream(data)?); + } + if data.starts_with(b"BZh") { + return parse_archive(&decompress_bzip2_stream(data)?); + } + if data.starts_with(b"PK\x03\x04") || data.starts_with(b"PK\x05\x06") { + parse_zip_archive(data) + } else if data.get(257..262) == Some(b"ustar") { + parse_tar_archive(data) + } else { + parse_native_phar_archive(data) + } +} + +/// Decompresses a whole gzip (`.gz`) stream into its plain bytes. +fn decompress_gzip_stream(data: &[u8]) -> Option> { + let mut out = Vec::new(); + let mut decoder = flate2::read::GzDecoder::new(data); + std::io::Read::read_to_end(&mut decoder, &mut out).ok()?; + Some(out) +} + +/// Decompresses a whole bzip2 (`.bz2`) stream into its plain bytes. +fn decompress_bzip2_stream(data: &[u8]) -> Option> { + let mut out = Vec::new(); + let mut decoder = bzip2_rs::DecoderReader::new(data); + std::io::Read::read_to_end(&mut decoder, &mut out).ok()?; + Some(out) +} + +/// Returns the plain (uncompressed) archive bytes, stripping a whole-archive gzip or +/// bzip2 wrapper when present so a recompress operates on the canonical archive. +fn uncompressed_archive_bytes(raw: &[u8]) -> Option> { + if raw.starts_with(b"\x1f\x8b") { + decompress_gzip_stream(raw) + } else if raw.starts_with(b"BZh") { + decompress_bzip2_stream(raw) + } else { + Some(raw.to_vec()) + } +} + +/// Returns the destination path for compressing `src`: any existing `.gz`/`.bz2` +/// suffix is stripped, then `.` is appended (e.g. `foo.tar` → `foo.tar.gz`). +fn compression_dest_path(src: &[u8], new_ext: &str) -> Option> { + let s = std::str::from_utf8(src).ok()?; + let base = s + .strip_suffix(".gz") + .or_else(|| s.strip_suffix(".bz2")) + .unwrap_or(s); + Some(format!("{base}.{new_ext}").into_bytes()) +} + +/// Reads `src`, gzip-wraps its plain archive bytes, writes them to `.gz`, and +/// returns that destination path (PHP `PharData::compress(Phar::GZ)`). +fn gzip_archive(src: &[u8]) -> Option> { + let plain = uncompressed_archive_bytes(&read_path(src)?)?; + let mut encoder = + flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + std::io::Write::write_all(&mut encoder, &plain).ok()?; + let dest = compression_dest_path(src, "gz")?; + write_path(&dest, &encoder.finish().ok()?)?; + Some(dest) +} + +/// Reads `src`, bzip2-wraps its plain archive bytes, writes them to `.bz2`, and +/// returns that destination path (PHP `PharData::compress(Phar::BZ2)`). +fn bzip2_archive(src: &[u8]) -> Option> { + let plain = uncompressed_archive_bytes(&read_path(src)?)?; + let mut encoder = bzip2::write::BzEncoder::new(Vec::new(), bzip2::Compression::default()); + std::io::Write::write_all(&mut encoder, &plain).ok()?; + let dest = compression_dest_path(src, "bz2")?; + write_path(&dest, &encoder.finish().ok()?)?; + Some(dest) +} + +/// Reads a whole-archive-compressed `src` (a `.gz`/`.bz2` path), writes its plain +/// bytes to the path with that suffix removed, and returns that destination path +/// (PHP `PharData::decompress()`). Fails when `src` carries no compression suffix. +fn decompress_archive(src: &[u8]) -> Option> { + let s = std::str::from_utf8(src).ok()?; + let dest = s + .strip_suffix(".gz") + .or_else(|| s.strip_suffix(".bz2"))? + .as_bytes() + .to_vec(); + write_path(&dest, &uncompressed_archive_bytes(&read_path(src)?)?)?; + Some(dest) +} + +/// Reads a filesystem path given as UTF-8 bytes. +fn read_path(path: &[u8]) -> Option> { + std::fs::read(std::path::Path::new(std::str::from_utf8(path).ok()?)).ok() +} + +/// Writes `bytes` to a filesystem path given as UTF-8 bytes. +fn write_path(path: &[u8], bytes: &[u8]) -> Option<()> { + std::fs::write(std::path::Path::new(std::str::from_utf8(path).ok()?), bytes).ok() +} + /// Parses archive bytes into decoded entries and reports the archive family. fn parse_archive_entries(data: &[u8]) -> Option<(Vec, ArchiveFormat)> { - parse_native_phar_entries(data) - .map(|entries| (entries, ArchiveFormat::NativePhar)) - .or_else(|| parse_zip_entries(data).map(|entries| (entries, ArchiveFormat::Zip))) - .or_else(|| parse_tar_entries(data).map(|entries| (entries, ArchiveFormat::Tar))) + parse_archive(data).map(|archive| (archive.entries, archive.format)) } /// Selects the archive family for a missing output path. @@ -575,24 +1123,43 @@ fn format_for_new_archive_path(path: &std::path::Path) -> ArchiveFormat { } /// Builds an archive in the selected output family. -fn build_archive(entries: &[ArchiveEntry], format: ArchiveFormat) -> Option> { +fn build_archive( + entries: &[ArchiveEntry], + format: ArchiveFormat, + metadata: &[u8], + stub: &[u8], +) -> Option> { match format { - ArchiveFormat::NativePhar => build_native_phar_archive(entries), - ArchiveFormat::Tar => build_tar_archive(entries), - ArchiveFormat::Zip => build_zip_archive(entries), + ArchiveFormat::NativePhar => build_native_phar_archive(entries, metadata, stub), + ArchiveFormat::Tar => build_tar_archive(entries, metadata, stub), + ArchiveFormat::Zip => build_zip_archive(entries, metadata, stub), } } +/// Rebuilds an [`Archive`] into serialized bytes, preserving its metadata and stub. +fn build_archive_value(archive: &Archive) -> Option> { + build_archive( + &archive.entries, + archive.format, + &archive.metadata, + &archive.stub, + ) +} + /// Parses a native PHAR archive and returns a decoded entry payload. fn parse_native_phar_entry(data: &[u8], entry: &[u8]) -> Option> { - parse_native_phar_entries(data)? + parse_native_phar_archive(data)? + .entries .into_iter() .find(|candidate| candidate.name == entry) .map(|candidate| candidate.payload) } -/// Parses a native PHAR archive and returns every decoded entry payload. -fn parse_native_phar_entries(data: &[u8]) -> Option> { +/// Parses a native PHAR archive into entries plus its global metadata and stub. +/// +/// The stub is the byte prefix up to and including the `__HALT_COMPILER();` marker +/// (and any trailing ` ?>\r\n`); the global metadata is the manifest's metadata field. +fn parse_native_phar_archive(data: &[u8]) -> Option { let halt = b"__HALT_COMPILER();"; let halt_idx = find_subslice(data, halt)?; let mut p = halt_idx + halt.len(); @@ -603,6 +1170,7 @@ fn parse_native_phar_entries(data: &[u8]) -> Option> { } let manifest_start = p; + let stub = data.get(..manifest_start)?.to_vec(); let manifest_len = le32(data, manifest_start)? as usize; let data_section = manifest_start.checked_add(4)?.checked_add(manifest_len)?; let num_files = le32(data, manifest_start + 4)?; @@ -610,7 +1178,9 @@ fn parse_native_phar_entries(data: &[u8]) -> Option> { let alias_len = le32(data, q)? as usize; q = q.checked_add(4)?.checked_add(alias_len)?; let meta_len = le32(data, q)? as usize; - q = q.checked_add(4)?.checked_add(meta_len)?; + q = q.checked_add(4)?; + let metadata = data.get(q..q.checked_add(meta_len)?)?.to_vec(); + q = q.checked_add(meta_len)?; let mut data_offset = 0usize; let mut entries = Vec::with_capacity(num_files as usize); @@ -628,7 +1198,9 @@ fn parse_native_phar_entries(data: &[u8]) -> Option> { let flags = le32(data, q)?; q = q.checked_add(4)?; let entry_meta_len = le32(data, q)? as usize; - q = q.checked_add(4)?.checked_add(entry_meta_len)?; + q = q.checked_add(4)?; + let entry_metadata = data.get(q..q.checked_add(entry_meta_len)?)?.to_vec(); + q = q.checked_add(entry_meta_len)?; let start = data_section.checked_add(data_offset)?; let stored = data.get(start..start.checked_add(compressed)?)?; @@ -637,10 +1209,16 @@ fn parse_native_phar_entries(data: &[u8]) -> Option> { name: name.to_vec(), payload, compression: phar_compression_from_flags(flags), + metadata: entry_metadata, }); data_offset = data_offset.checked_add(compressed)?; } - Some(entries) + Some(Archive { + entries, + format: ArchiveFormat::NativePhar, + metadata, + stub, + }) } /// Extracts the PHAR compression mode from per-entry flags. @@ -681,26 +1259,74 @@ fn upsert_entry(entries: &mut Vec, entry_name: &[u8], payload: &[u name: entry_name.to_vec(), payload: payload.to_vec(), compression: PharCompression::None, + metadata: Vec::new(), }); } } -/// Removes an archive entry and reports failure when no matching entry exists. -fn remove_entry(entries: &mut Vec, entry_name: &[u8]) -> Option<()> { - let index = entries.iter().position(|entry| entry.name == entry_name)?; - entries.remove(index); - Some(()) +/// Returns the serialized per-file metadata for `entry_name`, or `None` if the +/// archive cannot be read or has no such entry. +fn get_file_metadata_bytes(archive_path: &[u8], entry_name: &[u8]) -> Option> { + let path = std::path::Path::new(std::str::from_utf8(archive_path).ok()?); + let archive = parse_archive(&std::fs::read(path).ok()?)?; + let entry = archive.entries.iter().find(|e| e.name == entry_name)?; + Some(entry.metadata.clone()) } -/// Builds a SHA1-signed native PHAR archive from decoded entries. -fn build_native_phar_archive(entries: &[ArchiveEntry]) -> Option> { - let mut manifest = Vec::new(); +/// Sets (or clears, when `metadata` is empty) the per-file metadata for +/// `entry_name` and rewrites the archive. Fails if the entry does not exist. +fn set_file_metadata_bytes( + archive_path: &[u8], + entry_name: &[u8], + metadata: &[u8], +) -> Option<()> { + let path = std::path::Path::new(std::str::from_utf8(archive_path).ok()?); + let mut archive = parse_archive(&std::fs::read(path).ok()?)?; + let entry = archive.entries.iter_mut().find(|e| e.name == entry_name)?; + entry.metadata.clear(); + entry.metadata.extend_from_slice(metadata); + let rebuilt = build_archive_value(&archive)?; + std::fs::write(path, rebuilt).ok() +} + +/// Reads per-file metadata addressed by a `phar://archive/entry` URL, splitting it +/// into archive path and entry name before delegating to [`get_file_metadata_bytes`]. +fn get_file_metadata_url(url: &[u8]) -> Option> { + let rest = url.strip_prefix(b"phar://")?; + let (archive_path, entry) = split_archive_entry(rest)?; + get_file_metadata_bytes(archive_path, entry) +} + +/// Writes per-file metadata addressed by a `phar://archive/entry` URL, splitting it +/// into archive path and entry name before delegating to [`set_file_metadata_bytes`]. +fn set_file_metadata_url(url: &[u8], metadata: &[u8]) -> Option<()> { + let rest = url.strip_prefix(b"phar://")?; + let (archive_path, entry) = split_archive_entry(rest)?; + set_file_metadata_bytes(archive_path, entry, metadata) +} + +/// Removes an archive entry and reports failure when no matching entry exists. +fn remove_entry(entries: &mut Vec, entry_name: &[u8]) -> Option<()> { + let index = entries.iter().position(|entry| entry.name == entry_name)?; + entries.remove(index); + Some(()) +} + +/// Builds a SHA1-signed native PHAR archive from decoded entries. +fn build_native_phar_archive( + entries: &[ArchiveEntry], + metadata: &[u8], + stub: &[u8], +) -> Option> { + let mut manifest = Vec::new(); let mut stored_entries = Vec::with_capacity(entries.len()); manifest.extend_from_slice(&u32::try_from(entries.len()).ok()?.to_le_bytes()); manifest.extend_from_slice(&[0x11, 0x00]); manifest.extend_from_slice(&PHAR_HDR_SIGNATURE.to_le_bytes()); manifest.extend_from_slice(&0u32.to_le_bytes()); - manifest.extend_from_slice(&0u32.to_le_bytes()); + // Global metadata field: length-prefixed serialized blob (empty when unset). + manifest.extend_from_slice(&u32::try_from(metadata.len()).ok()?.to_le_bytes()); + manifest.extend_from_slice(metadata); for entry in entries { let name_len = u32::try_from(entry.name.len()).ok()?; let payload_len = u32::try_from(entry.payload.len()).ok()?; @@ -715,12 +1341,18 @@ fn build_native_phar_archive(entries: &[ArchiveEntry]) -> Option> { manifest.extend_from_slice( &(PHAR_FILE_MODE_0644 | phar_compression_flag(entry.compression)).to_le_bytes(), ); - manifest.extend_from_slice(&0u32.to_le_bytes()); + // Per-entry metadata field: length-prefixed serialized blob (empty when unset). + manifest.extend_from_slice(&u32::try_from(entry.metadata.len()).ok()?.to_le_bytes()); + manifest.extend_from_slice(&entry.metadata); stored_entries.push(stored); } let mut out = Vec::new(); - out.extend_from_slice(b"\r\n"); + if stub.is_empty() { + out.extend_from_slice(PHAR_DEFAULT_STUB); + } else { + out.extend_from_slice(stub); + } out.extend_from_slice(&u32::try_from(manifest.len()).ok()?.to_le_bytes()); out.extend_from_slice(&manifest); for stored in stored_entries { @@ -769,44 +1401,98 @@ fn compression_from_php_constant(value: usize) -> Option { } /// Builds a POSIX ustar archive with stored regular-file entries. -fn build_tar_archive(entries: &[ArchiveEntry]) -> Option> { +fn build_tar_archive(entries: &[ArchiveEntry], metadata: &[u8], stub: &[u8]) -> Option> { let mut out = Vec::new(); + write_tar_body(&mut out, entries, metadata, stub)?; + out.extend_from_slice(&[0u8; 1024]); + Some(out) +} + +/// Writes a tar phar's data records (stub, global metadata, entries, and per-file +/// metadata side entries) into `out`, without the trailing zero blocks. The bytes +/// it produces are exactly the range a tar phar signature is computed over. +fn write_tar_body( + out: &mut Vec, + entries: &[ArchiveEntry], + metadata: &[u8], + stub: &[u8], +) -> Option<()> { + // Tar-based phars store the stub and global metadata as reserved `.phar/*` files. + if !stub.is_empty() { + write_tar_entry(out, PHAR_STUB_ENTRY, stub)?; + } + if !metadata.is_empty() { + write_tar_entry(out, PHAR_METADATA_ENTRY, metadata)?; + } for entry in entries { - let (name, prefix) = split_tar_name(&entry.name)?; - let mut header = [0u8; 512]; - header[..name.len()].copy_from_slice(name); - if let Some(prefix) = prefix { - header[345..345 + prefix.len()].copy_from_slice(prefix); - } - let mode = b"0000644\0"; - header[100..100 + mode.len()].copy_from_slice(mode); - let uid = b"0000000\0"; - header[108..108 + uid.len()].copy_from_slice(uid); - header[116..116 + uid.len()].copy_from_slice(uid); - let size = format!("{:011o}\0", entry.payload.len()); - header[124..124 + size.len()].copy_from_slice(size.as_bytes()); - let mtime = b"00000000000\0"; - header[136..136 + mtime.len()].copy_from_slice(mtime); - header[156] = b'0'; - header[257..263].copy_from_slice(b"ustar\0"); - header[263..265].copy_from_slice(b"00"); - for byte in &mut header[148..156] { - *byte = b' '; + write_tar_entry(out, &entry.name, &entry.payload)?; + } + // Per-file metadata rides in `.phar/.metadata//.metadata.bin` side entries. + for entry in entries { + if !entry.metadata.is_empty() { + write_tar_entry(out, &tar_file_metadata_name(&entry.name), &entry.metadata)?; } - let checksum: u32 = header.iter().map(|&byte| byte as u32).sum(); - let checksum = format!("{:06o}\0 ", checksum); - header[148..156].copy_from_slice(checksum.as_bytes()); - out.extend_from_slice(&header); - out.extend_from_slice(&entry.payload); - out.resize( - out.len() + round_up_to_512(entry.payload.len())? - entry.payload.len(), - 0, - ); } + Some(()) +} + +/// Rebuilds a tar phar with a PHP-compatible `.phar/signature.bin` trailer entry. +/// +/// The signature is computed over the data records (everything before the +/// signature entry's header), then the signature entry is appended as the last +/// record before the trailing zero blocks, matching php-src `phar_tar_flush`. +fn sign_tar_archive(archive: &Archive, flag: u32, key: Option<&[u8]>) -> Option> { + let mut out = Vec::new(); + write_tar_body(&mut out, &archive.entries, &archive.metadata, &archive.stub)?; + let sig = compute_signature(flag, key, &out)?; + write_tar_entry(&mut out, PHAR_SIGNATURE_ENTRY, &signature_bin_payload(flag, &sig)?)?; out.extend_from_slice(&[0u8; 1024]); Some(out) } +/// Builds the tar side-entry path that holds one file's serialized metadata. +fn tar_file_metadata_name(entry_name: &[u8]) -> Vec { + let mut name = Vec::with_capacity( + PHAR_FILE_METADATA_PREFIX.len() + entry_name.len() + PHAR_FILE_METADATA_SUFFIX.len(), + ); + name.extend_from_slice(PHAR_FILE_METADATA_PREFIX); + name.extend_from_slice(entry_name); + name.extend_from_slice(PHAR_FILE_METADATA_SUFFIX); + name +} + +/// Writes one uncompressed POSIX ustar entry (512-byte header + padded payload). +fn write_tar_entry(out: &mut Vec, entry_name: &[u8], payload: &[u8]) -> Option<()> { + let (name, prefix) = split_tar_name(entry_name)?; + let mut header = [0u8; 512]; + header[..name.len()].copy_from_slice(name); + if let Some(prefix) = prefix { + header[345..345 + prefix.len()].copy_from_slice(prefix); + } + let mode = b"0000644\0"; + header[100..100 + mode.len()].copy_from_slice(mode); + let uid = b"0000000\0"; + header[108..108 + uid.len()].copy_from_slice(uid); + header[116..116 + uid.len()].copy_from_slice(uid); + let size = format!("{:011o}\0", payload.len()); + header[124..124 + size.len()].copy_from_slice(size.as_bytes()); + let mtime = b"00000000000\0"; + header[136..136 + mtime.len()].copy_from_slice(mtime); + header[156] = b'0'; + header[257..263].copy_from_slice(b"ustar\0"); + header[263..265].copy_from_slice(b"00"); + for byte in &mut header[148..156] { + *byte = b' '; + } + let checksum: u32 = header.iter().map(|&byte| byte as u32).sum(); + let checksum = format!("{:06o}\0 ", checksum); + header[148..156].copy_from_slice(checksum.as_bytes()); + out.extend_from_slice(&header); + out.extend_from_slice(payload); + out.resize(out.len() + round_up_to_512(payload.len())? - payload.len(), 0); + Some(()) +} + /// Splits a tar entry path into ustar `name` and optional `prefix` fields. fn split_tar_name(name: &[u8]) -> Option<(&[u8], Option<&[u8]>)> { if name.len() <= 100 { @@ -826,65 +1512,259 @@ fn split_tar_name(name: &[u8]) -> Option<(&[u8], Option<&[u8]>)> { } /// Builds a ZIP archive with stored or deflated entries and central-directory records. -fn build_zip_archive(entries: &[ArchiveEntry]) -> Option> { +fn build_zip_archive(entries: &[ArchiveEntry], metadata: &[u8], stub: &[u8]) -> Option> { let mut out = Vec::new(); let mut central = Vec::new(); - for entry in entries { - let name_len = u16::try_from(entry.name.len()).ok()?; - let payload_len = u32::try_from(entry.payload.len()).ok()?; - let (method, stored) = encode_zip_payload(&entry.payload, entry.compression)?; - let stored_len = u32::try_from(stored.len()).ok()?; - let local_offset = u32::try_from(out.len()).ok()?; - let crc = crc32(&entry.payload); - - out.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); - out.extend_from_slice(&20u16.to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(&method.to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(&crc.to_le_bytes()); - out.extend_from_slice(&stored_len.to_le_bytes()); - out.extend_from_slice(&payload_len.to_le_bytes()); - out.extend_from_slice(&name_len.to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(&entry.name); - out.extend_from_slice(&stored); + let count = write_zip_body(&mut out, &mut central, entries, stub)?; + finalize_zip(&mut out, ¢ral, count, metadata)?; + Some(out) +} - central.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); - central.extend_from_slice(&20u16.to_le_bytes()); - central.extend_from_slice(&20u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&method.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&crc.to_le_bytes()); - central.extend_from_slice(&stored_len.to_le_bytes()); - central.extend_from_slice(&payload_len.to_le_bytes()); - central.extend_from_slice(&name_len.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u32.to_le_bytes()); - central.extend_from_slice(&local_offset.to_le_bytes()); - central.extend_from_slice(&entry.name); +/// Writes a zip phar's local file entries (stub plus regular entries) into `out` +/// and their central-directory records into `central`, returning the entry count. +/// When a zip password is set, every file entry — the stub included — is +/// ZipCrypto-encrypted; only the separately-written `.phar/signature.bin` stays +/// in the clear. +fn write_zip_body( + out: &mut Vec, + central: &mut Vec, + entries: &[ArchiveEntry], + stub: &[u8], +) -> Option { + let mut count = 0usize; + // Zip-based phars store the stub as the reserved `.phar/stub.php` entry. + if !stub.is_empty() { + write_zip_entry(out, central, PHAR_STUB_ENTRY, stub, PharCompression::None, &[], true)?; + count += 1; } - let central_offset = u32::try_from(out.len()).ok()?; - let central_len = u32::try_from(central.len()).ok()?; - let entry_count = u16::try_from(entries.len()).ok()?; - out.extend_from_slice(¢ral); + for entry in entries { + write_zip_entry( + out, + central, + &entry.name, + &entry.payload, + entry.compression, + &entry.metadata, + true, + )?; + count += 1; + } + Some(count) +} + +/// Appends the central directory and the end-of-central-directory record (with the +/// global metadata carried as the ZIP archive comment) to a zip phar under build. +fn finalize_zip(out: &mut Vec, central: &[u8], count: usize, metadata: &[u8]) -> Option<()> { + let central_offset = out.len(); + let central_len = central.len(); + // Zip-based phars store global metadata in the EOCD archive comment. + let comment_len = u16::try_from(metadata.len()).ok()?; + out.extend_from_slice(central); + + // Emit the ZIP64 EOCD record + locator when the entry count, central-directory + // size, or offset overflows the regular EOCD's 16-/32-bit fields. + let sentinel = ZIP32_SENTINEL as usize; + let needs_zip64 = + count >= ZIP16_SENTINEL as usize || central_offset > sentinel || central_len > sentinel; + if needs_zip64 { + let eocd64_offset = out.len() as u64; + // -- ZIP64 end-of-central-directory record -- + out.extend_from_slice(&0x0606_4b50u32.to_le_bytes()); + out.extend_from_slice(&44u64.to_le_bytes()); // size of the rest of this record + out.extend_from_slice(&45u16.to_le_bytes()); // version made by + out.extend_from_slice(&45u16.to_le_bytes()); // version needed to extract + out.extend_from_slice(&0u32.to_le_bytes()); // number of this disk + out.extend_from_slice(&0u32.to_le_bytes()); // disk with central directory + out.extend_from_slice(&(count as u64).to_le_bytes()); // entries on this disk + out.extend_from_slice(&(count as u64).to_le_bytes()); // total entries + out.extend_from_slice(&(central_len as u64).to_le_bytes()); + out.extend_from_slice(&(central_offset as u64).to_le_bytes()); + // -- ZIP64 end-of-central-directory locator -- + out.extend_from_slice(&0x0706_4b50u32.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); // disk with the ZIP64 EOCD + out.extend_from_slice(&eocd64_offset.to_le_bytes()); + out.extend_from_slice(&1u32.to_le_bytes()); // total number of disks + } + + // Regular EOCD, using the 0xFFFF / 0xFFFFFFFF sentinels for overflowed fields. + let entry_count = u16::try_from(count).unwrap_or(ZIP16_SENTINEL); + let cd_len = u32::try_from(central_len).unwrap_or(ZIP32_SENTINEL); + let cd_offset = u32::try_from(central_offset).unwrap_or(ZIP32_SENTINEL); out.extend_from_slice(&0x0605_4b50u32.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&entry_count.to_le_bytes()); out.extend_from_slice(&entry_count.to_le_bytes()); - out.extend_from_slice(¢ral_len.to_le_bytes()); - out.extend_from_slice(¢ral_offset.to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&cd_len.to_le_bytes()); + out.extend_from_slice(&cd_offset.to_le_bytes()); + out.extend_from_slice(&comment_len.to_le_bytes()); + out.extend_from_slice(metadata); + Some(()) +} + +/// Rebuilds a zip phar with a PHP-compatible `.phar/signature.bin` entry. +/// +/// php-src `phar_zip_applysignature` hashes the local file entries, the central +/// directory, and the archive comment — but not the EOCD — and then appends the +/// signature as the archive's last local entry and last central record. +fn sign_zip_archive(archive: &Archive, flag: u32, key: Option<&[u8]>) -> Option> { + let mut out = Vec::new(); + let mut central = Vec::new(); + let mut count = write_zip_body(&mut out, &mut central, &archive.entries, &archive.stub)?; + // Signed range: local entries ++ central records ++ comment, signature excluded. + let mut signed = out.clone(); + signed.extend_from_slice(¢ral); + signed.extend_from_slice(&archive.metadata); + let sig = compute_signature(flag, key, &signed)?; + // The signature entry stays in the clear — a verifier must read it without the + // password (and the signature already covers the encrypted local-entry bytes). + write_zip_entry( + &mut out, + &mut central, + PHAR_SIGNATURE_ENTRY, + &signature_bin_payload(flag, &sig)?, + PharCompression::None, + &[], + false, + )?; + count += 1; + finalize_zip(&mut out, ¢ral, count, &archive.metadata)?; Some(out) } +/// Writes one ZIP entry: its local file header + stored payload into `out`, and the +/// matching central-directory record into `central`. When `encrypt` is set and a zip +/// password is configured, the stored payload is ZipCrypto-encrypted and the +/// general-purpose "encrypted" flag is set in both headers. +fn write_zip_entry( + out: &mut Vec, + central: &mut Vec, + name: &[u8], + payload: &[u8], + compression: PharCompression, + metadata: &[u8], + encrypt: bool, +) -> Option<()> { + let name_len = u16::try_from(name.len()).ok()?; + let comment_len = u16::try_from(metadata.len()).ok()?; + let payload_len = payload.len(); + let (method, stored) = encode_zip_payload(payload, compression)?; + let local_offset = out.len(); + let crc = crc32(payload); + + // Encrypt the stored payload (traditional ZipCrypto) when requested and a zip + // password is set. The 12-byte header's check byte is the CRC's high byte, since + // no data descriptor is written — matching the read-side `zip_entry_crypto` + // branch. Encryption grows the stored size by 12 bytes and sets flag bit 0. + let password = if encrypt { current_zip_password() } else { None }; + let (stored, flags) = match password { + Some(pw) => ( + zipcrypto_encrypt(&pw, &stored, (crc >> 24) as u8), + ZIP_FLAG_ENCRYPTED, + ), + None => (stored, 0u16), + }; + let stored_len = stored.len(); + + // ZIP64 is needed when a size or the local-header offset overflows 32 bits. + let sentinel = ZIP32_SENTINEL as usize; + let zip64_sizes = stored_len > sentinel || payload_len > sentinel; + let zip64_offset = local_offset > sentinel; + let version: u16 = if zip64_sizes || zip64_offset { 45 } else { 20 }; + + // Local header: defers both sizes to a ZIP64 extra field once either overflows. + let local_csz = if zip64_sizes { ZIP32_SENTINEL } else { stored_len as u32 }; + let local_usz = if zip64_sizes { ZIP32_SENTINEL } else { payload_len as u32 }; + let local_extra = if zip64_sizes { + zip64_local_extra(payload_len as u64, stored_len as u64) + } else { + Vec::new() + }; + let local_extra_len = u16::try_from(local_extra.len()).ok()?; + + out.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); + out.extend_from_slice(&version.to_le_bytes()); + out.extend_from_slice(&flags.to_le_bytes()); + out.extend_from_slice(&method.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&crc.to_le_bytes()); + out.extend_from_slice(&local_csz.to_le_bytes()); + out.extend_from_slice(&local_usz.to_le_bytes()); + out.extend_from_slice(&name_len.to_le_bytes()); + out.extend_from_slice(&local_extra_len.to_le_bytes()); + out.extend_from_slice(name); + out.extend_from_slice(&local_extra); + out.extend_from_slice(&stored); + + // Central record: each overflowed field becomes a sentinel + a ZIP64 extra entry. + let cen_csz = if stored_len > sentinel { ZIP32_SENTINEL } else { stored_len as u32 }; + let cen_usz = if payload_len > sentinel { ZIP32_SENTINEL } else { payload_len as u32 }; + let cen_off = if zip64_offset { ZIP32_SENTINEL } else { local_offset as u32 }; + let cen_extra = if zip64_sizes || zip64_offset { + zip64_central_extra( + (payload_len > sentinel).then_some(payload_len as u64), + (stored_len > sentinel).then_some(stored_len as u64), + zip64_offset.then_some(local_offset as u64), + ) + } else { + Vec::new() + }; + let cen_extra_len = u16::try_from(cen_extra.len()).ok()?; + + central.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); + central.extend_from_slice(&version.to_le_bytes()); + central.extend_from_slice(&version.to_le_bytes()); + central.extend_from_slice(&flags.to_le_bytes()); + central.extend_from_slice(&method.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&crc.to_le_bytes()); + central.extend_from_slice(&cen_csz.to_le_bytes()); + central.extend_from_slice(&cen_usz.to_le_bytes()); + central.extend_from_slice(&name_len.to_le_bytes()); + central.extend_from_slice(&cen_extra_len.to_le_bytes()); + // File comment length: carries this entry's serialized per-file metadata. + central.extend_from_slice(&comment_len.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u32.to_le_bytes()); + central.extend_from_slice(&cen_off.to_le_bytes()); + central.extend_from_slice(name); + central.extend_from_slice(&cen_extra); + central.extend_from_slice(metadata); + Some(()) +} + +/// Builds a ZIP64 local-header extra field carrying the 64-bit uncompressed and +/// compressed sizes (tag 0x0001, both fields always present in local headers). +fn zip64_local_extra(uncompressed: u64, compressed: u64) -> Vec { + let mut out = Vec::with_capacity(20); + out.extend_from_slice(&ZIP64_EXTRA_TAG.to_le_bytes()); + out.extend_from_slice(&16u16.to_le_bytes()); + out.extend_from_slice(&uncompressed.to_le_bytes()); + out.extend_from_slice(&compressed.to_le_bytes()); + out +} + +/// Builds a ZIP64 central-directory extra field holding only the overflowed +/// fields, in APPNOTE order: uncompressed size, compressed size, header offset. +fn zip64_central_extra( + uncompressed: Option, + compressed: Option, + offset: Option, +) -> Vec { + let mut body = Vec::new(); + for field in [uncompressed, compressed, offset].into_iter().flatten() { + body.extend_from_slice(&field.to_le_bytes()); + } + let mut out = Vec::with_capacity(4 + body.len()); + out.extend_from_slice(&ZIP64_EXTRA_TAG.to_le_bytes()); + out.extend_from_slice(&(body.len() as u16).to_le_bytes()); + out.extend_from_slice(&body); + out +} + /// Encodes a ZIP entry payload and returns its ZIP compression method. fn encode_zip_payload(payload: &[u8], compression: PharCompression) -> Option<(u16, Vec)> { match compression { @@ -909,6 +1789,287 @@ fn append_sha1_signature(archive: &mut Vec) { archive.extend_from_slice(b"GBMB"); } +/// Returns the raw digest length for a PHP hash-based PHAR signature flag +/// (MD5=1, SHA1=2, SHA256=3, SHA512=4); `None` for non-hash flags. +fn signature_digest_len(flags: u32) -> Option { + match flags { + 1 => Some(16), + 2 => Some(20), + 3 => Some(32), + 4 => Some(64), + _ => None, + } +} + +/// Returns the archive bytes with any trailing PHP signature trailer removed +/// (native PHAR `digest ++ LE32(flag) ++ "GBMB"`, or the OpenSSL variant +/// `sig ++ LE32(sig_len) ++ LE32(0x10) ++ "GBMB"`). Returns the input unchanged +/// when no recognized trailer is present. +fn strip_signature_trailer(archive: &[u8]) -> &[u8] { + let n = archive.len(); + if n < 8 || &archive[n - 4..] != b"GBMB" { + return archive; + } + let flags = u32::from_le_bytes(archive[n - 8..n - 4].try_into().unwrap()); + if flags == PHAR_OPENSSL_SIGNATURE_TYPE { + if n >= 12 { + let sig_len = u32::from_le_bytes(archive[n - 12..n - 8].try_into().unwrap()) as usize; + if let Some(total) = sig_len.checked_add(12) { + if n >= total { + return &archive[..n - total]; + } + } + } + } else if let Some(dlen) = signature_digest_len(flags) { + let total = dlen + 8; + if n >= total { + return &archive[..n - total]; + } + } + archive +} + +/// Computes the PKCS#1 v1.5 RSA-SHA1 signature of `data` with a PEM private key +/// (PKCS#8 or PKCS#1), matching PHP's `openssl_sign(..., OPENSSL_ALGO_SHA1)`. +fn rsa_sha1_sign(data: &[u8], key_pem: &[u8]) -> Option> { + use rsa::pkcs1::DecodeRsaPrivateKey; + use rsa::pkcs8::DecodePrivateKey; + use rsa::{Pkcs1v15Sign, RsaPrivateKey}; + use sha1::{Digest, Sha1}; + + let pem = std::str::from_utf8(key_pem).ok()?; + let key = RsaPrivateKey::from_pkcs8_pem(pem) + .ok() + .or_else(|| RsaPrivateKey::from_pkcs1_pem(pem).ok())?; + let hashed = Sha1::digest(data); + key.sign(Pkcs1v15Sign::new::(), &hashed).ok() +} + +/// Computes a PHP-compatible signature over `data` for a signature `flag`: a raw +/// MD5/SHA1/SHA256/SHA512 digest (flags 1..=4) or an RSA-SHA1 OpenSSL signature +/// (flag 0x10, requiring the PEM `key`). Returns `None` for an unknown flag or a +/// missing/invalid key. +fn compute_signature(flag: u32, key: Option<&[u8]>, data: &[u8]) -> Option> { + use md5::Md5; + use sha1::{Digest, Sha1}; + use sha2::{Sha256, Sha512}; + + match flag { + 1 => Some(Md5::digest(data).to_vec()), + 2 => Some(Sha1::digest(data).to_vec()), + 3 => Some(Sha256::digest(data).to_vec()), + 4 => Some(Sha512::digest(data).to_vec()), + PHAR_OPENSSL_SIGNATURE_TYPE => rsa_sha1_sign(data, key?), + _ => None, + } +} + +/// Builds the `.phar/signature.bin` payload for a tar/zip phar: +/// `LE32(sig_flag) ++ LE32(sig_len) ++ signature`. +fn signature_bin_payload(flag: u32, sig: &[u8]) -> Option> { + let mut out = Vec::with_capacity(8 + sig.len()); + out.extend_from_slice(&flag.to_le_bytes()); + out.extend_from_slice(&u32::try_from(sig.len()).ok()?.to_le_bytes()); + out.extend_from_slice(sig); + Some(out) +} + +/// Detects the archive family of `data` for signature operations: zip (PK magic), +/// tar (ustar magic at offset 257), or native PHAR (default). Returns `None` for a +/// gzip/bzip2-wrapped archive, where signature rewriting is not supported. +fn signing_format(data: &[u8]) -> Option { + if data.starts_with(&[0x50, 0x4b, 0x03, 0x04]) || data.starts_with(&[0x50, 0x4b, 0x05, 0x06]) { + Some(ArchiveFormat::Zip) + } else if data.get(257..262) == Some(b"ustar") { + Some(ArchiveFormat::Tar) + } else if data.starts_with(&[0x1f, 0x8b]) || data.starts_with(b"BZh") { + None + } else { + Some(ArchiveFormat::NativePhar) + } +} + +/// Re-signs the phar at `path` with an OpenSSL (RSA-SHA1) signature. Native PHARs +/// gain a `sig ++ LE32(sig_len) ++ LE32(0x10) ++ "GBMB"` trailer; tar/zip phars +/// gain a `.phar/signature.bin` entry. The caller-supplied public key is what +/// verifiers use; PHP does not auto-write a `.pubkey` here either. +fn sign_archive_openssl(path: &[u8], key_pem: &[u8]) -> Option<()> { + let data = read_path(path)?; + match signing_format(&data)? { + ArchiveFormat::Zip => { + let signed = + sign_zip_archive(&parse_zip_archive(&data)?, PHAR_OPENSSL_SIGNATURE_TYPE, Some(key_pem))?; + write_path(path, &signed) + } + ArchiveFormat::Tar => { + let signed = + sign_tar_archive(&parse_tar_archive(&data)?, PHAR_OPENSSL_SIGNATURE_TYPE, Some(key_pem))?; + write_path(path, &signed) + } + ArchiveFormat::NativePhar => { + let mut out = strip_signature_trailer(&data).to_vec(); + let sig = rsa_sha1_sign(&out, key_pem)?; + out.extend_from_slice(&sig); + out.extend_from_slice(&u32::try_from(sig.len()).ok()?.to_le_bytes()); + out.extend_from_slice(&PHAR_OPENSSL_SIGNATURE_TYPE.to_le_bytes()); + out.extend_from_slice(b"GBMB"); + write_path(path, &out) + } + } +} + +/// Re-signs the phar at `path` with a hash-based signature (MD5/SHA1/SHA256/SHA512 +/// per `algo` 1..=4). Native PHARs append `digest ++ LE32(algo) ++ "GBMB"`; tar/zip +/// phars gain a `.phar/signature.bin` entry. +fn sign_archive_hash(path: &[u8], algo: u32) -> Option<()> { + let data = read_path(path)?; + match signing_format(&data)? { + ArchiveFormat::Zip => { + let signed = sign_zip_archive(&parse_zip_archive(&data)?, algo, None)?; + write_path(path, &signed) + } + ArchiveFormat::Tar => { + let signed = sign_tar_archive(&parse_tar_archive(&data)?, algo, None)?; + write_path(path, &signed) + } + ArchiveFormat::NativePhar => { + let mut out = strip_signature_trailer(&data).to_vec(); + let digest = compute_signature(algo, None, &out)?; + out.extend_from_slice(&digest); + out.extend_from_slice(&algo.to_le_bytes()); + out.extend_from_slice(b"GBMB"); + write_path(path, &out) + } + } +} + +/// Decodes a tar/zip `.phar/signature.bin` payload into its flag and signature +/// bytes (`LE32(flag) ++ LE32(len) ++ signature`). +fn parse_signature_bin(payload: &[u8]) -> Option<(u32, Vec)> { + let flag = le32(payload, 0)?; + let len = le32(payload, 4)? as usize; + Some((flag, payload.get(8..8usize.checked_add(len)?)?.to_vec())) +} + +/// Returns the raw `.phar/signature.bin` payload from a tar phar, if present. +fn read_tar_signature(data: &[u8]) -> Option> { + let mut p = 0usize; + while p.checked_add(512)? <= data.len() { + let header = &data[p..p + 512]; + if header.iter().all(|&b| b == 0) { + break; + } + let size = parse_tar_octal(&header[124..136])?; + let payload_start = p.checked_add(512)?; + let typeflag = header[156]; + if (typeflag == 0 || typeflag == b'0') && tar_entry_name(header)? == PHAR_SIGNATURE_ENTRY { + return data + .get(payload_start..payload_start.checked_add(size)?) + .map(<[u8]>::to_vec); + } + p = payload_start.checked_add(round_up_to_512(size)?)?; + } + None +} + +/// Returns the raw `.phar/signature.bin` payload from a zip phar, if present. +fn read_zip_signature(data: &[u8]) -> Option> { + let (entry_count, central_dir_offset) = zip_eocd_info(data)?; + let mut p = central_dir_offset; + for _ in 0..entry_count { + if le32(data, p)? != 0x0201_4b50 { + return None; + } + let method = le16(data, p + 10)?; + let mut compressed_size = le32(data, p + 20)? as usize; + let mut uncompressed_size = le32(data, p + 24)? as usize; + let name_len = le16(data, p + 28)? as usize; + let extra_len = le16(data, p + 30)? as usize; + let comment_len = le16(data, p + 32)? as usize; + let mut local_offset = le32(data, p + 42)? as usize; + let name_start = p + 46; + let name = data.get(name_start..name_start.checked_add(name_len)?)?; + if name == PHAR_SIGNATURE_ENTRY { + apply_zip64_central_extra( + data, + name_start.checked_add(name_len)?, + extra_len, + &mut uncompressed_size, + &mut compressed_size, + &mut local_offset, + )?; + // The reserved signature entry is never encrypted. + return decode_zip_local_entry( + data, + local_offset, + method, + compressed_size, + uncompressed_size, + false, + 0, + ); + } + p = name_start + .checked_add(name_len)? + .checked_add(extra_len)? + .checked_add(comment_len)?; + } + None +} + +/// Reads the signature of the phar at `path`, returning the flag and the raw +/// signature/digest bytes. Native PHARs use the `GBMB` trailer; tar/zip phars use +/// the `.phar/signature.bin` entry. +fn read_signature_info(path: &[u8]) -> Option<(u32, Vec)> { + let data = read_path(path)?; + match signing_format(&data)? { + ArchiveFormat::Zip => parse_signature_bin(&read_zip_signature(&data)?), + ArchiveFormat::Tar => parse_signature_bin(&read_tar_signature(&data)?), + ArchiveFormat::NativePhar => { + let n = data.len(); + if n < 8 || &data[n - 4..] != b"GBMB" { + return None; + } + let flags = u32::from_le_bytes(data[n - 8..n - 4].try_into().unwrap()); + if flags == PHAR_OPENSSL_SIGNATURE_TYPE { + let sig_len = + u32::from_le_bytes(data.get(n - 12..n - 8)?.try_into().unwrap()) as usize; + let start = n.checked_sub(12)?.checked_sub(sig_len)?; + Some((flags, data.get(start..n - 12)?.to_vec())) + } else { + let dlen = signature_digest_len(flags)?; + let start = n.checked_sub(8)?.checked_sub(dlen)?; + Some((flags, data.get(start..n - 8)?.to_vec())) + } + } + } +} + +/// Returns the uppercase hex of the PHAR's signature/digest bytes (PHP +/// `Phar::getSignature()['hash']`). +fn signature_hash_hex(path: &[u8]) -> Option> { + let (_, bytes) = read_signature_info(path)?; + let mut hex = Vec::with_capacity(bytes.len() * 2); + for byte in bytes { + hex.extend_from_slice(format!("{byte:02X}").as_bytes()); + } + Some(hex) +} + +/// Returns the PHP signature type name for the PHAR (`getSignature()['hash_type']`). +fn signature_type_name(path: &[u8]) -> Option> { + let (flags, _) = read_signature_info(path)?; + let name: &[u8] = match flags { + 1 => b"MD5", + 2 => b"SHA-1", + 3 => b"SHA-256", + 4 => b"SHA-512", + PHAR_OPENSSL_SIGNATURE_TYPE => b"OpenSSL", + _ => return None, + }; + Some(name.to_vec()) +} + /// Computes PHP-compatible reflected CRC32 for a PHAR entry payload. fn crc32(bytes: &[u8]) -> u32 { let mut crc = 0xffff_ffffu32; @@ -924,55 +2085,89 @@ fn crc32(bytes: &[u8]) -> u32 { /// Parses a ZIP archive central directory and returns a store/deflate entry. fn parse_zip_entry(data: &[u8], entry: &[u8]) -> Option> { - parse_zip_entries(data)? + parse_zip_archive(data)? + .entries .into_iter() .find(|candidate| candidate.name == entry) .map(|candidate| candidate.payload) } -/// Parses a ZIP archive central directory and returns every supported entry. -fn parse_zip_entries(data: &[u8]) -> Option> { +/// Parses a zip-based phar into entries plus its global metadata and stub. +/// +/// Global metadata is read from the EOCD archive comment; the reserved +/// `.phar/stub.php` entry becomes the stub and other `.phar/*` control entries are +/// hidden from the entry listing. +fn parse_zip_archive(data: &[u8]) -> Option { let eocd = find_zip_eocd(data)?; - let entry_count = le16(data, eocd + 10)? as usize; - let central_dir_offset = le32(data, eocd + 16)? as usize; - let mut entries = Vec::with_capacity(entry_count); + let (entry_count, central_dir_offset) = zip_eocd_info(data)?; + let comment_len = le16(data, eocd + 20)? as usize; + let comment_start = eocd.checked_add(22)?; + let metadata = data + .get(comment_start..comment_start.checked_add(comment_len)?)? + .to_vec(); + let mut entries = Vec::with_capacity(entry_count.min(1 << 16)); + let mut stub = Vec::new(); let mut p = central_dir_offset; for _ in 0..entry_count { if le32(data, p)? != 0x0201_4b50 { return None; } - let flags = le16(data, p + 8)?; - if flags & ZIP_FLAG_DATA_DESCRIPTOR != 0 { - return None; - } + // A data-descriptor entry (general-purpose flag bit 3) carries zeroed + // CRC/sizes in its local header and the real values in the central + // directory we are already reading here, so it needs no special handling + // beyond trusting these central-directory sizes. let method = le16(data, p + 10)?; - let compressed_size = le32(data, p + 20)? as usize; - let uncompressed_size = le32(data, p + 24)? as usize; + let mut compressed_size = le32(data, p + 20)? as usize; + let mut uncompressed_size = le32(data, p + 24)? as usize; let name_len = le16(data, p + 28)? as usize; let extra_len = le16(data, p + 30)? as usize; - let comment_len = le16(data, p + 32)? as usize; - let local_offset = le32(data, p + 42)? as usize; + let entry_comment_len = le16(data, p + 32)? as usize; + let mut local_offset = le32(data, p + 42)? as usize; let name_start = p + 46; let name = data.get(name_start..name_start.checked_add(name_len)?)?; + // ZIP64: sentinel size/offset fields defer to the central record's extra. + apply_zip64_central_extra( + data, + name_start.checked_add(name_len)?, + extra_len, + &mut uncompressed_size, + &mut compressed_size, + &mut local_offset, + )?; + let (encrypted, check_byte) = zip_entry_crypto(data, p)?; let payload = decode_zip_local_entry( data, local_offset, method, compressed_size, uncompressed_size, + encrypted, + check_byte, )?; - let compression = zip_compression_from_method(method)?; - entries.push(ArchiveEntry { - name: name.to_vec(), - payload, - compression, - }); - p = name_start - .checked_add(name_len)? - .checked_add(extra_len)? - .checked_add(comment_len)?; + let comment_start = name_start.checked_add(name_len)?.checked_add(extra_len)?; + if name == PHAR_STUB_ENTRY { + stub = payload; + } else if !is_phar_control_entry(name) { + let compression = zip_compression_from_method(method)?; + // Per-file metadata rides in the central-directory file comment. + let entry_metadata = data + .get(comment_start..comment_start.checked_add(entry_comment_len)?)? + .to_vec(); + entries.push(ArchiveEntry { + name: name.to_vec(), + payload, + compression, + metadata: entry_metadata, + }); + } + p = comment_start.checked_add(entry_comment_len)?; } - Some(entries) + Some(Archive { + entries, + format: ArchiveFormat::Zip, + metadata, + stub, + }) } /// Maps supported ZIP methods to the bridge's compression representation. @@ -995,13 +2190,111 @@ fn find_zip_eocd(data: &[u8]) -> Option { .find(|&i| data.get(i..i + 4) == Some(&[0x50, 0x4b, 0x05, 0x06])) } +/// Returns a ZIP archive's `(total entry count, central-directory offset)`, +/// transparently following the ZIP64 EOCD record when the regular EOCD uses +/// sentinels for an entry count, central-directory size, or offset that overflows +/// its 32-/16-bit field. +fn zip_eocd_info(data: &[u8]) -> Option<(usize, usize)> { + let eocd = find_zip_eocd(data)?; + let mut entry_count = le16(data, eocd + 10)? as usize; + let cd_size = le32(data, eocd + 12)?; + let mut cd_offset = le32(data, eocd + 16)? as usize; + let needs_zip64 = le16(data, eocd + 10)? == ZIP16_SENTINEL + || cd_size == ZIP32_SENTINEL + || cd_offset as u32 == ZIP32_SENTINEL; + if needs_zip64 { + if let Some((count, offset)) = read_zip64_eocd(data, eocd) { + entry_count = count; + cd_offset = offset; + } + } + Some((entry_count, cd_offset)) +} + +/// Reads the ZIP64 end-of-central-directory record (located via the 20-byte +/// locator immediately before the regular EOCD), returning its 64-bit total entry +/// count and central-directory offset. +fn read_zip64_eocd(data: &[u8], eocd: usize) -> Option<(usize, usize)> { + let locator = eocd.checked_sub(20)?; + if le32(data, locator)? != 0x0706_4b50 { + return None; + } + let eocd64 = le64(data, locator + 8)? as usize; + if le32(data, eocd64)? != 0x0606_4b50 { + return None; + } + let total_entries = le64(data, eocd64 + 32)? as usize; + let cd_offset = le64(data, eocd64 + 48)? as usize; + Some((total_entries, cd_offset)) +} + +/// Overrides any sentinel (`0xFFFFFFFF`) compressed size, uncompressed size, or +/// local-header offset of a ZIP central record with the 64-bit value from its +/// ZIP64 extra field (tag 0x0001). The extra field lists only the overflowed +/// fields, in the fixed order: original size, compressed size, header offset. +fn apply_zip64_central_extra( + data: &[u8], + extra_start: usize, + extra_len: usize, + uncompressed: &mut usize, + compressed: &mut usize, + local_offset: &mut usize, +) -> Option<()> { + let end = extra_start.checked_add(extra_len)?; + let mut p = extra_start; + while p.checked_add(4)? <= end { + let tag = le16(data, p)?; + let size = le16(data, p + 2)? as usize; + let body = p + 4; + if tag == ZIP64_EXTRA_TAG { + let mut q = body; + if *uncompressed as u32 == ZIP32_SENTINEL { + *uncompressed = le64(data, q)? as usize; + q += 8; + } + if *compressed as u32 == ZIP32_SENTINEL { + *compressed = le64(data, q)? as usize; + q += 8; + } + if *local_offset as u32 == ZIP32_SENTINEL { + *local_offset = le64(data, q)? as usize; + } + return Some(()); + } + p = body.checked_add(size)?; + } + Some(()) +} + +/// Reads a ZIP central record's encryption state: whether the entry is ZipCrypto +/// encrypted (flag bit 0) and the password check byte (the high byte of the mod +/// time for data-descriptor entries, otherwise of the CRC). +fn zip_entry_crypto(data: &[u8], central_off: usize) -> Option<(bool, u8)> { + let flags = le16(data, central_off + 8)?; + let encrypted = flags & ZIP_FLAG_ENCRYPTED != 0; + let check_byte = if flags & ZIP_FLAG_DATA_DESCRIPTOR != 0 { + (le16(data, central_off + 12)? >> 8) as u8 + } else { + (le32(data, central_off + 16)? >> 24) as u8 + }; + Some((encrypted, check_byte)) +} + /// Decodes a ZIP local file payload using sizes from its central directory. +/// +/// `encrypted` marks a traditional-PKWARE (ZipCrypto) entry; `check_byte` is the +/// expected last byte of its 12-byte encryption header used to reject a wrong +/// password. Encrypted entries require a password set via +/// [`elephc_phar_set_zip_password`]; without one (or with the wrong one) they +/// return `None`. fn decode_zip_local_entry( data: &[u8], local_offset: usize, method: u16, compressed_size: usize, uncompressed_size: usize, + encrypted: bool, + check_byte: u8, ) -> Option> { if le32(data, local_offset)? != 0x0403_4b50 { return None; @@ -1013,11 +2306,21 @@ fn decode_zip_local_entry( .checked_add(local_name_len)? .checked_add(local_extra_len)?; let stored = data.get(payload_start..payload_start.checked_add(compressed_size)?)?; + // Traditional ZipCrypto entries carry a 12-byte encryption header that the + // password-derived keystream removes before the (optionally deflated) payload. + let decrypted; + let body: &[u8] = if encrypted { + let password = current_zip_password()?; + decrypted = zipcrypto_decrypt(&password, stored, check_byte)?; + &decrypted + } else { + stored + }; match method { - ZIP_METHOD_STORE => Some(stored.to_vec()), + ZIP_METHOD_STORE => Some(body.to_vec()), ZIP_METHOD_DEFLATE => { let mut out = Vec::with_capacity(uncompressed_size); - let mut decoder = flate2::read::DeflateDecoder::new(stored); + let mut decoder = flate2::read::DeflateDecoder::new(body); decoder.read_to_end(&mut out).ok()?; (out.len() == uncompressed_size).then_some(out) } @@ -1025,38 +2328,225 @@ fn decode_zip_local_entry( } } +/// Traditional-PKWARE (ZipCrypto) cipher state: three 32-bit keys advanced per +/// plaintext byte. Drives both reading and writing of encrypted entries. +/// Cryptographically weak — kept only for compatibility with legacy ZipCrypto +/// archives, not as a real confidentiality mechanism. +struct ZipCryptoKeys { + k0: u32, + k1: u32, + k2: u32, +} + +impl ZipCryptoKeys { + /// Seeds the keys from the password (PKWARE's fixed initial constants). + fn new(password: &[u8]) -> Self { + let mut keys = Self { + k0: 0x1234_5678, + k1: 0x2345_6789, + k2: 0x3456_7890, + }; + for &byte in password { + keys.update(byte); + } + keys + } + + /// Advances the three keys with one plaintext byte. + fn update(&mut self, byte: u8) { + self.k0 = crc32_byte(self.k0, byte); + self.k1 = self.k1.wrapping_add(self.k0 & 0xff); + self.k1 = self.k1.wrapping_mul(134_775_813).wrapping_add(1); + self.k2 = crc32_byte(self.k2, (self.k1 >> 24) as u8); + } + + /// Returns the next keystream byte (derived from `k2`). + fn keystream(&self) -> u8 { + let temp = (self.k2 | 2) & 0xffff; + ((temp.wrapping_mul(temp ^ 1)) >> 8) as u8 + } + + /// Decrypts one ciphertext byte and advances the keys with the plaintext. + fn decrypt(&mut self, cipher: u8) -> u8 { + let plain = cipher ^ self.keystream(); + self.update(plain); + plain + } + + /// Encrypts one plaintext byte and advances the keys with that plaintext. + fn encrypt(&mut self, plain: u8) -> u8 { + let cipher = plain ^ self.keystream(); + self.update(plain); + cipher + } +} + +/// One-byte CRC32 step (poly 0xEDB88320) used by the ZipCrypto key schedule. +fn crc32_byte(crc: u32, byte: u8) -> u32 { + let mut t = (crc ^ byte as u32) & 0xff; + for _ in 0..8 { + t = if t & 1 != 0 { (t >> 1) ^ 0xedb8_8320 } else { t >> 1 }; + } + (crc >> 8) ^ t +} + +/// Decrypts a ZipCrypto entry payload (12-byte header + ciphertext) with +/// `password`, returning the post-header plaintext. Returns `None` when the data +/// is too short or the header's check byte rejects the password. +fn zipcrypto_decrypt(password: &[u8], data: &[u8], check_byte: u8) -> Option> { + if data.len() < 12 { + return None; + } + let mut keys = ZipCryptoKeys::new(password); + let mut header_last = 0u8; + for &byte in &data[..12] { + header_last = keys.decrypt(byte); + } + if header_last != check_byte { + return None; + } + Some(data[12..].iter().map(|&c| keys.decrypt(c)).collect()) +} + +/// Encrypts `data` as a traditional-PKWARE (ZipCrypto) entry payload with +/// `password`: prepends a 12-byte encryption header (11 pseudo-random filler bytes +/// plus `check_byte` at index 11) and returns the encrypted `header ++ data`, which +/// is 12 bytes longer than `data`. `check_byte` must be the byte the reader will +/// verify (the CRC's high byte when no data descriptor is used). The first 11 header +/// bytes are never read back, so their randomness affects only resistance to attack, +/// not round-trip correctness. +fn zipcrypto_encrypt(password: &[u8], data: &[u8], check_byte: u8) -> Vec { + let mut header = [0u8; 12]; + header[..11].copy_from_slice(&zipcrypto_header_filler()); + header[11] = check_byte; + let mut keys = ZipCryptoKeys::new(password); + let mut out = Vec::with_capacity(data.len() + 12); + for &plain in header.iter().chain(data) { + out.push(keys.encrypt(plain)); + } + out +} + +/// Produces 11 non-constant filler bytes for a ZipCrypto encryption header, mixing +/// a per-call atomic nonce with the current time through an xorshift64* step. +/// Dependency-free; only needs to avoid an all-constant header, since the bytes are +/// discarded on read. +fn zipcrypto_header_filler() -> [u8; 11] { + static NONCE: AtomicU64 = AtomicU64::new(0); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0); + let mut state = now ^ NONCE.fetch_add(1, Ordering::Relaxed).wrapping_mul(0x9E37_79B9_7F4A_7C15); + let mut filler = [0u8; 11]; + for byte in filler.iter_mut() { + // xorshift64* advance, then take a high byte of the scrambled state. + state ^= state >> 12; + state ^= state << 25; + state ^= state >> 27; + *byte = (state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 33) as u8; + } + filler +} + +/// Returns the password currently set for reading and writing encrypted ZIP +/// entries, if any. +fn current_zip_password() -> Option> { + ZIP_PASSWORD.with(|slot| slot.borrow().clone()) +} + +/// Sets (or, when empty, clears) the password used to read and write encrypted +/// ZIP entries. +fn set_zip_password(password: &[u8]) { + ZIP_PASSWORD.with(|slot| { + *slot.borrow_mut() = if password.is_empty() { + None + } else { + Some(password.to_vec()) + }; + }); +} + /// Parses a POSIX tar archive and returns a regular-file entry. fn parse_tar_entry(data: &[u8], entry: &[u8]) -> Option> { - parse_tar_entries(data)? + parse_tar_archive(data)? + .entries .into_iter() .find(|candidate| candidate.name == entry) .map(|candidate| candidate.payload) } -/// Parses a POSIX tar archive and returns regular-file entries. -fn parse_tar_entries(data: &[u8]) -> Option> { +/// Parses a tar-based phar into regular entries plus its global metadata and stub. +/// +/// The reserved `.phar/stub.php` and `.phar/.metadata.bin` files become the stub and +/// metadata; any other `.phar/*` control file is hidden from the entry listing. +fn parse_tar_archive(data: &[u8]) -> Option { let mut p = 0usize; let mut entries = Vec::new(); + let mut metadata = Vec::new(); + let mut stub = Vec::new(); + // Per-file metadata side entries may appear after their target entry; collect + // them and attach once the full entry list is known. + let mut file_metadata: Vec<(Vec, Vec)> = Vec::new(); + let mut first_header = true; while p.checked_add(512)? <= data.len() { let header = &data[p..p + 512]; if header.iter().all(|&b| b == 0) { - return Some(entries); + break; } + // Require the POSIX ustar magic on the first record so non-tar inputs + // (e.g. native PHARs whose stub contains `__HALT_COMPILER();`) are rejected + // rather than mis-parsed as tar. + if first_header && header.get(257..262) != Some(b"ustar") { + return None; + } + first_header = false; let size = parse_tar_octal(&header[124..136])?; let payload_start = p.checked_add(512)?; let payload_end = payload_start.checked_add(size)?; let payload = data.get(payload_start..payload_end)?; let typeflag = header[156]; if typeflag == 0 || typeflag == b'0' { - entries.push(ArchiveEntry { - name: tar_entry_name(header)?, - payload: payload.to_vec(), - compression: PharCompression::None, - }); + let name = tar_entry_name(header)?; + if name == PHAR_STUB_ENTRY { + stub = payload.to_vec(); + } else if name == PHAR_METADATA_ENTRY { + metadata = payload.to_vec(); + } else if let Some(target) = tar_file_metadata_target(&name) { + file_metadata.push((target, payload.to_vec())); + } else if !is_phar_control_entry(&name) { + entries.push(ArchiveEntry { + name, + payload: payload.to_vec(), + compression: PharCompression::None, + metadata: Vec::new(), + }); + } } p = payload_start.checked_add(round_up_to_512(size)?)?; } - Some(entries) + for (target, meta) in file_metadata { + if let Some(entry) = entries.iter_mut().find(|e| e.name == target) { + entry.metadata = meta; + } + } + Some(Archive { + entries, + format: ArchiveFormat::Tar, + metadata, + stub, + }) +} + +/// If `name` is a `.phar/.metadata//.metadata.bin` side entry, returns the +/// target entry path ``; otherwise returns `None`. +fn tar_file_metadata_target(name: &[u8]) -> Option> { + let rest = name.strip_prefix(PHAR_FILE_METADATA_PREFIX)?; + let inner = rest.strip_suffix(PHAR_FILE_METADATA_SUFFIX)?; + if inner.is_empty() { + return None; + } + Some(inner.to_vec()) } /// Builds the full tar path from the `prefix` and `name` header fields. @@ -1121,6 +2611,12 @@ fn le32(data: &[u8], off: usize) -> Option { Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) } +/// Reads a little-endian `u64` from `data` (used for ZIP64 fields). +fn le64(data: &[u8], off: usize) -> Option { + let b = data.get(off..off + 8)?; + Some(u64::from_le_bytes(b.try_into().ok()?)) +} + /// Returns the offset of `needle` in `hay`. fn find_subslice(hay: &[u8], needle: &[u8]) -> Option { if needle.is_empty() || hay.len() < needle.len() { @@ -1196,136 +2692,557 @@ mod tests { .map(|entry| entry.payload.as_slice()) } - /// Builds the serialized entry-name format returned by `entry_names_bytes`. - fn serialized_names(names: &[&str]) -> Vec { - let mut out = Vec::new(); - for name in names { - out.extend_from_slice(&(name.len() as u64).to_le_bytes()); - out.extend_from_slice(name.as_bytes()); - } - out + /// Builds the serialized entry-name format returned by `entry_names_bytes`. + fn serialized_names(names: &[&str]) -> Vec { + let mut out = Vec::new(); + for name in names { + out.extend_from_slice(&(name.len() as u64).to_le_bytes()); + out.extend_from_slice(name.as_bytes()); + } + out + } + + /// Builds a small tar archive with regular-file entries. + fn build_tar(entries: &[(&str, &[u8])]) -> Vec { + let mut out = Vec::new(); + for (name, content) in entries { + let mut header = [0u8; 512]; + header[..name.len()].copy_from_slice(name.as_bytes()); + let size = format!("{:011o}\0", content.len()); + header[124..124 + size.len()].copy_from_slice(size.as_bytes()); + header[156] = b'0'; + header[257..263].copy_from_slice(b"ustar\0"); + header[263..265].copy_from_slice(b"00"); + for byte in &mut header[148..156] { + *byte = b' '; + } + let checksum: u32 = header.iter().map(|&b| b as u32).sum(); + let checksum = format!("{:06o}\0 ", checksum); + header[148..156].copy_from_slice(checksum.as_bytes()); + out.extend_from_slice(&header); + out.extend_from_slice(content); + out.resize(out.len() + round_up_to_512(content.len()).unwrap() - content.len(), 0); + } + out.extend_from_slice(&[0u8; 1024]); + out + } + + /// Builds a ZIP archive with central-directory records. + fn build_zip(entries: &[(&str, &[u8], bool)]) -> Vec { + let mut out = Vec::new(); + let mut central = Vec::new(); + for (name, content, deflate) in entries { + let local_offset = out.len() as u32; + let stored = if *deflate { + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(content).unwrap(); + encoder.finish().unwrap() + } else { + content.to_vec() + }; + let method = if *deflate { ZIP_METHOD_DEFLATE } else { ZIP_METHOD_STORE }; + out.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); + out.extend_from_slice(&20u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&method.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&(stored.len() as u32).to_le_bytes()); + out.extend_from_slice(&(content.len() as u32).to_le_bytes()); + out.extend_from_slice(&(name.len() as u16).to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(name.as_bytes()); + out.extend_from_slice(&stored); + + central.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); + central.extend_from_slice(&20u16.to_le_bytes()); + central.extend_from_slice(&20u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&method.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u32.to_le_bytes()); + central.extend_from_slice(&(stored.len() as u32).to_le_bytes()); + central.extend_from_slice(&(content.len() as u32).to_le_bytes()); + central.extend_from_slice(&(name.len() as u16).to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u32.to_le_bytes()); + central.extend_from_slice(&local_offset.to_le_bytes()); + central.extend_from_slice(name.as_bytes()); + } + let central_offset = out.len() as u32; + out.extend_from_slice(¢ral); + out.extend_from_slice(&0x0605_4b50u32.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&(entries.len() as u16).to_le_bytes()); + out.extend_from_slice(&(entries.len() as u16).to_le_bytes()); + out.extend_from_slice(&(central.len() as u32).to_le_bytes()); + out.extend_from_slice(¢ral_offset.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out + } + + /// Verifies native PHAR manifest extraction. + #[test] + fn extracts_native_phar_entry() { + let archive = build_native_phar(&[("a.txt", b"alpha"), ("dir/b.txt", b"bravo")]); + assert_eq!( + extract_entry_bytes(&archive, b"dir/b.txt").as_deref(), + Some(&b"bravo"[..]) + ); + } + + /// Verifies tar container extraction. + #[test] + fn extracts_tar_entry() { + let archive = build_tar(&[("a.txt", b"alpha"), ("dir/b.txt", b"bravo")]); + assert_eq!( + extract_entry_bytes(&archive, b"dir/b.txt").as_deref(), + Some(&b"bravo"[..]) + ); + } + + /// Verifies ZIP store and deflate extraction. + #[test] + fn extracts_zip_entries() { + let archive = build_zip(&[ + ("plain.txt", b"stored", false), + ("deflated.txt", b"deflated payload", true), + ]); + assert_eq!( + extract_entry_bytes(&archive, b"plain.txt").as_deref(), + Some(&b"stored"[..]) + ); + assert_eq!( + extract_entry_bytes(&archive, b"deflated.txt").as_deref(), + Some(&b"deflated payload"[..]) + ); + } + + /// Builds a single-entry ZIP whose local header uses a streaming data + /// descriptor (general-purpose flag bit 3): the local CRC/size fields are + /// zero, the real values live in a trailing data descriptor, and the central + /// directory carries the authoritative sizes. + fn build_zip_with_data_descriptor(name: &str, content: &[u8], deflate: bool) -> Vec { + let stored = if deflate { + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(content).unwrap(); + encoder.finish().unwrap() + } else { + content.to_vec() + }; + let method = if deflate { ZIP_METHOD_DEFLATE } else { ZIP_METHOD_STORE }; + let crc = crc32(content); + let comp = stored.len() as u32; + let uncomp = content.len() as u32; + let mut out = Vec::new(); + // -- local file header: zeroed sizes, data-descriptor flag set -- + out.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); + out.extend_from_slice(&20u16.to_le_bytes()); + out.extend_from_slice(&0x0008u16.to_le_bytes()); + out.extend_from_slice(&method.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&(name.len() as u16).to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(name.as_bytes()); + out.extend_from_slice(&stored); + // -- trailing data descriptor carrying the real crc/sizes -- + out.extend_from_slice(&0x0807_4b50u32.to_le_bytes()); + out.extend_from_slice(&crc.to_le_bytes()); + out.extend_from_slice(&comp.to_le_bytes()); + out.extend_from_slice(&uncomp.to_le_bytes()); + // -- central directory with authoritative sizes -- + let central_offset = out.len() as u32; + let mut central = Vec::new(); + central.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); + central.extend_from_slice(&20u16.to_le_bytes()); + central.extend_from_slice(&20u16.to_le_bytes()); + central.extend_from_slice(&0x0008u16.to_le_bytes()); + central.extend_from_slice(&method.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&crc.to_le_bytes()); + central.extend_from_slice(&comp.to_le_bytes()); + central.extend_from_slice(&uncomp.to_le_bytes()); + central.extend_from_slice(&(name.len() as u16).to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u32.to_le_bytes()); + central.extend_from_slice(&0u32.to_le_bytes()); + central.extend_from_slice(name.as_bytes()); + out.extend_from_slice(¢ral); + // -- end of central directory -- + out.extend_from_slice(&0x0605_4b50u32.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&1u16.to_le_bytes()); + out.extend_from_slice(&1u16.to_le_bytes()); + out.extend_from_slice(&(central.len() as u32).to_le_bytes()); + out.extend_from_slice(¢ral_offset.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out + } + + /// Verifies a ZIP entry written with a streaming data descriptor (flag bit 3) + /// is read via the authoritative central-directory sizes instead of rejected, + /// for both stored and deflated payloads. + #[test] + fn extracts_zip_entry_with_data_descriptor() { + let stored = build_zip_with_data_descriptor("stream.txt", b"streamed payload", false); + assert_eq!( + extract_entry_bytes(&stored, b"stream.txt").as_deref(), + Some(&b"streamed payload"[..]) + ); + let deflated = + build_zip_with_data_descriptor("stream.txt", b"streamed deflated payload", true); + assert_eq!( + extract_entry_bytes(&deflated, b"stream.txt").as_deref(), + Some(&b"streamed deflated payload"[..]) + ); + } + + /// The ZIP64 extra-field builders emit the tag, length, and only the requested + /// 64-bit fields in APPNOTE order. + #[test] + fn builds_zip64_extra_fields() { + // Local extra always carries both sizes (16-byte body). + let local = zip64_local_extra(0x1_0000_0001, 0x2_0000_0002); + assert_eq!(le16(&local, 0), Some(ZIP64_EXTRA_TAG)); + assert_eq!(le16(&local, 2), Some(16)); + assert_eq!(le64(&local, 4), Some(0x1_0000_0001)); + assert_eq!(le64(&local, 12), Some(0x2_0000_0002)); + // Central extra carries only the overflowed fields, in order. + let central = zip64_central_extra(Some(7), None, Some(9)); + assert_eq!(le16(¢ral, 2), Some(16)); + assert_eq!(le64(¢ral, 4), Some(7)); + assert_eq!(le64(¢ral, 12), Some(9)); + assert!(zip64_central_extra(None, None, None).len() == 4); + } + + /// Builds a single-entry ZIP that uses every ZIP64 read path: a central record + /// whose sizes and header offset are 0xFFFFFFFF sentinels resolved by a ZIP64 + /// extra field, plus a ZIP64 EOCD record + locator behind a sentinel EOCD. + fn build_zip64_sentinel_fixture(name: &str, content: &[u8]) -> Vec { + let mut out = Vec::new(); + let local_offset = out.len() as u32; + let crc = crc32(content); + let len = content.len() as u32; + // -- local header with real sizes (central drives sizes anyway) -- + out.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); + out.extend_from_slice(&45u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&ZIP_METHOD_STORE.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&crc.to_le_bytes()); + out.extend_from_slice(&len.to_le_bytes()); + out.extend_from_slice(&len.to_le_bytes()); + out.extend_from_slice(&(name.len() as u16).to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(name.as_bytes()); + out.extend_from_slice(content); + // -- central record: all three size/offset fields are sentinels -- + let central_offset = out.len(); + let extra = zip64_central_extra(Some(len as u64), Some(len as u64), Some(local_offset as u64)); + let mut central = Vec::new(); + central.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); + central.extend_from_slice(&45u16.to_le_bytes()); + central.extend_from_slice(&45u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&ZIP_METHOD_STORE.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&crc.to_le_bytes()); + central.extend_from_slice(&ZIP32_SENTINEL.to_le_bytes()); + central.extend_from_slice(&ZIP32_SENTINEL.to_le_bytes()); + central.extend_from_slice(&(name.len() as u16).to_le_bytes()); + central.extend_from_slice(&(extra.len() as u16).to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u32.to_le_bytes()); + central.extend_from_slice(&ZIP32_SENTINEL.to_le_bytes()); + central.extend_from_slice(name.as_bytes()); + central.extend_from_slice(&extra); + let central_len = central.len(); + out.extend_from_slice(¢ral); + // -- ZIP64 EOCD record + locator -- + let eocd64_offset = out.len() as u64; + out.extend_from_slice(&0x0606_4b50u32.to_le_bytes()); + out.extend_from_slice(&44u64.to_le_bytes()); + out.extend_from_slice(&45u16.to_le_bytes()); + out.extend_from_slice(&45u16.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&1u64.to_le_bytes()); + out.extend_from_slice(&1u64.to_le_bytes()); + out.extend_from_slice(&(central_len as u64).to_le_bytes()); + out.extend_from_slice(&(central_offset as u64).to_le_bytes()); + out.extend_from_slice(&0x0706_4b50u32.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&eocd64_offset.to_le_bytes()); + out.extend_from_slice(&1u32.to_le_bytes()); + // -- regular EOCD with count/offset sentinels -- + out.extend_from_slice(&0x0605_4b50u32.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&ZIP16_SENTINEL.to_le_bytes()); + out.extend_from_slice(&ZIP16_SENTINEL.to_le_bytes()); + out.extend_from_slice(&ZIP32_SENTINEL.to_le_bytes()); + out.extend_from_slice(&ZIP32_SENTINEL.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out + } + + /// A ZIP64 archive (sentinel central fields + extra field + EOCD64/locator) is + /// read by resolving the 64-bit values, not rejected. + #[test] + fn reads_zip64_archive_with_sentinels() { + let archive = build_zip64_sentinel_fixture("big.txt", b"zip64 payload body"); + assert_eq!( + extract_entry_bytes(&archive, b"big.txt").as_deref(), + Some(&b"zip64 payload body"[..]) + ); } - /// Builds a small tar archive with regular-file entries. - fn build_tar(entries: &[(&str, &[u8])]) -> Vec { - let mut out = Vec::new(); - for (name, content) in entries { - let mut header = [0u8; 512]; - header[..name.len()].copy_from_slice(name.as_bytes()); - let size = format!("{:011o}\0", content.len()); - header[124..124 + size.len()].copy_from_slice(size.as_bytes()); - header[156] = b'0'; - header[257..263].copy_from_slice(b"ustar\0"); - header[263..265].copy_from_slice(b"00"); - for byte in &mut header[148..156] { - *byte = b' '; - } - let checksum: u32 = header.iter().map(|&b| b as u32).sum(); - let checksum = format!("{:06o}\0 ", checksum); - header[148..156].copy_from_slice(checksum.as_bytes()); - out.extend_from_slice(&header); - out.extend_from_slice(content); - out.resize(out.len() + round_up_to_512(content.len()).unwrap() - content.len(), 0); + /// Writing more than 65535 entries triggers ZIP64 output (EOCD64 record + + /// locator), and the bridge reads its own ZIP64 archive back. Set + /// `ELEPHC_KEEP_ZIP64=` to also dump the archive for an external check. + #[test] + fn writes_and_reads_zip64_many_entries() { + let count = 70_000usize; + let entries: Vec = (0..count) + .map(|i| ArchiveEntry { + name: format!("f{i}.txt").into_bytes(), + payload: b"x".to_vec(), + compression: PharCompression::None, + metadata: Vec::new(), + }) + .collect(); + let archive = build_zip_archive(&entries, &[], &[]).unwrap(); + // The ZIP64 EOCD record and locator must be present. + assert!(find_subslice(&archive, &0x0606_4b50u32.to_le_bytes()).is_some()); + assert!(find_subslice(&archive, &0x0706_4b50u32.to_le_bytes()).is_some()); + // The regular EOCD carries the count sentinel. + let eocd = find_zip_eocd(&archive).unwrap(); + assert_eq!(le16(&archive, eocd + 10), Some(ZIP16_SENTINEL)); + // Round-trip: the bridge reads back all entries and a sampled payload. + let parsed = parse_zip_archive(&archive).unwrap(); + assert_eq!(parsed.entries.len(), count); + assert_eq!( + extract_entry_bytes(&archive, b"f69999.txt").as_deref(), + Some(&b"x"[..]) + ); + if let Some(path) = std::env::var_os("ELEPHC_KEEP_ZIP64") { + std::fs::write(path, &archive).unwrap(); } - out.extend_from_slice(&[0u8; 1024]); - out } - /// Builds a ZIP archive with central-directory records. - fn build_zip(entries: &[(&str, &[u8], bool)]) -> Vec { - let mut out = Vec::new(); - let mut central = Vec::new(); - for (name, content, deflate) in entries { - let local_offset = out.len() as u32; - let stored = if *deflate { - let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(content).unwrap(); - encoder.finish().unwrap() - } else { - content.to_vec() - }; - let method = if *deflate { ZIP_METHOD_DEFLATE } else { ZIP_METHOD_STORE }; - out.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); - out.extend_from_slice(&20u16.to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(&method.to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(&0u32.to_le_bytes()); - out.extend_from_slice(&(stored.len() as u32).to_le_bytes()); - out.extend_from_slice(&(content.len() as u32).to_le_bytes()); - out.extend_from_slice(&(name.len() as u16).to_le_bytes()); - out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(name.as_bytes()); - out.extend_from_slice(&stored); - - central.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); - central.extend_from_slice(&20u16.to_le_bytes()); - central.extend_from_slice(&20u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&method.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u32.to_le_bytes()); - central.extend_from_slice(&(stored.len() as u32).to_le_bytes()); - central.extend_from_slice(&(content.len() as u32).to_le_bytes()); - central.extend_from_slice(&(name.len() as u16).to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u16.to_le_bytes()); - central.extend_from_slice(&0u32.to_le_bytes()); - central.extend_from_slice(&local_offset.to_le_bytes()); - central.extend_from_slice(name.as_bytes()); + /// Returns the general-purpose bit-flag field of the first local file header + /// whose name matches `name`, found by scanning for the local-header signature. + /// Used to assert that the writer set (or cleared) the ZipCrypto "encrypted" + /// flag bit on a given entry. + fn zip_local_flag(archive: &[u8], name: &[u8]) -> Option { + let sig = 0x0403_4b50u32.to_le_bytes(); + let mut i = 0; + while i + 30 <= archive.len() { + if archive[i..i + 4] == sig { + let flag = u16::from_le_bytes([archive[i + 6], archive[i + 7]]); + let name_len = u16::from_le_bytes([archive[i + 26], archive[i + 27]]) as usize; + let name_start = i + 30; + if archive.get(name_start..name_start + name_len) == Some(name) { + return Some(flag); + } + } + i += 1; } + None + } + + /// Builds a single-entry ZIP whose stored entry is traditional-PKWARE + /// (ZipCrypto) encrypted with `password`: a 12-byte encryption header (last + /// byte = the CRC's high byte check) plus the encrypted payload. + fn build_zipcrypto_zip(name: &str, content: &[u8], password: &[u8]) -> Vec { + let crc = crc32(content); + // Reuse the production encryptor so the test fixture and the writer share a + // single cipher direction (check byte = the CRC's high byte, no descriptor). + let enc = zipcrypto_encrypt(password, content, (crc >> 24) as u8); + let csz = enc.len() as u32; + let usz = content.len() as u32; + let mut out = Vec::new(); + // -- local header with the encrypted flag set -- + out.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); + out.extend_from_slice(&20u16.to_le_bytes()); + out.extend_from_slice(&ZIP_FLAG_ENCRYPTED.to_le_bytes()); + out.extend_from_slice(&ZIP_METHOD_STORE.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&crc.to_le_bytes()); + out.extend_from_slice(&csz.to_le_bytes()); + out.extend_from_slice(&usz.to_le_bytes()); + out.extend_from_slice(&(name.len() as u16).to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(name.as_bytes()); + out.extend_from_slice(&enc); + // -- central record -- let central_offset = out.len() as u32; + let mut central = Vec::new(); + central.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); + central.extend_from_slice(&20u16.to_le_bytes()); + central.extend_from_slice(&20u16.to_le_bytes()); + central.extend_from_slice(&ZIP_FLAG_ENCRYPTED.to_le_bytes()); + central.extend_from_slice(&ZIP_METHOD_STORE.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&crc.to_le_bytes()); + central.extend_from_slice(&csz.to_le_bytes()); + central.extend_from_slice(&usz.to_le_bytes()); + central.extend_from_slice(&(name.len() as u16).to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u16.to_le_bytes()); + central.extend_from_slice(&0u32.to_le_bytes()); + central.extend_from_slice(&0u32.to_le_bytes()); // local header offset + central.extend_from_slice(name.as_bytes()); out.extend_from_slice(¢ral); + // -- end of central directory -- out.extend_from_slice(&0x0605_4b50u32.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); - out.extend_from_slice(&(entries.len() as u16).to_le_bytes()); - out.extend_from_slice(&(entries.len() as u16).to_le_bytes()); + out.extend_from_slice(&1u16.to_le_bytes()); + out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&(central.len() as u32).to_le_bytes()); out.extend_from_slice(¢ral_offset.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out } - /// Verifies native PHAR manifest extraction. + /// A ZipCrypto-encrypted ZIP entry decrypts only with the correct password set + /// via `set_zip_password`; a missing or wrong password yields no payload. #[test] - fn extracts_native_phar_entry() { - let archive = build_native_phar(&[("a.txt", b"alpha"), ("dir/b.txt", b"bravo")]); + fn reads_zipcrypto_encrypted_entry() { + let content = b"secret zipcrypto payload\n"; + let archive = build_zipcrypto_zip("zc.txt", content, b"hunter2"); + // No password set: the encrypted entry is unreadable. + set_zip_password(b""); + assert_eq!(extract_entry_bytes(&archive, b"zc.txt"), None); + // Wrong password is rejected by the header check byte. + set_zip_password(b"wrong-password"); + assert_eq!(extract_entry_bytes(&archive, b"zc.txt"), None); + // Correct password decrypts the entry. + set_zip_password(b"hunter2"); assert_eq!( - extract_entry_bytes(&archive, b"dir/b.txt").as_deref(), - Some(&b"bravo"[..]) + extract_entry_bytes(&archive, b"zc.txt").as_deref(), + Some(&content[..]) ); + set_zip_password(b""); } - /// Verifies tar container extraction. + /// With a zip password set, `build_zip_archive` encrypts every file entry — the + /// stub included — so entries read back only with the correct password; a wrong + /// or cleared password fails, and an archive built with no password stays plain. #[test] - fn extracts_tar_entry() { - let archive = build_tar(&[("a.txt", b"alpha"), ("dir/b.txt", b"bravo")]); + fn writes_then_reads_zipcrypto_entry() { + let stored = b"plain stored payload".to_vec(); + // Repetitive bytes so the deflate path actually compresses. + let deflated = b"compress me ".repeat(64); + let entries = vec![ + ArchiveEntry { + name: b"a.txt".to_vec(), + payload: stored.clone(), + compression: PharCompression::None, + metadata: Vec::new(), + }, + ArchiveEntry { + name: b"b.txt".to_vec(), + payload: deflated.clone(), + compression: PharCompression::Gzip, + metadata: Vec::new(), + }, + ]; + let stub = PHAR_DEFAULT_STUB.to_vec(); + + set_zip_password(b"hunter2"); + let archive = build_zip_archive(&entries, &[], &stub).unwrap(); + + // The correct password decrypts both the stored and the deflated entry. assert_eq!( - extract_entry_bytes(&archive, b"dir/b.txt").as_deref(), - Some(&b"bravo"[..]) + extract_entry_bytes(&archive, b"a.txt").as_deref(), + Some(&stored[..]) ); + assert_eq!( + extract_entry_bytes(&archive, b"b.txt").as_deref(), + Some(&deflated[..]) + ); + + // The encrypted flag is set on a regular entry and on the stub (chosen scope). + assert_eq!(zip_local_flag(&archive, b"a.txt"), Some(ZIP_FLAG_ENCRYPTED)); + assert_eq!(zip_local_flag(&archive, PHAR_STUB_ENTRY), Some(ZIP_FLAG_ENCRYPTED)); + + // A wrong then cleared password cannot decrypt the entry. + set_zip_password(b"nope"); + assert_eq!(extract_entry_bytes(&archive, b"a.txt"), None); + set_zip_password(b""); + assert_eq!(extract_entry_bytes(&archive, b"a.txt"), None); + + // Built with no password the archive is plain and reads with none set. + let plain = build_zip_archive(&entries, &[], &stub).unwrap(); + assert_eq!( + extract_entry_bytes(&plain, b"a.txt").as_deref(), + Some(&stored[..]) + ); + assert_eq!(zip_local_flag(&plain, b"a.txt"), Some(0)); } - /// Verifies ZIP store and deflate extraction. + /// Signing a zip phar whose entries are encrypted still produces a readable + /// `.phar/signature.bin`: the signed range covers the encrypted bytes, the entry + /// decrypts with the password, the signature reports SHA-256, and the signature + /// entry itself stays in the clear (no encrypted flag). #[test] - fn extracts_zip_entries() { - let archive = build_zip(&[ - ("plain.txt", b"stored", false), - ("deflated.txt", b"deflated payload", true), - ]); + fn signed_encrypted_zip_still_verifies() { + let path = + std::env::temp_dir().join(format!("elephc_phar_encsig_{}.zip", std::process::id())); + let pb = path.to_string_lossy(); + + set_zip_password(b"hunter2"); + // Write an encrypted entry, then SHA-256 (algo 3) sign the archive. assert_eq!( - extract_entry_bytes(&archive, b"plain.txt").as_deref(), - Some(&b"stored"[..]) + put_entry_bytes(pb.as_bytes(), b"doc.txt", b"top secret\n"), + Some(11) ); + assert_eq!(sign_archive_hash(pb.as_bytes(), 3), Some(())); + let data = std::fs::read(&path).unwrap(); + + // The entry still decrypts; the signature reports SHA-256 with a 32-byte digest. assert_eq!( - extract_entry_bytes(&archive, b"deflated.txt").as_deref(), - Some(&b"deflated payload"[..]) + extract_entry_bytes(&data, b"doc.txt").as_deref(), + Some(&b"top secret\n"[..]) ); + assert_eq!(signature_type_name(pb.as_bytes()).as_deref(), Some(&b"SHA-256"[..])); + let (flag, digest) = read_signature_info(pb.as_bytes()).unwrap(); + assert_eq!(flag, 3); + assert_eq!(digest.len(), 32); + + // The entry is encrypted but the signature entry stays in the clear. + assert_eq!(zip_local_flag(&data, b"doc.txt"), Some(ZIP_FLAG_ENCRYPTED)); + assert_eq!(zip_local_flag(&data, PHAR_SIGNATURE_ENTRY), Some(0)); + + // Without the password the encrypted entry is unreadable. + set_zip_password(b""); + assert_eq!(extract_entry_bytes(&data, b"doc.txt"), None); + std::fs::remove_file(&path).ok(); } /// Verifies entry-name listing across supported archive families. @@ -1431,7 +3348,7 @@ mod tests { ); let archive = std::fs::read(&path).unwrap(); std::fs::remove_file(&path).ok(); - let entries = parse_native_phar_entries(&archive).unwrap(); + let entries = parse_native_phar_archive(&archive).unwrap().entries; assert_eq!(entries[0].compression, PharCompression::Gzip); assert_eq!(entries[0].payload, b"gzip updated payload"); } @@ -1460,7 +3377,7 @@ mod tests { ); let archive = std::fs::read(&path).unwrap(); std::fs::remove_file(&path).ok(); - let entries = parse_native_phar_entries(&archive).unwrap(); + let entries = parse_native_phar_archive(&archive).unwrap().entries; assert_eq!(entries[0].compression, PharCompression::Bzip2); assert_eq!(entries[0].payload, b"bzip2 updated payload"); } @@ -1498,7 +3415,7 @@ mod tests { assert_eq!(elephc_phar_stream_finalize(fd_two), 1); let archive = std::fs::read(&path).unwrap(); std::fs::remove_file(&path).ok(); - let entries = parse_native_phar_entries(&archive).unwrap(); + let entries = parse_native_phar_archive(&archive).unwrap().entries; assert_eq!(entry_payload(&entries, b"one.txt"), Some(b"alpha".as_slice())); assert_eq!(entry_payload(&entries, b"two.txt"), Some(b"bravo".as_slice())); } @@ -1664,7 +3581,7 @@ mod tests { ); assert_eq!(set_archive_compression(path_bytes.as_bytes(), 4_096), Some(())); let gzip_archive = std::fs::read(&path).unwrap(); - let gzip_entries = parse_native_phar_entries(&gzip_archive).unwrap(); + let gzip_entries = parse_native_phar_archive(&gzip_archive).unwrap().entries; assert!(gzip_entries .iter() .all(|entry| entry.compression == PharCompression::Gzip)); @@ -1676,7 +3593,7 @@ mod tests { assert_eq!(set_archive_compression(path_bytes.as_bytes(), 0), Some(())); let plain_archive = std::fs::read(&path).unwrap(); std::fs::remove_file(&path).ok(); - let plain_entries = parse_native_phar_entries(&plain_archive).unwrap(); + let plain_entries = parse_native_phar_archive(&plain_archive).unwrap().entries; assert!(plain_entries .iter() .all(|entry| entry.compression == PharCompression::None)); @@ -1705,7 +3622,7 @@ mod tests { let path_bytes = path.to_string_lossy(); assert_eq!(set_archive_compression(path_bytes.as_bytes(), 4_096), Some(())); let deflated_archive = std::fs::read(&path).unwrap(); - let deflated_entries = parse_zip_entries(&deflated_archive).unwrap(); + let deflated_entries = parse_zip_archive(&deflated_archive).unwrap().entries; assert!(deflated_entries .iter() .all(|entry| entry.compression == PharCompression::Gzip)); @@ -1717,7 +3634,7 @@ mod tests { assert_eq!(set_archive_compression(path_bytes.as_bytes(), 0), Some(())); let stored_archive = std::fs::read(&path).unwrap(); std::fs::remove_file(&path).ok(); - let stored_entries = parse_zip_entries(&stored_archive).unwrap(); + let stored_entries = parse_zip_archive(&stored_archive).unwrap().entries; assert!(stored_entries .iter() .all(|entry| entry.compression == PharCompression::None)); @@ -1787,4 +3704,390 @@ mod tests { Some(&b"bravo"[..]) ); } + + /// Stub used by the metadata/stub round-trip tests; ends with `?>\r\n` so the + /// native-PHAR `__HALT_COMPILER();` boundary scan round-trips it exactly. + const ROUND_TRIP_STUB: &[u8] = b"\r\n"; + const ROUND_TRIP_META: &[u8] = b"a:1:{s:3:\"ver\";s:5:\"1.2.3\";}"; + + /// Shared body: set metadata+stub, prove they survive a later entry write, and + /// that the reserved `.phar/*` control files stay hidden from the entry listing. + fn check_metadata_stub_round_trip(ext: &str, tag: &str) { + let path = std::env::temp_dir().join(format!( + "elephc_phar_meta_{}_{}.{}", + std::process::id(), + tag, + ext + )); + let pb = path.to_string_lossy(); + let pbytes = pb.as_bytes(); + assert_eq!(put_entry_bytes(pbytes, b"a.txt", b"alpha"), Some(5)); + assert_eq!(set_metadata_bytes(pbytes, ROUND_TRIP_META), Some(())); + assert_eq!(set_stub_bytes(pbytes, ROUND_TRIP_STUB), Some(())); + // A later entry write must preserve both metadata and stub. + assert_eq!(put_entry_bytes(pbytes, b"b.txt", b"bravo"), Some(5)); + assert_eq!(get_metadata_bytes(pbytes).as_deref(), Some(ROUND_TRIP_META)); + assert_eq!(get_stub_bytes(pbytes).as_deref(), Some(ROUND_TRIP_STUB)); + let archive = std::fs::read(&path).unwrap(); + std::fs::remove_file(&path).ok(); + assert_eq!( + extract_entry_bytes(&archive, b"a.txt").as_deref(), + Some(&b"alpha"[..]) + ); + assert_eq!( + extract_entry_bytes(&archive, b"b.txt").as_deref(), + Some(&b"bravo"[..]) + ); + let (entries, _) = parse_archive_entries(&archive).unwrap(); + assert_eq!(entries.len(), 2, "{} entry count", tag); + assert!( + entries.iter().all(|e| !e.name.starts_with(b".phar/")), + "{} leaked a .phar/ control entry", + tag + ); + } + + const ROUND_TRIP_FILE_META: &[u8] = b"a:1:{s:4:\"role\";s:5:\"first\";}"; + + /// Drives a per-file metadata round-trip for one archive family: set metadata on + /// one entry, confirm it survives a later entry write, and that only the targeted + /// entry carries metadata while `.phar/` control entries never leak. + fn check_file_metadata_round_trip(ext: &str, tag: &str) { + let path = std::env::temp_dir().join(format!( + "elephc_phar_filemeta_{}_{}.{}", + std::process::id(), + tag, + ext + )); + let pb = path.to_string_lossy(); + let pbytes = pb.as_bytes(); + assert_eq!(put_entry_bytes(pbytes, b"a.txt", b"alpha"), Some(5)); + assert_eq!(put_entry_bytes(pbytes, b"b.txt", b"bravo"), Some(5)); + assert_eq!( + set_file_metadata_bytes(pbytes, b"a.txt", ROUND_TRIP_FILE_META), + Some(()) + ); + // A later entry write must preserve the per-file metadata. + assert_eq!(put_entry_bytes(pbytes, b"c.txt", b"charlie"), Some(7)); + assert_eq!( + get_file_metadata_bytes(pbytes, b"a.txt").as_deref(), + Some(ROUND_TRIP_FILE_META), + "{} a.txt metadata", + tag + ); + // Untouched entries carry no metadata. + assert_eq!( + get_file_metadata_bytes(pbytes, b"b.txt").as_deref(), + Some(&b""[..]), + "{} b.txt metadata", + tag + ); + // Setting metadata on a missing entry fails. + assert_eq!( + set_file_metadata_bytes(pbytes, b"missing.txt", ROUND_TRIP_FILE_META), + None + ); + let archive = std::fs::read(&path).unwrap(); + std::fs::remove_file(&path).ok(); + let (entries, _) = parse_archive_entries(&archive).unwrap(); + assert_eq!(entries.len(), 3, "{} entry count", tag); + assert!( + entries.iter().all(|e| !e.name.starts_with(b".phar/")), + "{} leaked a .phar/ control entry", + tag + ); + } + + /// Drives a whole-archive compression round-trip: build a tar, compress it with + /// `compressor`, confirm the returned compressed file parses transparently with + /// entries intact, then decompress it and confirm the entries survive again. + fn check_archive_compression_round_trip( + tag: &str, + ext: &str, + compressor: fn(&[u8]) -> Option>, + ) { + let dir = std::env::temp_dir(); + let src = dir.join(format!("elephc_phar_comp_{}_{}.tar", std::process::id(), tag)); + let sb = src.to_string_lossy(); + assert_eq!(put_entry_bytes(sb.as_bytes(), b"a.txt", b"alpha"), Some(5)); + assert_eq!(put_entry_bytes(sb.as_bytes(), b"b.txt", b"bravo"), Some(5)); + let comp_bytes = compressor(sb.as_bytes()).expect("compress"); + let comp = String::from_utf8(comp_bytes).unwrap(); + assert_eq!(comp, format!("{}.{}", sb, ext), "{} dest path", tag); + // The compressed file parses transparently with entries intact. + let (entries, _) = parse_archive_entries(&std::fs::read(&comp).unwrap()).unwrap(); + assert_eq!(entries.len(), 2, "{} compressed entry count", tag); + assert_eq!( + extract_entry_bytes(&std::fs::read(&comp).unwrap(), b"a.txt"), + Some(b"alpha".to_vec()) + ); + // Decompressing reproduces a plain tar (the `.tar` base) with the same entries. + let back_bytes = decompress_archive(comp.as_bytes()).expect("decompress"); + let back = String::from_utf8(back_bytes).unwrap(); + assert_eq!(back, sb.to_string(), "{} decompress dest path", tag); + let plain = std::fs::read(&back).unwrap(); + assert_eq!(plain.get(257..262), Some(&b"ustar"[..]), "{} decompressed is tar", tag); + assert_eq!(extract_entry_bytes(&plain, b"b.txt"), Some(b"bravo".to_vec())); + for p in [src.to_string_lossy().to_string(), comp] { + std::fs::remove_file(p).ok(); + } + } + + /// A tar archive round-trips through whole-archive gzip compression. + #[test] + fn tar_archive_gzip_compress_round_trip() { + check_archive_compression_round_trip("gz", "gz", gzip_archive); + } + + /// A tar archive round-trips through whole-archive bzip2 compression. + #[test] + fn tar_archive_bzip2_compress_round_trip() { + check_archive_compression_round_trip("bz", "bz2", bzip2_archive); + } + + const TEST_RSA_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\n\ +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOuAP7xZaVfhwn9l\n\ +BaMgxKPU1ODBpuT7Ybu6Fav03TJp1BKc1wUMiXnUPraUUI2R2JxoattDe7R/LcGk\n\ +jVoPiBGGPoxxTaByd5LJZJk6MJAiGBhzQT7bkK3OMDHLQqhziefqDFfnDLt/TN7+\n\ +umuMCPtLmuF6UUXiebMzyH21x7jvAgMBAAECgYBBhL+2rgVxzrxm5vsnhEFQ9zB2\n\ +i0ncYNey+7V1zr0PfoPi3cGwhOlmfJcqAp9ak534/c/kyqSK9esL+bTdvn5zIQqC\n\ +Swt2znffaW9nC6lM/pkZcvGLETt2m0L71n6pZVkMewsGBm9YrBQFA1krC7BV674U\n\ +mlOmmYpM3LPgzmRLwQJBAPm/G7O4Stmzu5xV5qtvYX1dNZ2gydkVyfK/AwCYpfbK\n\ +8ZXntKeWCt1BER1hNBSMPacHKb0LotK3j3LNNteLHCECQQDxZdNsXNLTHylWKA/X\n\ +dyM3SH9mM6ESZP07cU7Ifq6t9zJdTfGdiyxsAjaaXxDmShL+bAjU16iwaTAGcYTB\n\ +NrMPAkEAoUGwVV7Nlbvji5I7mr4UKKoikGDdc/oJp1+GRMBLiQqI6s3ta7gJ08rL\n\ +jjjRM+NJe6u4W4RD4eL8EJhIrOv5gQJAK4Tm+8c0PtmEU0L/sCGLWMEaLquqIy3P\n\ +tXK0+FJWXYiOLOILaBKaHJK9k1EGM+4wxGtnoC+M+tjLzq2SeF7LIwJAPdLUn2Qq\n\ +eGMK12chOVcx41RxYctqsOlEKCIt011yGsV2/Mdm9ljTXeyXvNXCVOVcnHaf1v5w\n\ +rNiobfy8sSb6iw==\n\ +-----END PRIVATE KEY-----\n"; + + /// OpenSSL signing replaces the native PHAR's SHA1 trailer with an RSA-SHA1 + /// signature trailer, the signature is deterministic and verifies against the + /// derived public key, and the signature metadata reads back as OpenSSL. + #[test] + fn native_phar_openssl_signature_round_trip() { + use rsa::pkcs8::DecodePrivateKey; + use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey}; + use sha1::{Digest, Sha1}; + + let path = std::env::temp_dir().join(format!("elephc_phar_sig_{}.phar", std::process::id())); + let pb = path.to_string_lossy(); + assert_eq!(put_entry_bytes(pb.as_bytes(), b"a.txt", b"alpha"), Some(5)); + assert_eq!( + sign_archive_openssl(pb.as_bytes(), TEST_RSA_KEY_PEM.as_bytes()), + Some(()) + ); + + let signed = std::fs::read(&path).unwrap(); + let n = signed.len(); + assert_eq!(&signed[n - 4..], b"GBMB"); + assert_eq!( + u32::from_le_bytes(signed[n - 8..n - 4].try_into().unwrap()), + PHAR_OPENSSL_SIGNATURE_TYPE + ); + + // The signature reads back as OpenSSL with a 1024-bit (128-byte) RSA signature. + let (flags, sig) = read_signature_info(pb.as_bytes()).unwrap(); + assert_eq!(flags, PHAR_OPENSSL_SIGNATURE_TYPE); + assert_eq!(sig.len(), 128, "1024-bit RSA signature is 128 bytes"); + assert_eq!( + u32::from_le_bytes(signed[n - 12..n - 8].try_into().unwrap()) as usize, + sig.len() + ); + assert_eq!(signature_type_name(pb.as_bytes()).as_deref(), Some(&b"OpenSSL"[..])); + + // Re-signing is deterministic (PKCS#1 v1.5). + assert_eq!( + sign_archive_openssl(pb.as_bytes(), TEST_RSA_KEY_PEM.as_bytes()), + Some(()) + ); + let (_, sig2) = read_signature_info(pb.as_bytes()).unwrap(); + assert_eq!(sig, sig2, "PKCS#1 v1.5 signature is deterministic"); + + // The signature verifies against the public key over the signed data. + let key = RsaPrivateKey::from_pkcs8_pem(TEST_RSA_KEY_PEM).unwrap(); + let pubkey = RsaPublicKey::from(&key); + let data = strip_signature_trailer(&std::fs::read(&path).unwrap()).to_vec(); + let hashed = Sha1::digest(&data); + pubkey + .verify(Pkcs1v15Sign::new::(), &hashed, &sig) + .expect("signature verifies"); + std::fs::remove_file(&path).ok(); + } + + /// Hash-based signing rewrites the native PHAR trailer with the requested digest + /// algorithm, readable back via the signature metadata. + #[test] + fn native_phar_hash_signature_round_trip() { + let path = std::env::temp_dir().join(format!("elephc_phar_hsig_{}.phar", std::process::id())); + let pb = path.to_string_lossy(); + assert_eq!(put_entry_bytes(pb.as_bytes(), b"a.txt", b"alpha"), Some(5)); + // SHA-256 (algo 3): 32-byte digest, type "SHA-256". + assert_eq!(sign_archive_hash(pb.as_bytes(), 3), Some(())); + let (flags, digest) = read_signature_info(pb.as_bytes()).unwrap(); + assert_eq!(flags, 3); + assert_eq!(digest.len(), 32); + assert_eq!(signature_type_name(pb.as_bytes()).as_deref(), Some(&b"SHA-256"[..])); + std::fs::remove_file(&path).ok(); + } + + /// Reconstructs the byte range a tar/zip phar signature is computed over from a + /// parsed archive: the tar data records, or the zip locals + central + comment. + fn tar_zip_signed_range(arch: &Archive) -> Vec { + match arch.format { + ArchiveFormat::Tar => { + let mut body = Vec::new(); + write_tar_body(&mut body, &arch.entries, &arch.metadata, &arch.stub).unwrap(); + body + } + ArchiveFormat::Zip => { + let mut out = Vec::new(); + let mut central = Vec::new(); + write_zip_body(&mut out, &mut central, &arch.entries, &arch.stub).unwrap(); + out.extend_from_slice(¢ral); + out.extend_from_slice(&arch.metadata); + out + } + ArchiveFormat::NativePhar => unreachable!("native phars sign with a trailer"), + } + } + + /// Hash signing a tar/zip phar writes a hidden `.phar/signature.bin` entry + /// (`LE32(flag) ++ LE32(len) ++ digest`) computed over the signed range, leaving + /// real entries readable and reporting the right algorithm. + fn check_tar_zip_hash_signature(ext: &str) { + let path = + std::env::temp_dir().join(format!("elephc_phar_sig_{}_{ext}.{ext}", std::process::id())); + let pb = path.to_string_lossy(); + assert_eq!( + put_entry_bytes(pb.as_bytes(), b"doc.txt", b"bundled document\n"), + Some(17) + ); + // SHA-256 (algo 3). + assert_eq!(sign_archive_hash(pb.as_bytes(), 3), Some(())); + let data = std::fs::read(&path).unwrap(); + // The signature entry is hidden; the real entry still reads back. + assert_eq!( + extract_entry_bytes(&data, b"doc.txt").as_deref(), + Some(&b"bundled document\n"[..]) + ); + let (flag, digest) = read_signature_info(pb.as_bytes()).unwrap(); + assert_eq!(flag, 3); + assert_eq!(digest.len(), 32); + assert_eq!(signature_type_name(pb.as_bytes()).as_deref(), Some(&b"SHA-256"[..])); + // The digest covers exactly the reconstructed signed range. + let arch = parse_archive(&data).unwrap(); + assert_eq!(digest, compute_signature(3, None, &tar_zip_signed_range(&arch)).unwrap()); + std::fs::remove_file(&path).ok(); + } + + /// SHA-256 signing a tar phar round-trips through `.phar/signature.bin`. + #[test] + fn tar_phar_hash_signature_round_trip() { + check_tar_zip_hash_signature("tar"); + } + + /// SHA-256 signing a zip phar round-trips through `.phar/signature.bin`. + #[test] + fn zip_phar_hash_signature_round_trip() { + check_tar_zip_hash_signature("zip"); + } + + /// OpenSSL signing a tar/zip phar writes an RSA-SHA1 `.phar/signature.bin` that + /// verifies against the derived public key over the archive's signed range. + fn check_tar_zip_openssl_signature(ext: &str) { + use rsa::pkcs8::DecodePrivateKey; + use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey}; + use sha1::{Digest, Sha1}; + + let path = + std::env::temp_dir().join(format!("elephc_phar_osig_{}_{ext}.{ext}", std::process::id())); + let pb = path.to_string_lossy(); + assert_eq!( + put_entry_bytes(pb.as_bytes(), b"doc.txt", b"bundled document\n"), + Some(17) + ); + assert_eq!( + sign_archive_openssl(pb.as_bytes(), TEST_RSA_KEY_PEM.as_bytes()), + Some(()) + ); + let data = std::fs::read(&path).unwrap(); + let (flag, sig) = read_signature_info(pb.as_bytes()).unwrap(); + assert_eq!(flag, PHAR_OPENSSL_SIGNATURE_TYPE); + assert_eq!(sig.len(), 128, "1024-bit RSA signature is 128 bytes"); + assert_eq!(signature_type_name(pb.as_bytes()).as_deref(), Some(&b"OpenSSL"[..])); + // The signature verifies against the public key over the signed range. + let arch = parse_archive(&data).unwrap(); + let key = RsaPrivateKey::from_pkcs8_pem(TEST_RSA_KEY_PEM).unwrap(); + let pubkey = RsaPublicKey::from(&key); + let hashed = Sha1::digest(tar_zip_signed_range(&arch)); + pubkey + .verify(Pkcs1v15Sign::new::(), &hashed, &sig) + .expect("tar/zip OpenSSL signature verifies"); + std::fs::remove_file(&path).ok(); + } + + /// OpenSSL signing a tar phar verifies against the derived public key. + #[test] + fn tar_phar_openssl_signature_round_trip() { + check_tar_zip_openssl_signature("tar"); + } + + /// OpenSSL signing a zip phar verifies against the derived public key. + #[test] + fn zip_phar_openssl_signature_round_trip() { + check_tar_zip_openssl_signature("zip"); + } + + /// Per-file metadata round-trips through the native manifest per-entry field. + #[test] + fn native_phar_file_metadata_round_trip() { + check_file_metadata_round_trip("phar", "native"); + } + + /// Per-file metadata round-trips through `.phar/.metadata//.metadata.bin`. + #[test] + fn tar_phar_file_metadata_round_trip() { + check_file_metadata_round_trip("tar", "tar"); + } + + /// Per-file metadata round-trips through the zip central-directory file comment. + #[test] + fn zip_phar_file_metadata_round_trip() { + check_file_metadata_round_trip("zip", "zip"); + } + + /// Verifies native-PHAR global metadata and stub persist and survive entry writes. + #[test] + fn native_phar_metadata_and_stub_round_trip() { + check_metadata_stub_round_trip("phar", "native"); + } + + /// Verifies tar-based phar metadata/stub persist via `.phar/.metadata.bin` and + /// `.phar/stub.php`, and survive entry writes. + #[test] + fn tar_phar_metadata_and_stub_round_trip() { + check_metadata_stub_round_trip("tar", "tar"); + } + + /// Verifies zip-based phar metadata persists in the EOCD comment and the stub in + /// `.phar/stub.php`, and both survive entry writes. + #[test] + fn zip_phar_metadata_and_stub_round_trip() { + check_metadata_stub_round_trip("zip", "zip"); + } + + /// Verifies `set_stub_bytes` rejects a stub without the `__HALT_COMPILER();` marker. + #[test] + fn set_stub_requires_halt_compiler() { + let path = std::env::temp_dir().join(format!( + "elephc_phar_badstub_{}.phar", + std::process::id() + )); + let pb = path.to_string_lossy(); + assert_eq!(put_entry_bytes(pb.as_bytes(), b"a.txt", b"alpha"), Some(5)); + assert_eq!(set_stub_bytes(pb.as_bytes(), b" tz-prelude inject the timezone-introspection prelude when used -> list-id-prelude inject the DateTimeZone identifier-list prelude when used -> var-export-prelude inject the var_export prelude when used + -> image-prelude inject the image (GD/Exif/Imagick) prelude when used + -> web-prelude inject the web runtime prelude with --web -> name-resolve apply namespace/use rules, canonicalize names -> autoload-run run autoload insertion -> opt-fold AST constant folding @@ -56,8 +58,9 @@ PHP source passed with [`--define`](linking-and-conditional-compilation.md#conditional-compilation). - **resolve / prelude injection / name-resolve** — `include`/`require` are resolved, declarations are discovered, demand-loaded PHP preludes for PDO, - timezone introspection, `DateTimeZone::listIdentifiers()`, and `var_export()` - are injected only when referenced, and namespace/`use` rules rewrite + timezone introspection, `DateTimeZone::listIdentifiers()`, `var_export()`, + and image processing are injected only when referenced, the web runtime + prelude is injected with `--web`, and namespace/`use` rules rewrite references to fully-qualified names. Autoloading is wired in around these steps. - **typecheck** — the [Type Checker](../internals/the-type-checker.md) infers and diff --git a/docs/compiling/linking-and-conditional-compilation.md b/docs/compiling/linking-and-conditional-compilation.md index b145cd7872..44397f61bf 100644 --- a/docs/compiling/linking-and-conditional-compilation.md +++ b/docs/compiling/linking-and-conditional-compilation.md @@ -45,6 +45,36 @@ elephc app.php --framework Cocoa --framework Metal automatically; the flags above are for libraries not already named in the source. See [FFI & Extern](../beyond-php/extern.md). +## Bridge crates and `--with-CRATE` + +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). + +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. + +`--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 +references it) and, for crates whose PHP surface comes from an injected prelude +(`pdo`, `tz`, `image`), force-injects that prelude so the classes/functions are +available. This is useful when a program reaches a feature through indirection +that detection cannot see. The flag is repeatable: + +```bash +elephc app.php --with-pdo +elephc app.php --with-crypto --with-tls +``` + +`--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 +archive is included. + ## Heap size The compiled program uses a fixed-size runtime heap, **8 MB** by default. Programs @@ -61,6 +91,25 @@ elephc --heap-size=16777216 heavy.php # 16 MB If a program exhausts its heap it aborts with a fatal "heap memory exhausted" error; raising `--heap-size` is the fix. See [Memory Model](../internals/memory-model.md). +## Runtime dead stripping + +The compiler ships a single runtime with helpers for every supported builtin, but +a given program only uses a few of them. When linking an **executable**, the +linker keeps only the runtime helpers reachable from the program and drops the +rest, so a small program does not carry the whole runtime. This is automatic — +there is no flag — and never changes behavior, only binary size. + +It works the same on every supported target, using each platform's native +mechanism: + +- **Linux** emits each runtime helper into its own section and links with + `--gc-sections`. +- **macOS** emits the runtime object with `.subsections_via_symbols` so each + helper is a separately collectable atom, and links with `-dead_strip`. + +Shared libraries (`--emit cdylib`) keep the full runtime, since any exported +symbol may be reached by a host the linker cannot see. + ## Conditional compilation elephc supports compile-time feature branches with `ifdef`. Symbols are defined diff --git a/docs/compiling/optimization.md b/docs/compiling/optimization.md index efec17f7d2..0ffc57b7da 100644 --- a/docs/compiling/optimization.md +++ b/docs/compiling/optimization.md @@ -292,20 +292,12 @@ on compute-heavy code. | Value | Meaning | |---|---| | `tagged` (default) | Inline two-word `{payload, tag}` scalars. | -| `sentinel` | In-band `PHP_INT_MAX - 1` sentinel in one-word slots (legacy opt-out). | +| `sentinel` | In-band `PHP_INT_MAX - 1` sentinel in one-word slots (compatibility opt-out). | ```bash -elephc --null-repr=sentinel legacy.php +elephc --null-repr=sentinel sentinel.php ``` `ELEPHC_NULL_REPR` overrides the default for a whole run. Most programs should -use the default; `sentinel` exists as a legacy opt-out. See +use the default; `sentinel` exists as a compatibility opt-out. See [Memory Model](../internals/memory-model.md). - -## The frozen legacy backend - -`--ast-backend` selects the legacy direct AST→assembly backend. It is -**deprecated**, frozen (no new language or runtime features), emits a warning, -and is scheduled for removal in v0.26.0. Use it only to compare behavior with the -old backend during the transition. The EIR backend is the default and the only -active implementation target. diff --git a/docs/compiling/targets.md b/docs/compiling/targets.md index 6dd480c511..09607e4e9f 100644 --- a/docs/compiling/targets.md +++ b/docs/compiling/targets.md @@ -51,3 +51,71 @@ targets from a macOS host. For the target-aware ABI and runtime details behind each platform, see [Architecture](../internals/architecture.md) and [The Code Generator](../internals/the-codegen.md). + +## Windows codegen parity gate + +`windows-x86_64` is an experimental cross-compilation target, not yet a +first-class supported target. CI cross-compiles every codegen fixture to +`windows-x86_64` and runs it under Wine to measure how much of the suite already +behaves correctly on Windows. To let that parity grow without silently +regressing, CI enforces a **curated no-regression gate**. + +### How the gate works + +Two lists live in the repository as the source of truth: + +- `tests/codegen/support/windows_codegen_allowlist.txt` — the codegen tests that + currently **pass** on `windows-x86_64` under Wine (the known-good set). +- `tests/codegen/support/windows_codegen_known_failures.txt` — the companion list + of tests that currently **fail** on Windows. + +Together they partition the `ci`-profile *runnable* codegen tests: + +``` +allow_list = (ci-profile runnable codegen tests) - known_failures +known_failures = (ci-profile runnable codegen tests) - allow_list +``` + +The sharded `windows-codegen-parity` CI job runs the full suite under Wine and, +per shard, computes `regressions = actual_failures ∩ allow_list`. The gate rule +is exact: + +> **The gate fails if and only if a test in the allow-list failed on Windows.** + +Tests that are **not** in the allow-list — the known failures **and** any +brand-new or native-only fixtures — never fail the gate. This protects the +known-good set while letting Windows-incompatible tests exist freely: parity can +only improve, never regress. The aggregating `windows-codegen-gate` job is green +only when all 16 shards are green, and it is the single Windows-codegen +dependency of the top-level `test` gate. Each shard also prints its +passed / failed / parity% to the job summary, so the informational parity picture +stays visible alongside the gate. + +### Refreshing the allow-list as parity grows + +When Windows fixes land and previously-failing tests start passing, move them +from the known-failures list into the allow-list by regenerating both files from +a real parity run: + +1. Download the 16 `windows-codegen-junit-` artifacts from a + `windows-codegen-parity` CI run (each is that shard's `junit.xml`). +2. Produce the current runnable set: + + ```bash + cargo nextest list --profile ci --test codegen_tests \ + --message-format json > nextest_list.json + ``` + +3. Regenerate both lists deterministically: + + ```bash + python3 scripts/gen_windows_codegen_allowlist.py generate \ + --list-json nextest_list.json \ + --junit path/to/windows-codegen-junit-*/junit.xml + ``` + +The script writes both files sorted and locale-independent, so the same inputs +always reproduce byte-identical lists. It errors if a supplied failing test is +not in the runnable set (a sign the inputs came from a different revision). Never +hand-edit the lists — always regenerate. The same script's `gate` subcommand is +what the CI job runs to perform the intersection check. diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index 19a11d26b1..efa256be47 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -137,9 +137,8 @@ PHP source (.php) │ ▼ ┌─────────────┐ -│ EIR Codegen │ src/codegen_ir/ + shared src/codegen/abi/ -│ │ Emits target assembly text from EIR. The legacy AST backend -│ │ remains in src/codegen/ behind --ast-backend. +│ EIR Codegen │ src/codegen/ + shared src/codegen_support/abi/ +│ │ Emits target assembly text from EIR. └──────┬──────┘ │ ▼ @@ -190,7 +189,8 @@ src/ ├── ir/ EIR types, builder, validator, printer, effects, and tests ├── ir_lower/ Active checked-AST to EIR lowering ├── ir_passes/ EIR optimization pass driver, identity folding, peephole patterns, constant folding, common-subexpression elimination, loop-invariant code motion, dead-instruction elimination, dead-store elimination, branch simplification, the cross-function small-function inliner (run to a module-level fixed point), dominance analysis, loop analysis, and linear-scan register allocation -├── codegen_ir/ Active EIR to target assembly backend +├── codegen/ Active EIR to target assembly backend +├── codegen_support/ Shared ABI, runtime, platform, metadata, and callable support ├── runtime_cache.rs Cached shared runtime object preparation ├── source_map.rs Assembly comment markers → JSON sidecar map ├── termination.rs Structured terminal-effect analysis shared by checker and optimizer @@ -259,19 +259,20 @@ src/ │ ├── yield_validation/ Generator return coercion and yield-scope validation │ └── ... │ -├── codegen/ -│ ├── mod.rs Frozen legacy AST-backend orchestration plus shared runtime feature scans -│ ├── driver_support.rs Pipeline glue and orchestration helpers -│ ├── main_emission.rs Top-level program, globals, and deferred-body emission -│ ├── class_methods.rs Instance/static method emission orchestration -│ ├── function_variants.rs Include-loaded function variant dispatchers +├── codegen_support/ +│ ├── mod.rs Shared codegen metadata registries and support re-exports +│ ├── driver_support.rs Runtime object, deferred callable, boxing, and hash-key helpers +│ ├── arrays.rs Shared array value-type metadata stamping helpers +│ ├── callable_invoker_args.rs Descriptor-invoker argument cloning and Mixed boxing helpers +│ ├── value_boxing.rs Shared runtime-value and owned-value boxing into Mixed cells +│ ├── wrappers/ Shared callback and fiber wrapper emitters │ ├── interface_wrappers.rs Interface dispatch return-shape adapters │ ├── callables.rs Top-level callable metadata and indirect-call helpers │ ├── reflection.rs Shared ReflectionAttribute materialization helpers -│ ├── prescan.rs Pre-pass that collects program-wide codegen metadata -│ ├── program_usage.rs Program-usage analysis feeding metadata emission -│ ├── program_usage/ Required-class and variable usage scanners -│ ├── functions/generator/ Generator wrapper and resume state-machine lowering +│ ├── prescan.rs Constant pre-scan feeding EIR lowering +│ ├── program_usage.rs Required-class analysis feeding metadata emission +│ ├── program_usage/ Required-class scanners +│ ├── functions/generator/ Closure generator wrapper and resume state-machine lowering │ ├── expr.rs Expression codegen dispatcher │ ├── expr/ Expression submodules │ │ ├── arrays.rs Array-expression dispatch @@ -307,10 +308,8 @@ src/ │ │ └── storage/ `locals.rs`, `extern_globals.rs` │ ├── functions/ User function emission │ │ ├── mod.rs Function lowering entry point -│ │ ├── callback_wrapper.rs Captured callback environment wrappers │ │ ├── cleanup.rs Epilogue / ownership cleanup helpers │ │ ├── control_flow.rs Return / early-exit lowering -│ │ ├── fiber_wrapper.rs Fiber callback entry wrappers │ │ ├── locals.rs Local slot layout │ │ └── types/ Function-local type inference helpers │ ├── abi/ Target-aware calling convention helpers @@ -353,18 +352,18 @@ src/ │ ├── diagnostics.rs Suppressible runtime-warning channel used by `@` │ ├── 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, ... (132 files) -│ ├── callables/ Runtime `is_callable()` fallback for dynamic strings/arrays/hashes/objects/Mixed plus callable descriptor release (3 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, ... (145 files) +│ ├── callables/ Runtime `is_callable()` fallback for dynamic strings/arrays/hashes/objects/Mixed, callable descriptor release, and `Closure::bind` support (4 files) │ ├── io/ fopen, fgets, fread, stat, streams, sockets, filters, scandir, ... (113 files) │ ├── buffers/ buffer_new, buffer_len, bounds_fail, use_after_free helpers (5 files incl. mod.rs) │ ├── exceptions.rs Exception runtime module root / re-exports │ ├── exceptions/ cleanup_frames, dynamic_instanceof, matches, throw_current, rethrow_current, class_implements helpers (6 files) -│ ├── system/ build_argv, time, getenv, shell_exec, php_uname, date, gmdate, mktime, strtotime, getdate, localtime, checkdate, microtime, hrtime, date_default_timezone, match_unhandled, json_encode_*, json_decode, preg_*, ... (40 files) +│ ├── system/ build_argv, time, getenv, shell_exec, php_uname, date, gmdate, mktime, strtotime, getdate, localtime, checkdate, microtime, hrtime, date_default_timezone, match_unhandled, json_encode_*, json_decode, preg_*, ... (42 files) │ ├── pointers/ ptoa, ptr_check_nonnull, str_to_cstr, cstr_to_str, ptr_read_string, ptr_write_string, ... (7 files) │ ├── fibers/ stack allocation/free, context switch, entry trampoline (4 files) + `api/` (target-aware public API helpers) │ ├── objects/ stdClass, Mixed property/index access, JSON stdClass encoding, destructor dispatch, new-by-name helpers (6 files) │ ├── spl/ SplDoublyLinkedList and SplFixedArray runtime container helpers (3 files) -│ └── generators/ Generator frame layout and __rt_gen_* helpers (2 files) +│ └── generators/ Generator frame layout and fiber-backed coroutine __rt_gen_* helpers (3 files) │ │ └── errors/ @@ -488,7 +487,7 @@ The runtime data emission in `src/codegen/runtime/data/` is split into `emit_run Offset Size Field 0 8 count (number of occupied entries) 8 8 capacity (number of slots) - 16 8 value_type (coarse summary: 0=int, 1=str, 2=float, 3=bool, 4=array, 5=assoc, 6=object, 7=mixed) + 16 8 value_type (coarse summary: 0=int, 1=str, 2=float, 3=bool, 4=array, 5=assoc, 6=object, 7=mixed, 8=null) 24 8 head (slot index of first inserted entry, or -1) 32 8 tail (slot index of last inserted entry, or -1) 40 ... entries (each entry is 64 bytes) diff --git a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md index 779f45a215..2a74ed73d8 100644 --- a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md @@ -2,15 +2,15 @@ title: "__elephc_gmmktime_raw() — internals" description: "Compiler internals for __elephc_gmmktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 254 + order: 429 --- ## `__elephc_gmmktime_raw()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:151](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L151) (`lower_gmmktime`) +- **Signature**: [`src/builtins/system/__elephc_gmmktime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_gmmktime_raw.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:151](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L151) (`lower_gmmktime`) - **Function symbol**: `lower_gmmktime()` @@ -39,4 +39,3 @@ function __elephc_gmmktime_raw(int $hour, int $minute, int $second, int $month, ## 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 c6d11a549c..adc4c4f0ef 100644 --- a/docs/internals/builtins/_internal/__elephc_mktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_mktime_raw.md @@ -2,15 +2,15 @@ title: "__elephc_mktime_raw() — internals" description: "Compiler internals for __elephc_mktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 255 + order: 430 --- ## `__elephc_mktime_raw()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:140](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L140) (`lower_mktime`) +- **Signature**: [`src/builtins/system/__elephc_mktime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_mktime_raw.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:140](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L140) (`lower_mktime`) - **Function symbol**: `lower_mktime()` @@ -39,4 +39,3 @@ function __elephc_mktime_raw(int $hour, int $minute, int $second, int $month, in ## 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 new file mode 100644 index 0000000000..3433fe71b4 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md @@ -0,0 +1,38 @@ +--- +title: "__elephc_phar_bzip2_archive() — internals" +description: "Compiler internals for __elephc_phar_bzip2_archive(): lowering path, type checks, and runtime helpers." +sidebar: + order: 431 +--- + +## `__elephc_phar_bzip2_archive()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_bzip2_archive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_bzip2_archive.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4120](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4120) (`lower_elephc_phar_bzip2_archive`) +- **Function symbol**: `lower_elephc_phar_bzip2_archive()` + + +### Lowering notes + +- Lowers `__elephc_phar_bzip2_archive(src)` into the whole-archive bzip2 bridge, +- returning the written destination path (or an empty string on failure). + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_bzip2_archive(string $src): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..8a52d76979 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md @@ -0,0 +1,38 @@ +--- +title: "__elephc_phar_decompress_archive() — internals" +description: "Compiler internals for __elephc_phar_decompress_archive(): lowering path, type checks, and runtime helpers." +sidebar: + order: 432 +--- + +## `__elephc_phar_decompress_archive()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_decompress_archive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_decompress_archive.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4135](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4135) (`lower_elephc_phar_decompress_archive`) +- **Function symbol**: `lower_elephc_phar_decompress_archive()` + + +### Lowering notes + +- Lowers `__elephc_phar_decompress_archive(src)` into the whole-archive decompression +- bridge, returning the written destination path (or an empty string on failure). + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_decompress_archive(string $src): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..d636445a97 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md @@ -0,0 +1,37 @@ +--- +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: 433 +--- + +## `__elephc_phar_get_file_metadata()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_get_file_metadata.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_file_metadata.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4074](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4074) (`lower_elephc_phar_get_file_metadata`) +- **Function symbol**: `lower_elephc_phar_get_file_metadata()` + + +### Lowering notes + +- Lowers `__elephc_phar_get_file_metadata()` into the per-file metadata-read bridge. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_get_file_metadata(string $url): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..2aabe977cf --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md @@ -0,0 +1,37 @@ +--- +title: "__elephc_phar_get_metadata() — internals" +description: "Compiler internals for __elephc_phar_get_metadata(): lowering path, type checks, and runtime helpers." +sidebar: + order: 434 +--- + +## `__elephc_phar_get_metadata()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_get_metadata.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_metadata.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3859](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3859) (`lower_elephc_phar_get_metadata`) +- **Function symbol**: `lower_elephc_phar_get_metadata()` + + +### Lowering notes + +- Lowers `__elephc_phar_get_metadata()` into the metadata-read bridge call. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_get_metadata(string $filename): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..fd5cc4bda1 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md @@ -0,0 +1,37 @@ +--- +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: 435 +--- + +## `__elephc_phar_get_signature_hash()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_get_signature_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_signature_hash.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4192](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4192) (`lower_elephc_phar_get_signature_hash`) +- **Function symbol**: `lower_elephc_phar_get_signature_hash()` + + +### Lowering notes + +- Lowers `__elephc_phar_get_signature_hash(path)` into the signature-hash read bridge. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_get_signature_hash(string $path): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..93136fe1db --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md @@ -0,0 +1,37 @@ +--- +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: 436 +--- + +## `__elephc_phar_get_signature_type()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_get_signature_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_signature_type.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4206](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4206) (`lower_elephc_phar_get_signature_type`) +- **Function symbol**: `lower_elephc_phar_get_signature_type()` + + +### Lowering notes + +- Lowers `__elephc_phar_get_signature_type(path)` into the signature-type read bridge. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_get_signature_type(string $path): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..bb944cd456 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md @@ -0,0 +1,37 @@ +--- +title: "__elephc_phar_get_stub() — internals" +description: "Compiler internals for __elephc_phar_get_stub(): lowering path, type checks, and runtime helpers." +sidebar: + order: 437 +--- + +## `__elephc_phar_get_stub()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_get_stub.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_stub.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3873](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3873) (`lower_elephc_phar_get_stub`) +- **Function symbol**: `lower_elephc_phar_get_stub()` + + +### Lowering notes + +- Lowers `__elephc_phar_get_stub()` into the stub-read bridge call. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_get_stub(string $filename): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..f4bbf8d80c --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md @@ -0,0 +1,38 @@ +--- +title: "__elephc_phar_gzip_archive() — internals" +description: "Compiler internals for __elephc_phar_gzip_archive(): lowering path, type checks, and runtime helpers." +sidebar: + order: 438 +--- + +## `__elephc_phar_gzip_archive()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_gzip_archive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_gzip_archive.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4105](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4105) (`lower_elephc_phar_gzip_archive`) +- **Function symbol**: `lower_elephc_phar_gzip_archive()` + + +### Lowering notes + +- Lowers `__elephc_phar_gzip_archive(src)` into the whole-archive gzip bridge, +- returning the written destination path (or an empty string on failure). + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_gzip_archive(string $src): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 ee4b47e718..bd3a6f3fec 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md +++ b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md @@ -2,15 +2,15 @@ title: "__elephc_phar_list_entries() — internals" description: "Compiler internals for __elephc_phar_list_entries(): lowering path, type checks, and runtime helpers." sidebar: - order: 414 + order: 439 --- ## `__elephc_phar_list_entries()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3634](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3634) (`lower_elephc_phar_list_entries`) +- **Signature**: [`src/builtins/io/__elephc_phar_list_entries.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_list_entries.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4277](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4277) (`lower_elephc_phar_list_entries`) - **Function symbol**: `lower_elephc_phar_list_entries()` @@ -26,7 +26,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function __elephc_phar_list_entries(mixed $filename): array +function __elephc_phar_list_entries(string $filename): array ``` ## What the type checker enforces diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md index f28e92584e..abc214a943 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md @@ -2,15 +2,15 @@ title: "__elephc_phar_set_compression() — internals" description: "Compiler internals for __elephc_phar_set_compression(): lowering path, type checks, and runtime helpers." sidebar: - order: 415 + order: 440 --- ## `__elephc_phar_set_compression()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3576](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3576) (`lower_elephc_phar_set_compression`) +- **Signature**: [`src/builtins/io/__elephc_phar_set_compression.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_compression.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3801](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3801) (`lower_elephc_phar_set_compression`) - **Function symbol**: `lower_elephc_phar_set_compression()` @@ -26,7 +26,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function __elephc_phar_set_compression(mixed $filename, mixed $compression): bool +function __elephc_phar_set_compression(string $filename, int $compression): bool ``` ## What the type checker enforces diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md new file mode 100644 index 0000000000..f087af7661 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md @@ -0,0 +1,39 @@ +--- +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: 441 +--- + +## `__elephc_phar_set_file_metadata()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_set_file_metadata.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_file_metadata.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4090](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4090) (`lower_elephc_phar_set_file_metadata`) +- **Function symbol**: `lower_elephc_phar_set_file_metadata()` + + +### Lowering notes + +- Lowers `__elephc_phar_set_file_metadata()` into the per-file metadata-write bridge. +- The single `phar://archive/entry` URL argument is split by the bridge, so this +- reuses the same `(url, data) -> bool` shape as the archive-level metadata writer. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_set_file_metadata(string $url, string $metadata): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 new file mode 100644 index 0000000000..eab3b4b58b --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md @@ -0,0 +1,37 @@ +--- +title: "__elephc_phar_set_metadata() — internals" +description: "Compiler internals for __elephc_phar_set_metadata(): lowering path, type checks, and runtime helpers." +sidebar: + order: 442 +--- + +## `__elephc_phar_set_metadata()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_set_metadata.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_metadata.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3882](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3882) (`lower_elephc_phar_set_metadata`) +- **Function symbol**: `lower_elephc_phar_set_metadata()` + + +### Lowering notes + +- Lowers `__elephc_phar_set_metadata()` into the metadata-write bridge call. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_set_metadata(string $filename, string $metadata): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 new file mode 100644 index 0000000000..383107ac65 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md @@ -0,0 +1,37 @@ +--- +title: "__elephc_phar_set_stub() — internals" +description: "Compiler internals for __elephc_phar_set_stub(): lowering path, type checks, and runtime helpers." +sidebar: + order: 443 +--- + +## `__elephc_phar_set_stub()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_set_stub.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_stub.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3896](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3896) (`lower_elephc_phar_set_stub`) +- **Function symbol**: `lower_elephc_phar_set_stub()` + + +### Lowering notes + +- Lowers `__elephc_phar_set_stub()` into the stub-write bridge call. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_set_stub(string $filename, string $stub): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 new file mode 100644 index 0000000000..8daa3e07ab --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md @@ -0,0 +1,38 @@ +--- +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: 444 +--- + +## `__elephc_phar_set_zip_password()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_set_zip_password.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_zip_password.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4178](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4178) (`lower_elephc_phar_set_zip_password`) +- **Function symbol**: `lower_elephc_phar_set_zip_password()` + + +### Lowering notes + +- Lowers `__elephc_phar_set_zip_password(password)` into the ZipCrypto password +- bridge that lets later reads decrypt encrypted ZIP entries. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_set_zip_password(string $password): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..9cbb110824 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md @@ -0,0 +1,37 @@ +--- +title: "__elephc_phar_sign_hash() — internals" +description: "Compiler internals for __elephc_phar_sign_hash(): lowering path, type checks, and runtime helpers." +sidebar: + order: 445 +--- + +## `__elephc_phar_sign_hash()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_sign_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_sign_hash.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4163](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4163) (`lower_elephc_phar_sign_hash`) +- **Function symbol**: `lower_elephc_phar_sign_hash()` + + +### Lowering notes + +- Lowers `__elephc_phar_sign_hash(path, algo)` into the hash-based signing bridge. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_sign_hash(string $path, string $algo): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 new file mode 100644 index 0000000000..7974e930f4 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md @@ -0,0 +1,37 @@ +--- +title: "__elephc_phar_sign_openssl() — internals" +description: "Compiler internals for __elephc_phar_sign_openssl(): lowering path, type checks, and runtime helpers." +sidebar: + order: 446 +--- + +## `__elephc_phar_sign_openssl()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/io/__elephc_phar_sign_openssl.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_sign_openssl.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4149](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4149) (`lower_elephc_phar_sign_openssl`) +- **Function symbol**: `lower_elephc_phar_sign_openssl()` + + +### Lowering notes + +- Lowers `__elephc_phar_sign_openssl(path, keyPem)` into the RSA-SHA1 signing bridge. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function __elephc_phar_sign_openssl(string $path, string $key): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 9ffae0b268..291ea94039 100644 --- a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md @@ -2,15 +2,15 @@ title: "__elephc_strtotime_raw() — internals" description: "Compiler internals for __elephc_strtotime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 256 + order: 447 --- ## `__elephc_strtotime_raw()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:543](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L543) (`lower_elephc_strtotime_raw`) +- **Signature**: [`src/builtins/system/__elephc_strtotime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_strtotime_raw.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:543](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L543) (`lower_elephc_strtotime_raw`) - **Function symbol**: `lower_elephc_strtotime_raw()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function __elephc_strtotime_raw(string $datetime, int $baseTimestamp): int +function __elephc_strtotime_raw(string $datetime, int $baseTimestamp = null): int ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function __elephc_strtotime_raw(string $datetime, int $baseTimestamp): int ## 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 new file mode 100644 index 0000000000..25d33ca7c2 --- /dev/null +++ b/docs/internals/builtins/array/array_all.md @@ -0,0 +1,39 @@ +--- +title: "array_all() — internals" +description: "Compiler internals for array_all(): lowering path, type checks, and runtime helpers." +sidebar: + order: 1 +--- + +## `array_all()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_all.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_all.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1578](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1578) (`lower_array_all`) +- **Function symbol**: `lower_array_all()` + + +### Lowering notes + +- Lowers `array_all()`: returns true when every element satisfies the predicate. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_udiff_uintersect` +- `__rt_array_walk_recursive` + +## Signature summary + +```php +function array_all(mixed $array, mixed $callback): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 new file mode 100644 index 0000000000..3ade04e927 --- /dev/null +++ b/docs/internals/builtins/array/array_any.md @@ -0,0 +1,38 @@ +--- +title: "array_any() — internals" +description: "Compiler internals for array_any(): lowering path, type checks, and runtime helpers." +sidebar: + order: 2 +--- + +## `array_any()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_any.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_any.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1573](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1573) (`lower_array_any`) +- **Function symbol**: `lower_array_any()` + + +### Lowering notes + +- Lowers `array_any()`: returns true when some element satisfies the predicate. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_walk_recursive` + +## Signature summary + +```php +function array_any(mixed $array, mixed $callback): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 1b003782b4..46bd53333e 100644 --- a/docs/internals/builtins/array/array_chunk.md +++ b/docs/internals/builtins/array/array_chunk.md @@ -2,15 +2,15 @@ title: "array_chunk() — internals" description: "Compiler internals for array_chunk(): lowering path, type checks, and runtime helpers." sidebar: - order: 1 + order: 3 --- ## `array_chunk()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:81](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L81) (`lower_array_chunk`) +- **Signature**: [`src/builtins/array/array_chunk.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_chunk.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:80](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L80) (`lower_array_chunk`) - **Function symbol**: `lower_array_chunk()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function array_chunk(array $array, int $length, bool $preserve_keys): array +function array_chunk(array $array, int $length): array ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 2 arguments. ## 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 043156eb44..ee7a38c63c 100644 --- a/docs/internals/builtins/array/array_column.md +++ b/docs/internals/builtins/array/array_column.md @@ -2,15 +2,15 @@ title: "array_column() — internals" description: "Compiler internals for array_column(): lowering path, type checks, and runtime helpers." sidebar: - order: 2 + order: 4 --- ## `array_column()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays/column.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays/column.rs#L23) (`lower_array_column`) +- **Signature**: [`src/builtins/array/array_column.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_column.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays/column.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays/column.rs#L23) (`lower_array_column`) - **Function symbol**: `lower_array_column()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function array_column(array $array, string $column_key, string $index_key): array +function array_column(array $array, string $column_key): array ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 2 arguments. ## 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 4c6c84fbff..3443233226 100644 --- a/docs/internals/builtins/array/array_combine.md +++ b/docs/internals/builtins/array/array_combine.md @@ -2,21 +2,21 @@ title: "array_combine() — internals" description: "Compiler internals for array_combine(): lowering path, type checks, and runtime helpers." sidebar: - order: 3 + order: 5 --- ## `array_combine()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:152](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L152) (`lower_array_combine`) +- **Signature**: [`src/builtins/array/array_combine.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_combine.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:160](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L160) (`lower_array_combine`) - **Function symbol**: `lower_array_combine()` ### Lowering notes -- Lowers `array_combine()` through the legacy hash-building runtime helpers. +- Lowers `array_combine()` through the hash-building runtime helpers. ## Runtime helpers @@ -35,4 +35,3 @@ function array_combine(array $keys, array $values): array ## 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 4c0a2152f2..0b5cbcfb05 100644 --- a/docs/internals/builtins/array/array_diff.md +++ b/docs/internals/builtins/array/array_diff.md @@ -2,15 +2,15 @@ title: "array_diff() — internals" description: "Compiler internals for array_diff(): lowering path, type checks, and runtime helpers." sidebar: - order: 4 + order: 6 --- ## `array_diff()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:874](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L874) (`lower_array_diff`) +- **Signature**: [`src/builtins/array/array_diff.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:870](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L870) (`lower_array_diff`) - **Function symbol**: `lower_array_diff()` @@ -25,7 +25,6 @@ The following runtime helpers are referenced: - `__rt_array_diff_key` - `__rt_array_diff_refcounted` - `__rt_array_intersect` -- `__rt_array_intersect_key` - `__rt_array_intersect_refcounted` ## Signature summary @@ -42,4 +41,3 @@ function array_diff(array $array, ...$arrays): array ## 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 new file mode 100644 index 0000000000..9d53e45ba5 --- /dev/null +++ b/docs/internals/builtins/array/array_diff_assoc.md @@ -0,0 +1,39 @@ +--- +title: "array_diff_assoc() — internals" +description: "Compiler internals for array_diff_assoc(): lowering path, type checks, and runtime helpers." +sidebar: + order: 7 +--- + +## `array_diff_assoc()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_diff_assoc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff_assoc.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1359](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1359) (`lower_array_diff_assoc`) +- **Function symbol**: `lower_array_diff_assoc()` + + +### Lowering notes + +- Lowers `array_diff_assoc()` via the shared associative diff/intersect helper (mode 0 = diff). + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_assoc_diff_intersect` + +## Signature summary + +```php +function array_diff_assoc(array $array, ...$arrays): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. +- **Variadic**: collects excess arguments into `$arrays`. + +## 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 8ed9625fb1..d3278e96f6 100644 --- a/docs/internals/builtins/array/array_diff_key.md +++ b/docs/internals/builtins/array/array_diff_key.md @@ -2,15 +2,15 @@ title: "array_diff_key() — internals" description: "Compiler internals for array_diff_key(): lowering path, type checks, and runtime helpers." sidebar: - order: 5 + order: 8 --- ## `array_diff_key()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:896](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L896) (`lower_array_diff_key`) +- **Signature**: [`src/builtins/array/array_diff_key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff_key.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:895](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L895) (`lower_array_diff_key`) - **Function symbol**: `lower_array_diff_key()` @@ -38,4 +38,3 @@ function array_diff_key(array $array, ...$arrays): array ## 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 437345b8fb..507edba2c7 100644 --- a/docs/internals/builtins/array/array_fill.md +++ b/docs/internals/builtins/array/array_fill.md @@ -2,15 +2,15 @@ title: "array_fill() — internals" description: "Compiler internals for array_fill(): lowering path, type checks, and runtime helpers." sidebar: - order: 6 + order: 9 --- ## `array_fill()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:115](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L115) (`lower_array_fill`) +- **Signature**: [`src/builtins/array/array_fill.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_fill.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:116](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L116) (`lower_array_fill`) - **Function symbol**: `lower_array_fill()` @@ -35,4 +35,3 @@ function array_fill(int $start_index, int $count, mixed $value): array ## 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 4443eae331..28c5fa95fb 100644 --- a/docs/internals/builtins/array/array_fill_keys.md +++ b/docs/internals/builtins/array/array_fill_keys.md @@ -2,21 +2,21 @@ title: "array_fill_keys() — internals" description: "Compiler internals for array_fill_keys(): lowering path, type checks, and runtime helpers." sidebar: - order: 7 + order: 10 --- ## `array_fill_keys()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:138](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L138) (`lower_array_fill_keys`) +- **Signature**: [`src/builtins/array/array_fill_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_fill_keys.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:139](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L139) (`lower_array_fill_keys`) - **Function symbol**: `lower_array_fill_keys()` ### Lowering notes -- Lowers `array_fill_keys()` through the legacy hash-building runtime helpers. +- Lowers `array_fill_keys()` through the hash-building runtime helpers. ## Runtime helpers @@ -35,4 +35,3 @@ function array_fill_keys(array $keys, mixed $value): array ## 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 2a51bd8899..55b3469842 100644 --- a/docs/internals/builtins/array/array_filter.md +++ b/docs/internals/builtins/array/array_filter.md @@ -2,15 +2,15 @@ title: "array_filter() — internals" description: "Compiler internals for array_filter(): lowering path, type checks, and runtime helpers." sidebar: - order: 8 + order: 11 --- ## `array_filter()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:211](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L211) (`lower_array_filter`) +- **Signature**: [`src/builtins/array/array_filter.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_filter.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:221](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L221) (`lower_array_filter`) - **Function symbol**: `lower_array_filter()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_filter(array $array, callable $callback, int $mode): array +function array_filter(array $array, callable $callback = null, int $mode = 0): array ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function array_filter(array $array, callable $callback, int $mode): array ## 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 new file mode 100644 index 0000000000..e9df77a4fd --- /dev/null +++ b/docs/internals/builtins/array/array_find.md @@ -0,0 +1,38 @@ +--- +title: "array_find() — internals" +description: "Compiler internals for array_find(): lowering path, type checks, and runtime helpers." +sidebar: + order: 12 +--- + +## `array_find()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_find.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_find.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1568](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1568) (`lower_array_find`) +- **Function symbol**: `lower_array_find()` + + +### Lowering notes + +- Lowers `array_find()`: returns the first element satisfying the predicate, boxed as Mixed (or null). + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_walk_recursive` + +## Signature summary + +```php +function array_find(mixed $array, mixed $callback): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 188b9f246a..d1b8864780 100644 --- a/docs/internals/builtins/array/array_flip.md +++ b/docs/internals/builtins/array/array_flip.md @@ -2,21 +2,21 @@ title: "array_flip() — internals" description: "Compiler internals for array_flip(): lowering path, type checks, and runtime helpers." sidebar: - order: 9 + order: 13 --- ## `array_flip()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:171](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L171) (`lower_array_flip`) +- **Signature**: [`src/builtins/array/array_flip.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_flip.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:179](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L179) (`lower_array_flip`) - **Function symbol**: `lower_array_flip()` ### Lowering notes -- Lowers `array_flip()` through the legacy hash-building runtime helpers. +- Lowers `array_flip()` through the hash-building runtime helpers. ## Runtime helpers @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function array_flip(array $array): float +function array_flip(array $array): array ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function array_flip(array $array): float ## 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 3bf45c26ab..e16a8026f6 100644 --- a/docs/internals/builtins/array/array_intersect.md +++ b/docs/internals/builtins/array/array_intersect.md @@ -2,15 +2,15 @@ title: "array_intersect() — internals" description: "Compiler internals for array_intersect(): lowering path, type checks, and runtime helpers." sidebar: - order: 10 + order: 14 --- ## `array_intersect()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:885](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L885) (`lower_array_intersect`) +- **Signature**: [`src/builtins/array/array_intersect.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:881](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L881) (`lower_array_intersect`) - **Function symbol**: `lower_array_intersect()` @@ -40,4 +40,3 @@ function array_intersect(array $array, ...$arrays): array ## 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 new file mode 100644 index 0000000000..eeea729525 --- /dev/null +++ b/docs/internals/builtins/array/array_intersect_assoc.md @@ -0,0 +1,42 @@ +--- +title: "array_intersect_assoc() — internals" +description: "Compiler internals for array_intersect_assoc(): lowering path, type checks, and runtime helpers." +sidebar: + order: 15 +--- + +## `array_intersect_assoc()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_intersect_assoc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect_assoc.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1373](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1373) (`lower_array_intersect_assoc`) +- **Function symbol**: `lower_array_intersect_assoc()` + + +### Lowering notes + +- Lowers `array_intersect_assoc()` via the shared associative diff/intersect helper (mode 1 = intersect). + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_find_any_all` +- `__rt_array_merge_recursive` +- `__rt_array_udiff_uintersect` +- `__rt_assoc_diff_intersect` + +## Signature summary + +```php +function array_intersect_assoc(array $array, ...$arrays): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. +- **Variadic**: collects excess arguments into `$arrays`. + +## 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 fae3b8802a..5d15bd7f22 100644 --- a/docs/internals/builtins/array/array_intersect_key.md +++ b/docs/internals/builtins/array/array_intersect_key.md @@ -2,15 +2,15 @@ title: "array_intersect_key() — internals" description: "Compiler internals for array_intersect_key(): lowering path, type checks, and runtime helpers." sidebar: - order: 11 + order: 16 --- ## `array_intersect_key()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:901](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L901) (`lower_array_intersect_key`) +- **Signature**: [`src/builtins/array/array_intersect_key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect_key.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:903](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L903) (`lower_array_intersect_key`) - **Function symbol**: `lower_array_intersect_key()` @@ -37,4 +37,3 @@ function array_intersect_key(array $array, ...$arrays): array ## 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 new file mode 100644 index 0000000000..382509af6f --- /dev/null +++ b/docs/internals/builtins/array/array_is_list.md @@ -0,0 +1,42 @@ +--- +title: "array_is_list() — internals" +description: "Compiler internals for array_is_list(): lowering path, type checks, and runtime helpers." +sidebar: + order: 17 +--- + +## `array_is_list()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_is_list.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_is_list.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1150](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1150) (`lower_array_is_list`) +- **Function symbol**: `lower_array_is_list()` + + +### Lowering notes + +- Lowers `array_is_list()` to the `__rt_array_is_list` runtime predicate, returning a bool. +- The runtime helper accepts any array kind (indexed, associative hash, or boxed mixed cell) and +- reports `1` when the keys are the sequential integers `0..n-1` in insertion order, `0` otherwise. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_edge_key` +- `__rt_array_is_list` +- `__rt_mixed_from_value` + +## Signature summary + +```php +function array_is_list(mixed $array): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 9b29e69060..d75a3f886a 100644 --- a/docs/internals/builtins/array/array_key_exists.md +++ b/docs/internals/builtins/array/array_key_exists.md @@ -2,15 +2,15 @@ title: "array_key_exists() — internals" description: "Compiler internals for array_key_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 12 + order: 18 --- ## `array_key_exists()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs#L22) (`lower_array_key_exists`) +- **Signature**: [`src/builtins/array/array_key_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_exists.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays/key_exists.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays/key_exists.rs#L22) (`lower_array_key_exists`) - **Function symbol**: `lower_array_key_exists()` @@ -35,4 +35,3 @@ function array_key_exists(string $key, array $array): bool ## 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 new file mode 100644 index 0000000000..6e1607d78a --- /dev/null +++ b/docs/internals/builtins/array/array_key_first.md @@ -0,0 +1,39 @@ +--- +title: "array_key_first() — internals" +description: "Compiler internals for array_key_first(): lowering path, type checks, and runtime helpers." +sidebar: + order: 19 +--- + +## `array_key_first()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_key_first.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_first.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1161](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1161) (`lower_array_key_first`) +- **Function symbol**: `lower_array_key_first()` + + +### Lowering notes + +- Lowers `array_key_first()` through the shared edge-key helper with selector `0`. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_edge_key` +- `__rt_mixed_from_value` + +## Signature summary + +```php +function array_key_first(array $array): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..dead1f9f6f --- /dev/null +++ b/docs/internals/builtins/array/array_key_last.md @@ -0,0 +1,39 @@ +--- +title: "array_key_last() — internals" +description: "Compiler internals for array_key_last(): lowering path, type checks, and runtime helpers." +sidebar: + order: 20 +--- + +## `array_key_last()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_key_last.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_last.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1169](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1169) (`lower_array_key_last`) +- **Function symbol**: `lower_array_key_last()` + + +### Lowering notes + +- Lowers `array_key_last()` through the shared edge-key helper with selector `1`. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_edge_key` +- `__rt_mixed_from_value` + +## Signature summary + +```php +function array_key_last(array $array): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 3bca53274d..4456b39e94 100644 --- a/docs/internals/builtins/array/array_keys.md +++ b/docs/internals/builtins/array/array_keys.md @@ -2,15 +2,15 @@ title: "array_keys() — internals" description: "Compiler internals for array_keys(): lowering path, type checks, and runtime helpers." sidebar: - order: 13 + order: 21 --- ## `array_keys()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays/keys.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays/keys.rs#L23) (`lower_array_keys`) +- **Signature**: [`src/builtins/array/array_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_keys.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays/keys.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays/keys.rs#L23) (`lower_array_keys`) - **Function symbol**: `lower_array_keys()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function array_keys(array $array, string $filter_value, bool $strict): array +function array_keys(array $array): array ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 1 argument. ## 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 abd40ca2c4..fab8abb735 100644 --- a/docs/internals/builtins/array/array_map.md +++ b/docs/internals/builtins/array/array_map.md @@ -2,15 +2,15 @@ title: "array_map() — internals" description: "Compiler internals for array_map(): lowering path, type checks, and runtime helpers." sidebar: - order: 14 + order: 22 --- ## `array_map()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:312](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L312) (`lower_array_map`) +- **Signature**: [`src/builtins/array/array_map.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_map.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:326](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L326) (`lower_array_map`) - **Function symbol**: `lower_array_map()` @@ -36,4 +36,3 @@ function array_map(callable $callback, array $array, ...$arrays): array ## 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 04de54292c..afd57e2ddf 100644 --- a/docs/internals/builtins/array/array_merge.md +++ b/docs/internals/builtins/array/array_merge.md @@ -2,15 +2,15 @@ title: "array_merge() — internals" description: "Compiler internals for array_merge(): lowering path, type checks, and runtime helpers." sidebar: - order: 15 + order: 23 --- ## `array_merge()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:850](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L850) (`lower_array_merge`) +- **Signature**: [`src/builtins/array/array_merge.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_merge.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:846](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L846) (`lower_array_merge`) - **Function symbol**: `lower_array_merge()` @@ -38,4 +38,3 @@ function array_merge(...$arrays): array ## 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 new file mode 100644 index 0000000000..2b9dcd6b1f --- /dev/null +++ b/docs/internals/builtins/array/array_merge_recursive.md @@ -0,0 +1,41 @@ +--- +title: "array_merge_recursive() — internals" +description: "Compiler internals for array_merge_recursive(): lowering path, type checks, and runtime helpers." +sidebar: + order: 24 +--- + +## `array_merge_recursive()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_merge_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_merge_recursive.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1387](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1387) (`lower_array_merge_recursive`) +- **Function symbol**: `lower_array_merge_recursive()` + + +### Lowering notes + +- Lowers `array_merge_recursive()` (recursive merge with scalar collisions combined into lists). + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_find_any_all` +- `__rt_array_merge_recursive` +- `__rt_array_udiff_uintersect` + +## Signature summary + +```php +function array_merge_recursive(...$arrays): array +``` + +## What the type checker enforces + +- **Arity**: takes no arguments. +- **Variadic**: collects excess arguments into `$arrays`. + +## 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 new file mode 100644 index 0000000000..cbfe1d8d7b --- /dev/null +++ b/docs/internals/builtins/array/array_multisort.md @@ -0,0 +1,41 @@ +--- +title: "array_multisort() — internals" +description: "Compiler internals for array_multisort(): lowering path, type checks, and runtime helpers." +sidebar: + order: 25 +--- + +## `array_multisort()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_multisort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_multisort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1719](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1719) (`lower_array_multisort`) +- **Function symbol**: `lower_array_multisort()` + + +### Lowering notes + +- Lowers `array_multisort()`: stable-sorts the first indexed array ascending and reorders the second +- in tandem, both in place. Both arguments are by-reference, so each is copy-on-write split with +- `ensure_unique_sort_source` and the (possibly relocated) pointer is written back to its local +- before the runtime mutates the storage. Returns `true`. Supports 8-byte scalar indexed arrays. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function array_multisort(array $array1, int $array2): bool +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. +- **By-reference parameters**: `$array1`, `$array2`. + +## 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 50996b40c6..d24cbcc12e 100644 --- a/docs/internals/builtins/array/array_pad.md +++ b/docs/internals/builtins/array/array_pad.md @@ -2,15 +2,15 @@ title: "array_pad() — internals" description: "Compiler internals for array_pad(): lowering path, type checks, and runtime helpers." sidebar: - order: 16 + order: 26 --- ## `array_pad()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:99](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L99) (`lower_array_pad`) +- **Signature**: [`src/builtins/array/array_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_pad.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:99](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L99) (`lower_array_pad`) - **Function symbol**: `lower_array_pad()` @@ -35,4 +35,3 @@ function array_pad(array $array, int $length, mixed $value): array ## 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 57793e3d3c..b524438a57 100644 --- a/docs/internals/builtins/array/array_pop.md +++ b/docs/internals/builtins/array/array_pop.md @@ -2,15 +2,15 @@ title: "array_pop() — internals" description: "Compiler internals for array_pop(): lowering path, type checks, and runtime helpers." sidebar: - order: 17 + order: 27 --- ## `array_pop()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1048](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1048) (`lower_array_pop`) +- **Signature**: [`src/builtins/array/array_pop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_pop.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1051](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1051) (`lower_array_pop`) - **Function symbol**: `lower_array_pop()` @@ -38,4 +38,3 @@ function array_pop(array $array): mixed ## 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 d05a4b5e37..9823cef4cb 100644 --- a/docs/internals/builtins/array/array_product.md +++ b/docs/internals/builtins/array/array_product.md @@ -2,15 +2,15 @@ title: "array_product() — internals" description: "Compiler internals for array_product(): lowering path, type checks, and runtime helpers." sidebar: - order: 18 + order: 28 --- ## `array_product()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:56](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L56) (`lower_array_product`) +- **Signature**: [`src/builtins/array/array_product.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_product.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:55](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L55) (`lower_array_product`) - **Function symbol**: `lower_array_product()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_product(array $array): float +function array_product(array $array): int ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function array_product(array $array): float ## 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 4fa879726d..daf4d4d7b6 100644 --- a/docs/internals/builtins/array/array_push.md +++ b/docs/internals/builtins/array/array_push.md @@ -2,15 +2,15 @@ title: "array_push() — internals" description: "Compiler internals for array_push(): lowering path, type checks, and runtime helpers." sidebar: - order: 19 + order: 29 --- ## `array_push()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:61](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L61) (`lower_array_push`) +- **Signature**: [`src/builtins/array/array_push.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_push.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:60](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L60) (`lower_array_push`) - **Function symbol**: `lower_array_push()` @@ -37,4 +37,3 @@ function array_push(array $array, ...$values): void ## 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 a3e91ba11e..8c035ab48a 100644 --- a/docs/internals/builtins/array/array_rand.md +++ b/docs/internals/builtins/array/array_rand.md @@ -2,15 +2,15 @@ title: "array_rand() — internals" description: "Compiler internals for array_rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 20 + order: 30 --- ## `array_rand()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1007](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1007) (`lower_array_rand`) +- **Signature**: [`src/builtins/array/array_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_rand.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1010](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1010) (`lower_array_rand`) - **Function symbol**: `lower_array_rand()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_rand(array $array, int $num): int +function array_rand(array $array): int ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 8decf44861..302d47051e 100644 --- a/docs/internals/builtins/array/array_reduce.md +++ b/docs/internals/builtins/array/array_reduce.md @@ -2,15 +2,15 @@ title: "array_reduce() — internals" description: "Compiler internals for array_reduce(): lowering path, type checks, and runtime helpers." sidebar: - order: 21 + order: 31 --- ## `array_reduce()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:701](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L701) (`lower_array_reduce`) +- **Signature**: [`src/builtins/array/array_reduce.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_reduce.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:695](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L695) (`lower_array_reduce`) - **Function symbol**: `lower_array_reduce()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_reduce(array $array, callable $callback, mixed $initial): int +function array_reduce(array $array, callable $callback, mixed $initial = null): int ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function array_reduce(array $array, callable $callback, mixed $initial): int ## 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 new file mode 100644 index 0000000000..2df08cdd42 --- /dev/null +++ b/docs/internals/builtins/array/array_replace.md @@ -0,0 +1,40 @@ +--- +title: "array_replace() — internals" +description: "Compiler internals for array_replace(): lowering path, type checks, and runtime helpers." +sidebar: + order: 32 +--- + +## `array_replace()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_replace.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1340](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1340) (`lower_array_replace`) +- **Function symbol**: `lower_array_replace()` + + +### Lowering notes + +- Lowers `array_replace()` (right-wins hash merge of two hashes). + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_replace` +- `__rt_array_replace_recursive` +- `__rt_assoc_diff_intersect` + +## Signature summary + +```php +function array_replace(array $array, array $replacements): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 new file mode 100644 index 0000000000..d85275ea3f --- /dev/null +++ b/docs/internals/builtins/array/array_replace_recursive.md @@ -0,0 +1,39 @@ +--- +title: "array_replace_recursive() — internals" +description: "Compiler internals for array_replace_recursive(): lowering path, type checks, and runtime helpers." +sidebar: + order: 33 +--- + +## `array_replace_recursive()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_replace_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_replace_recursive.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1345](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1345) (`lower_array_replace_recursive`) +- **Function symbol**: `lower_array_replace_recursive()` + + +### Lowering notes + +- Lowers `array_replace_recursive()` (recursive right-wins hash merge). + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_replace_recursive` +- `__rt_assoc_diff_intersect` + +## Signature summary + +```php +function array_replace_recursive(array $array, array $replacements): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 b8036cb78d..fec27a94d2 100644 --- a/docs/internals/builtins/array/array_reverse.md +++ b/docs/internals/builtins/array/array_reverse.md @@ -2,15 +2,15 @@ title: "array_reverse() — internals" description: "Compiler internals for array_reverse(): lowering path, type checks, and runtime helpers." sidebar: - order: 22 + order: 34 --- ## `array_reverse()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:185](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L185) (`lower_array_reverse`) +- **Signature**: [`src/builtins/array/array_reverse.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_reverse.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:193](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L193) (`lower_array_reverse`) - **Function symbol**: `lower_array_reverse()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function array_reverse(array $array, bool $preserve_keys): array +function array_reverse(array $array): array ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 c436b47093..2d0fc7acc5 100644 --- a/docs/internals/builtins/array/array_search.md +++ b/docs/internals/builtins/array/array_search.md @@ -2,15 +2,15 @@ title: "array_search() — internals" description: "Compiler internals for array_search(): lowering path, type checks, and runtime helpers." sidebar: - order: 23 + order: 35 --- ## `array_search()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1141](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1141) (`lower_array_search`) +- **Signature**: [`src/builtins/array/array_search.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_search.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1761](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1761) (`lower_array_search`) - **Function symbol**: `lower_array_search()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function array_search(mixed $needle, array $haystack, bool $strict): mixed +function array_search(mixed $needle, array $haystack, bool $strict = false): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function array_search(mixed $needle, array $haystack, bool $strict): mixed ## 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 23c39b9e7d..a724e7335d 100644 --- a/docs/internals/builtins/array/array_shift.md +++ b/docs/internals/builtins/array/array_shift.md @@ -2,15 +2,15 @@ title: "array_shift() — internals" description: "Compiler internals for array_shift(): lowering path, type checks, and runtime helpers." sidebar: - order: 24 + order: 36 --- ## `array_shift()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays/shift.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays/shift.rs#L23) (`lower_array_shift`) +- **Signature**: [`src/builtins/array/array_shift.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_shift.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays/shift.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays/shift.rs#L23) (`lower_array_shift`) - **Function symbol**: `lower_array_shift()` @@ -36,4 +36,3 @@ function array_shift(array $array): mixed ## 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 2e713b100a..f7c38d4b7f 100644 --- a/docs/internals/builtins/array/array_slice.md +++ b/docs/internals/builtins/array/array_slice.md @@ -2,15 +2,15 @@ title: "array_slice() — internals" description: "Compiler internals for array_slice(): lowering path, type checks, and runtime helpers." sidebar: - order: 25 + order: 37 --- ## `array_slice()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:906](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L906) (`lower_array_slice`) +- **Signature**: [`src/builtins/array/array_slice.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_slice.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:911](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L911) (`lower_array_slice`) - **Function symbol**: `lower_array_slice()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function array_slice(array $array, int $offset, int $length, bool $preserve_keys): array +function array_slice(array $array, int $offset, int $length = null): array ``` ## What the type checker enforces -- **Arity**: takes 3–4 arguments (1 optional). +- **Arity**: takes 2–3 arguments (1 optional). ## 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 f1c8731084..9253e05907 100644 --- a/docs/internals/builtins/array/array_splice.md +++ b/docs/internals/builtins/array/array_splice.md @@ -2,15 +2,15 @@ title: "array_splice() — internals" description: "Compiler internals for array_splice(): lowering path, type checks, and runtime helpers." sidebar: - order: 26 + order: 38 --- ## `array_splice()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:949](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L949) (`lower_array_splice`) +- **Signature**: [`src/builtins/array/array_splice.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_splice.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:956](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L956) (`lower_array_splice`) - **Function symbol**: `lower_array_splice()` @@ -25,15 +25,14 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function array_splice(array $array, int $offset, int $length, array $replacement): array +function array_splice(array $array, int $offset, int $length = null): array ``` ## What the type checker enforces -- **Arity**: takes 3–4 arguments (1 optional). +- **Arity**: takes 2–3 arguments (1 optional). - **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 b08a04adb2..5157c8d94b 100644 --- a/docs/internals/builtins/array/array_sum.md +++ b/docs/internals/builtins/array/array_sum.md @@ -2,15 +2,15 @@ title: "array_sum() — internals" description: "Compiler internals for array_sum(): lowering path, type checks, and runtime helpers." sidebar: - order: 27 + order: 39 --- ## `array_sum()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:51](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L51) (`lower_array_sum`) +- **Signature**: [`src/builtins/array/array_sum.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_sum.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:50](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L50) (`lower_array_sum`) - **Function symbol**: `lower_array_sum()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_sum(array $array): float +function array_sum(array $array): int ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function array_sum(array $array): float ## 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 new file mode 100644 index 0000000000..532e8c0091 --- /dev/null +++ b/docs/internals/builtins/array/array_udiff.md @@ -0,0 +1,37 @@ +--- +title: "array_udiff() — internals" +description: "Compiler internals for array_udiff(): lowering path, type checks, and runtime helpers." +sidebar: + order: 40 +--- + +## `array_udiff()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_udiff.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_udiff.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1703](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1703) (`lower_array_udiff`) +- **Function symbol**: `lower_array_udiff()` + + +### Lowering notes + +- Lowers `array_udiff()`: keeps first-array elements not equal (per comparator) to any second-array element. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function array_udiff(array $array1, array $array2, callable $callback): array +``` + +## What the type checker enforces + +- **Arity**: takes exactly 3 arguments. + +## 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 new file mode 100644 index 0000000000..3e785eacad --- /dev/null +++ b/docs/internals/builtins/array/array_uintersect.md @@ -0,0 +1,37 @@ +--- +title: "array_uintersect() — internals" +description: "Compiler internals for array_uintersect(): lowering path, type checks, and runtime helpers." +sidebar: + order: 41 +--- + +## `array_uintersect()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_uintersect.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_uintersect.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1708](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1708) (`lower_array_uintersect`) +- **Function symbol**: `lower_array_uintersect()` + + +### Lowering notes + +- Lowers `array_uintersect()`: keeps first-array elements equal (per comparator) to some second-array element. + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function array_uintersect(array $array1, array $array2, callable $callback): array +``` + +## What the type checker enforces + +- **Arity**: takes exactly 3 arguments. + +## 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 3dc034bf15..25f753cfee 100644 --- a/docs/internals/builtins/array/array_unique.md +++ b/docs/internals/builtins/array/array_unique.md @@ -2,15 +2,15 @@ title: "array_unique() — internals" description: "Compiler internals for array_unique(): lowering path, type checks, and runtime helpers." sidebar: - order: 28 + order: 42 --- ## `array_unique()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:198](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L198) (`lower_array_unique`) +- **Signature**: [`src/builtins/array/array_unique.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_unique.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:207](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L207) (`lower_array_unique`) - **Function symbol**: `lower_array_unique()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_unique(array $array, int $flags): array +function array_unique(array $array): array ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 f131d0a583..5a84c4c60c 100644 --- a/docs/internals/builtins/array/array_unshift.md +++ b/docs/internals/builtins/array/array_unshift.md @@ -2,15 +2,15 @@ title: "array_unshift() — internals" description: "Compiler internals for array_unshift(): lowering path, type checks, and runtime helpers." sidebar: - order: 29 + order: 43 --- ## `array_unshift()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays/unshift.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays/unshift.rs#L23) (`lower_array_unshift`) +- **Signature**: [`src/builtins/array/array_unshift.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_unshift.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays/unshift.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays/unshift.rs#L23) (`lower_array_unshift`) - **Function symbol**: `lower_array_unshift()` @@ -37,4 +37,3 @@ function array_unshift(array $array, ...$values): int ## 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 2cc23a9b85..b5b3b90a05 100644 --- a/docs/internals/builtins/array/array_values.md +++ b/docs/internals/builtins/array/array_values.md @@ -2,15 +2,15 @@ title: "array_values() — internals" description: "Compiler internals for array_values(): lowering path, type checks, and runtime helpers." sidebar: - order: 30 + order: 44 --- ## `array_values()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays/values.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays/values.rs#L22) (`lower_array_values`) +- **Signature**: [`src/builtins/array/array_values.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_values.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays/values.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays/values.rs#L22) (`lower_array_values`) - **Function symbol**: `lower_array_values()` @@ -35,4 +35,3 @@ function array_values(array $array): array ## 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 916c973000..590629ecd4 100644 --- a/docs/internals/builtins/array/array_walk.md +++ b/docs/internals/builtins/array/array_walk.md @@ -2,15 +2,15 @@ title: "array_walk() — internals" description: "Compiler internals for array_walk(): lowering path, type checks, and runtime helpers." sidebar: - order: 31 + order: 45 --- ## `array_walk()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:783](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L783) (`lower_array_walk`) +- **Signature**: [`src/builtins/array/array_walk.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_walk.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:779](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L779) (`lower_array_walk`) - **Function symbol**: `lower_array_walk()` @@ -26,15 +26,14 @@ The following runtime helpers are referenced: ## Signature summary ```php -function array_walk(array $array, callable $callback, mixed $arg): void +function array_walk(array $array, callable $callback): void ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 2 arguments. - **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 new file mode 100644 index 0000000000..ae21641238 --- /dev/null +++ b/docs/internals/builtins/array/array_walk_recursive.md @@ -0,0 +1,41 @@ +--- +title: "array_walk_recursive() — internals" +description: "Compiler internals for array_walk_recursive(): lowering path, type checks, and runtime helpers." +sidebar: + order: 46 +--- + +## `array_walk_recursive()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_walk_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_walk_recursive.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1584](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1584) (`lower_array_walk_recursive`) +- **Function symbol**: `lower_array_walk_recursive()` + + +### Lowering notes + +- Lowers `array_walk_recursive()`: invokes the callback on each scalar leaf of a (possibly nested) +- array, descending into array-valued elements. Returns void; leaves are passed as 8-byte scalars. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_udiff_uintersect` +- `__rt_array_walk_recursive` + +## Signature summary + +```php +function array_walk_recursive(array $array, callable $callback): void +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. +- **By-reference parameters**: `$array`. + +## 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 63909a96c8..b30e0b6ea0 100644 --- a/docs/internals/builtins/array/arsort.md +++ b/docs/internals/builtins/array/arsort.md @@ -2,15 +2,15 @@ title: "arsort() — internals" description: "Compiler internals for arsort(): lowering path, type checks, and runtime helpers." sidebar: - order: 32 + order: 47 --- ## `arsort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1091](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1091) (`lower_arsort`) +- **Signature**: [`src/builtins/array/arsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/arsort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1094](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1094) (`lower_arsort`) - **Function symbol**: `lower_arsort()` @@ -30,15 +30,14 @@ The following runtime helpers are referenced: ## Signature summary ```php -function arsort(array $array, int $flags): bool +function arsort(array $array): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. - **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 eda817e4a3..d4cff7b4bd 100644 --- a/docs/internals/builtins/array/asort.md +++ b/docs/internals/builtins/array/asort.md @@ -2,15 +2,15 @@ title: "asort() — internals" description: "Compiler internals for asort(): lowering path, type checks, and runtime helpers." sidebar: - order: 33 + order: 48 --- ## `asort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1086](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1086) (`lower_asort`) +- **Signature**: [`src/builtins/array/asort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/asort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1089](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1089) (`lower_asort`) - **Function symbol**: `lower_asort()` @@ -31,15 +31,14 @@ The following runtime helpers are referenced: ## Signature summary ```php -function asort(array $array, int $flags): bool +function asort(array $array): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. - **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 new file mode 100644 index 0000000000..281edbe9f8 --- /dev/null +++ b/docs/internals/builtins/array/call_user_func.md @@ -0,0 +1,40 @@ +--- +title: "call_user_func() — internals" +description: "Compiler internals for call_user_func(): lowering path, type checks, and runtime helpers." +sidebar: + order: 49 +--- + +## `call_user_func()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/callables/call_user_func.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/call_user_func.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:37](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L37) (`lower_call_user_func_builtin_escape`) +- **Function symbol**: `lower_call_user_func_builtin_escape()` + + +### Lowering notes + +- Rejects `call_user_func*` calls that escaped the dedicated EIR callback lowering path. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_product` +- `__rt_array_sum` + +## Signature summary + +```php +function call_user_func(callable $callback, ...$args): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. +- **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 new file mode 100644 index 0000000000..ac8baf6e82 --- /dev/null +++ b/docs/internals/builtins/array/call_user_func_array.md @@ -0,0 +1,39 @@ +--- +title: "call_user_func_array() — internals" +description: "Compiler internals for call_user_func_array(): lowering path, type checks, and runtime helpers." +sidebar: + order: 50 +--- + +## `call_user_func_array()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/callables/call_user_func_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/call_user_func_array.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:37](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L37) (`lower_call_user_func_builtin_escape`) +- **Function symbol**: `lower_call_user_func_builtin_escape()` + + +### Lowering notes + +- Rejects `call_user_func*` calls that escaped the dedicated EIR callback lowering path. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_array_product` +- `__rt_array_sum` + +## Signature summary + +```php +function call_user_func_array(callable $callback, array $args): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 2 arguments. + +## 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 c60dbfa3c1..b25bacadc1 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -2,21 +2,25 @@ title: "count() — internals" description: "Compiler internals for count(): lowering path, type checks, and runtime helpers." sidebar: - order: 34 + order: 51 --- ## `count()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:917](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L917) (`lower_count`) +- **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`:439](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L439) (`lower_count`) - **Function symbol**: `lower_count()` ### Lowering 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. +- Handles Array/AssocArray (reads length directly from the runtime header), Mixed/Union +- (delegates to `__rt_mixed_count`), and Countable Object (calls the object's `count` +- method via intrinsic or dynamic dispatch). ## Runtime helpers @@ -26,7 +30,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function count(array $value, int $mode): int +function count(array $value, int $mode = 0): int ``` ## What the type checker enforces @@ -36,4 +40,3 @@ function count(array $value, int $mode): int ## 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 3e649a97a2..eaceecf097 100644 --- a/docs/internals/builtins/array/in_array.md +++ b/docs/internals/builtins/array/in_array.md @@ -2,21 +2,21 @@ title: "in_array() — internals" description: "Compiler internals for in_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 35 + order: 52 --- ## `in_array()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1160](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1160) (`lower_in_array`) +- **Signature**: [`src/builtins/array/in_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/in_array.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1786](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1786) (`lower_in_array`) - **Function symbol**: `lower_in_array()` ### Lowering notes -- Lowers `in_array()` for indexed arrays with scalar or string payloads. +- Lowers `in_array()` for indexed and associative arrays with PHP loose or strict membership. ## Runtime helpers @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function in_array(mixed $needle, array $haystack, bool $strict): mixed +function in_array(mixed $needle, array $haystack, bool $strict = false): bool ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function in_array(mixed $needle, array $haystack, bool $strict): mixed ## 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 1a7ecb6ecb..ccd956e871 100644 --- a/docs/internals/builtins/array/krsort.md +++ b/docs/internals/builtins/array/krsort.md @@ -2,21 +2,21 @@ title: "krsort() — internals" description: "Compiler internals for krsort(): lowering path, type checks, and runtime helpers." sidebar: - order: 36 + order: 53 --- ## `krsort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1101](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1101) (`lower_krsort`) +- **Signature**: [`src/builtins/array/krsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/krsort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1104](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1104) (`lower_krsort`) - **Function symbol**: `lower_krsort()` ### Lowering notes -- Lowers `krsort()` through the legacy reverse key-sort helper surface. +- Lowers `krsort()` through the reverse key-sort helper surface. ## Runtime helpers @@ -28,15 +28,14 @@ The following runtime helpers are referenced: ## Signature summary ```php -function krsort(array $array, int $flags): bool +function krsort(array $array): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. - **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 94d13d80e6..dc4160fc4e 100644 --- a/docs/internals/builtins/array/ksort.md +++ b/docs/internals/builtins/array/ksort.md @@ -2,21 +2,21 @@ title: "ksort() — internals" description: "Compiler internals for ksort(): lowering path, type checks, and runtime helpers." sidebar: - order: 37 + order: 54 --- ## `ksort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1096](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1096) (`lower_ksort`) +- **Signature**: [`src/builtins/array/ksort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/ksort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1099](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1099) (`lower_ksort`) - **Function symbol**: `lower_ksort()` ### Lowering notes -- Lowers `ksort()` through the legacy key-sort helper surface. +- Lowers `ksort()` through the key-sort helper surface. ## Runtime helpers @@ -29,15 +29,14 @@ The following runtime helpers are referenced: ## Signature summary ```php -function ksort(array $array, int $flags): bool +function ksort(array $array): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. - **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 5c0f941b06..ab599f0366 100644 --- a/docs/internals/builtins/array/natcasesort.md +++ b/docs/internals/builtins/array/natcasesort.md @@ -2,15 +2,15 @@ title: "natcasesort() — internals" description: "Compiler internals for natcasesort(): lowering path, type checks, and runtime helpers." sidebar: - order: 38 + order: 55 --- ## `natcasesort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1111](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1111) (`lower_natcasesort`) +- **Signature**: [`src/builtins/array/natcasesort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/natcasesort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1114](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1114) (`lower_natcasesort`) - **Function symbol**: `lower_natcasesort()` @@ -37,4 +37,3 @@ function natcasesort(array $array): bool ## 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 2bf01f499f..0321f5afde 100644 --- a/docs/internals/builtins/array/natsort.md +++ b/docs/internals/builtins/array/natsort.md @@ -2,15 +2,15 @@ title: "natsort() — internals" description: "Compiler internals for natsort(): lowering path, type checks, and runtime helpers." sidebar: - order: 39 + order: 56 --- ## `natsort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1106](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1106) (`lower_natsort`) +- **Signature**: [`src/builtins/array/natsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/natsort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1109](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1109) (`lower_natsort`) - **Function symbol**: `lower_natsort()` @@ -38,4 +38,3 @@ function natsort(array $array): bool ## 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 782b5dbd2b..c213e1f81c 100644 --- a/docs/internals/builtins/array/range.md +++ b/docs/internals/builtins/array/range.md @@ -2,15 +2,15 @@ title: "range() — internals" description: "Compiler internals for range(): lowering path, type checks, and runtime helpers." sidebar: - order: 40 + order: 57 --- ## `range()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1020](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1020) (`lower_range`) +- **Signature**: [`src/builtins/array/range.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/range.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1023](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1023) (`lower_range`) - **Function symbol**: `lower_range()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function range(mixed $start, mixed $end, int $step): array +function range(mixed $start, mixed $end): array ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 2 arguments. ## 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 0ee767c829..0f78283380 100644 --- a/docs/internals/builtins/array/rsort.md +++ b/docs/internals/builtins/array/rsort.md @@ -2,15 +2,15 @@ title: "rsort() — internals" description: "Compiler internals for rsort(): lowering path, type checks, and runtime helpers." sidebar: - order: 41 + order: 58 --- ## `rsort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1081](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1081) (`lower_rsort`) +- **Signature**: [`src/builtins/array/rsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/rsort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1084](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1084) (`lower_rsort`) - **Function symbol**: `lower_rsort()` @@ -32,15 +32,14 @@ The following runtime helpers are referenced: ## Signature summary ```php -function rsort(array $array, int $flags): bool +function rsort(array $array): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. - **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 ff5f31d2c3..0e34663a33 100644 --- a/docs/internals/builtins/array/shuffle.md +++ b/docs/internals/builtins/array/shuffle.md @@ -2,15 +2,15 @@ title: "shuffle() — internals" description: "Compiler internals for shuffle(): lowering path, type checks, and runtime helpers." sidebar: - order: 42 + order: 59 --- ## `shuffle()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1116](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1116) (`lower_shuffle`) +- **Signature**: [`src/builtins/array/shuffle.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/shuffle.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1119](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1119) (`lower_shuffle`) - **Function symbol**: `lower_shuffle()` @@ -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_array_is_list` ## Signature summary @@ -36,4 +37,3 @@ function shuffle(array $array): bool ## 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 41afa23d20..9a811cf5d8 100644 --- a/docs/internals/builtins/array/sort.md +++ b/docs/internals/builtins/array/sort.md @@ -2,15 +2,15 @@ title: "sort() — internals" description: "Compiler internals for sort(): lowering path, type checks, and runtime helpers." sidebar: - order: 43 + order: 60 --- ## `sort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1076](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1076) (`lower_sort`) +- **Signature**: [`src/builtins/array/sort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/sort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1079](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1079) (`lower_sort`) - **Function symbol**: `lower_sort()` @@ -33,15 +33,14 @@ The following runtime helpers are referenced: ## Signature summary ```php -function sort(array $array, int $flags): bool +function sort(array $array): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. - **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 87f62df3dd..21f67fbc5a 100644 --- a/docs/internals/builtins/array/uasort.md +++ b/docs/internals/builtins/array/uasort.md @@ -2,25 +2,26 @@ title: "uasort() — internals" description: "Compiler internals for uasort(): lowering path, type checks, and runtime helpers." sidebar: - order: 44 + order: 61 --- ## `uasort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1131](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1131) (`lower_uasort`) +- **Signature**: [`src/builtins/array/uasort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/uasort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1134](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1134) (`lower_uasort`) - **Function symbol**: `lower_uasort()` ### Lowering notes -- Lowers `uasort()` through the legacy user-sort helper for static comparators. +- Lowers `uasort()` through the user-sort helper for static comparators. ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_array_is_list` ## Signature summary @@ -36,4 +37,3 @@ function uasort(array $array, callable $callback): bool ## 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 a2d02ce284..3357f721f8 100644 --- a/docs/internals/builtins/array/uksort.md +++ b/docs/internals/builtins/array/uksort.md @@ -2,25 +2,26 @@ title: "uksort() — internals" description: "Compiler internals for uksort(): lowering path, type checks, and runtime helpers." sidebar: - order: 45 + order: 62 --- ## `uksort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1126](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1126) (`lower_uksort`) +- **Signature**: [`src/builtins/array/uksort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/uksort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1129](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1129) (`lower_uksort`) - **Function symbol**: `lower_uksort()` ### Lowering notes -- Lowers `uksort()` through the legacy user-sort helper for static comparators. +- Lowers `uksort()` through the user-sort helper for static comparators. ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_array_is_list` ## Signature summary @@ -36,4 +37,3 @@ function uksort(array $array, callable $callback): bool ## 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 f24339639a..66427c380f 100644 --- a/docs/internals/builtins/array/usort.md +++ b/docs/internals/builtins/array/usort.md @@ -2,15 +2,15 @@ title: "usort() — internals" description: "Compiler internals for usort(): lowering path, type checks, and runtime helpers." sidebar: - order: 46 + order: 63 --- ## `usort()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/arrays.rs`:1121](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/arrays.rs#L1121) (`lower_usort`) +- **Signature**: [`src/builtins/array/usort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/usort.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/arrays.rs`:1124](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/arrays.rs#L1124) (`lower_usort`) - **Function symbol**: `lower_usort()` @@ -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_array_is_list` ## Signature summary @@ -36,4 +37,3 @@ function usort(array $array, callable $callback): bool ## 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 55032f2631..a49ffcb874 100644 --- a/docs/internals/builtins/buffer/buffer_free.md +++ b/docs/internals/builtins/buffer/buffer_free.md @@ -2,7 +2,7 @@ title: "buffer_free() — internals" description: "Compiler internals for buffer_free(): lowering path, type checks, and runtime helpers." sidebar: - order: 47 + order: 64 --- ## `buffer_free()` — 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_ir/lower_inst/builtins/buffers.rs`:24](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/buffers.rs#L24) (`lower_buffer_free`) +- **Lowering**: [`src/codegen/lower_inst/builtins/buffers.rs`:24](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/buffers.rs#L24) (`lower_buffer_free`) - **Function symbol**: `lower_buffer_free()` @@ -35,4 +35,3 @@ function buffer_free(buffer $buffer): mixed ## 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 f149f0367a..ce22f1982f 100644 --- a/docs/internals/builtins/buffer/buffer_len.md +++ b/docs/internals/builtins/buffer/buffer_len.md @@ -2,7 +2,7 @@ title: "buffer_len() — internals" description: "Compiler internals for buffer_len(): lowering path, type checks, and runtime helpers." sidebar: - order: 48 + order: 65 --- ## `buffer_len()` — 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_ir/lower_inst/builtins/buffers.rs`:19](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/buffers.rs#L19) (`lower_buffer_len`) +- **Lowering**: [`src/codegen/lower_inst/builtins/buffers.rs`:19](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/buffers.rs#L19) (`lower_buffer_len`) - **Function symbol**: `lower_buffer_len()` @@ -35,4 +35,3 @@ function buffer_len(buffer $buffer): int ## 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 4be5b4d53a..b578f7661e 100644 --- a/docs/internals/builtins/class/class_alias.md +++ b/docs/internals/builtins/class/class_alias.md @@ -2,15 +2,15 @@ title: "class_alias() — internals" description: "Compiler internals for class_alias(): lowering path, type checks, and runtime helpers." sidebar: - order: 49 + order: 66 --- ## `class_alias()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:41](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L41) (`lower_class_alias`) +- **Signature**: [`src/builtins/callables/class_alias.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_alias.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:41](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L41) (`lower_class_alias`) - **Function symbol**: `lower_class_alias()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function class_alias(string $class, string $alias, bool $autoload): bool +function class_alias(string $class, string $alias, bool $autoload = true): bool ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function class_alias(string $class, string $alias, bool $autoload): bool ## 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 b96f5fccfb..c94a5df71d 100644 --- a/docs/internals/builtins/class/class_attribute_args.md +++ b/docs/internals/builtins/class/class_attribute_args.md @@ -2,15 +2,15 @@ title: "class_attribute_args() — internals" description: "Compiler internals for class_attribute_args(): lowering path, type checks, and runtime helpers." sidebar: - order: 50 + order: 67 --- ## `class_attribute_args()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/attributes.rs`:52](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/attributes.rs#L52) (`lower_class_attribute_args`) +- **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`) - **Function symbol**: `lower_class_attribute_args()` @@ -35,4 +35,3 @@ function class_attribute_args(string $class_name, string $attribute_name): array ## 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 4bddb841f3..cf38873252 100644 --- a/docs/internals/builtins/class/class_attribute_names.md +++ b/docs/internals/builtins/class/class_attribute_names.md @@ -2,15 +2,15 @@ title: "class_attribute_names() — internals" description: "Compiler internals for class_attribute_names(): lowering path, type checks, and runtime helpers." sidebar: - order: 51 + order: 68 --- ## `class_attribute_names()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/attributes.rs`:36](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/attributes.rs#L36) (`lower_class_attribute_names`) +- **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`) - **Function symbol**: `lower_class_attribute_names()` @@ -35,4 +35,3 @@ function class_attribute_names(string $class_name): array ## 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 02897c44fb..cc3671b2f7 100644 --- a/docs/internals/builtins/class/class_exists.md +++ b/docs/internals/builtins/class/class_exists.md @@ -2,18 +2,22 @@ title: "class_exists() — internals" description: "Compiler internals for class_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 52 + order: 69 --- ## `class_exists()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`) +- **Function symbol**: `lower_class_like_exists()` +### Lowering notes + +- Lowers AOT class/interface/enum existence checks for literal names. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function class_exists(string $class, bool $autoload): bool +function class_exists(string $class, bool $autoload = true): bool ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function class_exists(string $class, bool $autoload): bool ## 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 013c855cc7..7ae201dc05 100644 --- a/docs/internals/builtins/class/class_get_attributes.md +++ b/docs/internals/builtins/class/class_get_attributes.md @@ -2,15 +2,15 @@ title: "class_get_attributes() — internals" description: "Compiler internals for class_get_attributes(): lowering path, type checks, and runtime helpers." sidebar: - order: 53 + order: 70 --- ## `class_get_attributes()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/attributes.rs`:68](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/attributes.rs#L68) (`lower_class_get_attributes`) +- **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`) - **Function symbol**: `lower_class_get_attributes()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function class_get_attributes(string $class_name): mixed +function class_get_attributes(string $class_name): array ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function class_get_attributes(string $class_name): mixed ## 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 3f133cac47..982dcc0290 100644 --- a/docs/internals/builtins/class/class_implements.md +++ b/docs/internals/builtins/class/class_implements.md @@ -2,18 +2,22 @@ title: "class_implements() — internals" description: "Compiler internals for class_implements(): lowering path, type checks, and runtime helpers." sidebar: - order: 54 + order: 71 --- ## `class_implements()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/callables/class_implements.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_implements.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/class_relations.rs`:32](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/class_relations.rs#L32) (`lower_class_relation`) +- **Function symbol**: `lower_class_relation()` +### Lowering notes + +- Lowers `class_implements()`, `class_parents()`, and `class_uses()` from static metadata. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function class_implements(mixed $object_or_class, bool $autoload): mixed +function class_implements(mixed $object_or_class, bool $autoload = true): mixed ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function class_implements(mixed $object_or_class, bool $autoload): mixed ## 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 0f6ed57086..95d5f91740 100644 --- a/docs/internals/builtins/class/class_parents.md +++ b/docs/internals/builtins/class/class_parents.md @@ -2,18 +2,22 @@ title: "class_parents() — internals" description: "Compiler internals for class_parents(): lowering path, type checks, and runtime helpers." sidebar: - order: 55 + order: 72 --- ## `class_parents()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/callables/class_parents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_parents.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/class_relations.rs`:32](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/class_relations.rs#L32) (`lower_class_relation`) +- **Function symbol**: `lower_class_relation()` +### Lowering notes + +- Lowers `class_implements()`, `class_parents()`, and `class_uses()` from static metadata. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function class_parents(mixed $object_or_class, bool $autoload): mixed +function class_parents(mixed $object_or_class, bool $autoload = true): mixed ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function class_parents(mixed $object_or_class, bool $autoload): mixed ## 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 3467272d55..ad4517f2e0 100644 --- a/docs/internals/builtins/class/class_uses.md +++ b/docs/internals/builtins/class/class_uses.md @@ -2,18 +2,22 @@ title: "class_uses() — internals" description: "Compiler internals for class_uses(): lowering path, type checks, and runtime helpers." sidebar: - order: 56 + order: 73 --- ## `class_uses()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/callables/class_uses.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_uses.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/class_relations.rs`:32](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/class_relations.rs#L32) (`lower_class_relation`) +- **Function symbol**: `lower_class_relation()` +### Lowering notes + +- Lowers `class_implements()`, `class_parents()`, and `class_uses()` from static metadata. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function class_uses(mixed $object_or_class, bool $autoload): mixed +function class_uses(mixed $object_or_class, bool $autoload = true): mixed ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function class_uses(mixed $object_or_class, bool $autoload): mixed ## 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 268bc3e37c..4d97bdcb0a 100644 --- a/docs/internals/builtins/class/enum_exists.md +++ b/docs/internals/builtins/class/enum_exists.md @@ -2,18 +2,22 @@ title: "enum_exists() — internals" description: "Compiler internals for enum_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 57 + order: 74 --- ## `enum_exists()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`) +- **Function symbol**: `lower_class_like_exists()` +### Lowering notes + +- Lowers AOT class/interface/enum existence checks for literal names. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function enum_exists(string $enum, bool $autoload): bool +function enum_exists(string $enum, bool $autoload = true): bool ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function enum_exists(string $enum, bool $autoload): bool ## 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 d77e700cd1..e72e15dfbb 100644 --- a/docs/internals/builtins/class/function_exists.md +++ b/docs/internals/builtins/class/function_exists.md @@ -2,15 +2,15 @@ title: "function_exists() — internals" description: "Compiler internals for function_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 58 + order: 75 --- ## `function_exists()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:759](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L759) (`lower_function_exists`) +- **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`) - **Function symbol**: `lower_function_exists()` @@ -40,4 +40,3 @@ function function_exists(string $function): bool ## Cross-references - [User reference for `function_exists()`](../../../php/builtins/class/function_exists.md) - diff --git a/docs/internals/builtins/class/get_class.md b/docs/internals/builtins/class/get_class.md index 61b565690f..323193ac61 100644 --- a/docs/internals/builtins/class/get_class.md +++ b/docs/internals/builtins/class/get_class.md @@ -2,18 +2,22 @@ title: "get_class() — internals" description: "Compiler internals for get_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 59 + order: 76 --- ## `get_class()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/callables/get_class.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_class.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:331](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L331) (`lower_class_name_lookup`) +- **Function symbol**: `lower_class_name_lookup()` +### Lowering notes + +- Lowers `get_class()` and `get_parent_class()` through static or dynamic class metadata. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function get_class(object $object): string +function get_class(object $object = null): string ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function get_class(object $object): string ## Cross-references - [User reference for `get_class()`](../../../php/builtins/class/get_class.md) - diff --git a/docs/internals/builtins/class/get_declared_classes.md b/docs/internals/builtins/class/get_declared_classes.md index 0610281618..aae271d112 100644 --- a/docs/internals/builtins/class/get_declared_classes.md +++ b/docs/internals/builtins/class/get_declared_classes.md @@ -2,18 +2,22 @@ title: "get_declared_classes() — internals" description: "Compiler internals for get_declared_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 60 + order: 77 --- ## `get_declared_classes()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`) +- **Function symbol**: `lower_get_declared_names()` +### Lowering notes + +- Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function get_declared_classes(): array ## 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 9176e5af3b..05df0d4739 100644 --- a/docs/internals/builtins/class/get_declared_interfaces.md +++ b/docs/internals/builtins/class/get_declared_interfaces.md @@ -2,18 +2,22 @@ title: "get_declared_interfaces() — internals" description: "Compiler internals for get_declared_interfaces(): lowering path, type checks, and runtime helpers." sidebar: - order: 61 + order: 78 --- ## `get_declared_interfaces()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`) +- **Function symbol**: `lower_get_declared_names()` +### Lowering notes + +- Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function get_declared_interfaces(): array ## 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 26481e0d0a..f9a034bfc1 100644 --- a/docs/internals/builtins/class/get_declared_traits.md +++ b/docs/internals/builtins/class/get_declared_traits.md @@ -2,18 +2,22 @@ title: "get_declared_traits() — internals" description: "Compiler internals for get_declared_traits(): lowering path, type checks, and runtime helpers." sidebar: - order: 62 + order: 79 --- ## `get_declared_traits()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`) +- **Function symbol**: `lower_get_declared_names()` +### Lowering notes + +- Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function get_declared_traits(): array ## Cross-references - [User reference for `get_declared_traits()`](../../../php/builtins/class/get_declared_traits.md) - diff --git a/docs/internals/builtins/class/get_parent_class.md b/docs/internals/builtins/class/get_parent_class.md index 4c93f18248..a574bde029 100644 --- a/docs/internals/builtins/class/get_parent_class.md +++ b/docs/internals/builtins/class/get_parent_class.md @@ -2,18 +2,22 @@ title: "get_parent_class() — internals" description: "Compiler internals for get_parent_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 63 + order: 80 --- ## `get_parent_class()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/callables/get_parent_class.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_parent_class.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:331](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L331) (`lower_class_name_lookup`) +- **Function symbol**: `lower_class_name_lookup()` +### Lowering notes + +- Lowers `get_class()` and `get_parent_class()` through static or dynamic class metadata. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function get_parent_class(mixed $object_or_class): string +function get_parent_class(mixed $object_or_class = null): string ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function get_parent_class(mixed $object_or_class): string ## 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 a14e918848..11a13350ab 100644 --- a/docs/internals/builtins/class/interface_exists.md +++ b/docs/internals/builtins/class/interface_exists.md @@ -2,18 +2,22 @@ title: "interface_exists() — internals" description: "Compiler internals for interface_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 64 + order: 81 --- ## `interface_exists()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`) +- **Function symbol**: `lower_class_like_exists()` +### Lowering notes + +- Lowers AOT class/interface/enum existence checks for literal names. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function interface_exists(string $interface, bool $autoload): bool +function interface_exists(string $interface, bool $autoload = true): bool ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function interface_exists(string $interface, bool $autoload): bool ## 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 4062f2ff08..3ca72ca135 100644 --- a/docs/internals/builtins/class/is_a.md +++ b/docs/internals/builtins/class/is_a.md @@ -2,18 +2,22 @@ title: "is_a() — internals" description: "Compiler internals for is_a(): lowering path, type checks, and runtime helpers." sidebar: - order: 65 + order: 82 --- ## `is_a()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/callables/is_a.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/is_a.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:369](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L369) (`lower_is_a_relation`) +- **Function symbol**: `lower_is_a_relation()` +### Lowering notes + +- Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function is_a(object $object_or_class, string $class, bool $allow_string): bool +function is_a(object $object_or_class, string $class, bool $allow_string = false): bool ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function is_a(object $object_or_class, string $class, bool $allow_string): bool ## 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 b41195c82e..404beddeac 100644 --- a/docs/internals/builtins/class/is_subclass_of.md +++ b/docs/internals/builtins/class/is_subclass_of.md @@ -2,18 +2,22 @@ title: "is_subclass_of() — internals" description: "Compiler internals for is_subclass_of(): lowering path, type checks, and runtime helpers." sidebar: - order: 66 + order: 83 --- ## `is_subclass_of()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/callables/is_subclass_of.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/is_subclass_of.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:369](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L369) (`lower_is_a_relation`) +- **Function symbol**: `lower_is_a_relation()` +### Lowering notes + +- Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function is_subclass_of(mixed $object_or_class, string $class, bool $allow_string): bool +function is_subclass_of(mixed $object_or_class, string $class, bool $allow_string = true): bool ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function is_subclass_of(mixed $object_or_class, string $class, bool $allow_strin ## 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 560cfaa628..93e606d30c 100644 --- a/docs/internals/builtins/class/trait_exists.md +++ b/docs/internals/builtins/class/trait_exists.md @@ -2,18 +2,22 @@ title: "trait_exists() — internals" description: "Compiler internals for trait_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 67 + order: 84 --- ## `trait_exists()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`) +- **Function symbol**: `lower_class_like_exists()` +### Lowering notes + +- Lowers AOT class/interface/enum existence checks for literal names. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function trait_exists(string $trait, bool $autoload): bool +function trait_exists(string $trait, bool $autoload = true): bool ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function trait_exists(string $trait, bool $autoload): bool ## 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 8811075285..db4899f487 100644 --- a/docs/internals/builtins/date/checkdate.md +++ b/docs/internals/builtins/date/checkdate.md @@ -2,15 +2,15 @@ title: "checkdate() — internals" description: "Compiler internals for checkdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 68 + order: 85 --- ## `checkdate()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:163](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L163) (`lower_checkdate`) +- **Signature**: [`src/builtins/system/checkdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/checkdate.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:163](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L163) (`lower_checkdate`) - **Function symbol**: `lower_checkdate()` @@ -40,4 +40,3 @@ function checkdate(int $month, int $day, int $year): bool ## 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 279496d460..284b39e58b 100644 --- a/docs/internals/builtins/date/date.md +++ b/docs/internals/builtins/date/date.md @@ -2,15 +2,15 @@ title: "date() — internals" description: "Compiler internals for date(): lowering path, type checks, and runtime helpers." sidebar: - order: 69 + order: 86 --- ## `date()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L22) (`lower_date`) +- **Signature**: [`src/builtins/system/date.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/date.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L22) (`lower_date`) - **Function symbol**: `lower_date()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function date(string $format, int $timestamp): string +function date(string $format, int $timestamp = null): string ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function date(string $format, int $timestamp): string ## 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 c8810568b9..cc1cbfbdb6 100644 --- a/docs/internals/builtins/date/date_default_timezone_get.md +++ b/docs/internals/builtins/date/date_default_timezone_get.md @@ -2,15 +2,15 @@ title: "date_default_timezone_get() — internals" description: "Compiler internals for date_default_timezone_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 70 + order: 87 --- ## `date_default_timezone_get()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:70](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L70) (`lower_date_default_timezone_get`) +- **Signature**: [`src/builtins/system/date_default_timezone_get.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/date_default_timezone_get.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:70](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L70) (`lower_date_default_timezone_get`) - **Function symbol**: `lower_date_default_timezone_get()` @@ -39,4 +39,3 @@ function date_default_timezone_get(): string ## 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 4a12cd0ec4..9cfee9f4a2 100644 --- a/docs/internals/builtins/date/date_default_timezone_set.md +++ b/docs/internals/builtins/date/date_default_timezone_set.md @@ -2,15 +2,15 @@ title: "date_default_timezone_set() — internals" description: "Compiler internals for date_default_timezone_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 71 + order: 88 --- ## `date_default_timezone_set()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:84](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L84) (`lower_date_default_timezone_set`) +- **Signature**: [`src/builtins/system/date_default_timezone_set.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/date_default_timezone_set.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:84](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L84) (`lower_date_default_timezone_set`) - **Function symbol**: `lower_date_default_timezone_set()` @@ -42,4 +42,3 @@ function date_default_timezone_set(string $timezoneId): bool ## 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 4b58944763..f13d4477cd 100644 --- a/docs/internals/builtins/date/getdate.md +++ b/docs/internals/builtins/date/getdate.md @@ -2,15 +2,15 @@ title: "getdate() — internals" description: "Compiler internals for getdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 72 + order: 89 --- ## `getdate()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:183](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L183) (`lower_getdate`) +- **Signature**: [`src/builtins/system/getdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/getdate.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:183](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L183) (`lower_getdate`) - **Function symbol**: `lower_getdate()` @@ -31,7 +31,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function getdate(int $timestamp): array +function getdate(int $timestamp = null): array ``` ## What the type checker enforces @@ -41,4 +41,3 @@ function getdate(int $timestamp): array ## 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 6db0c7c0a8..a1d773144b 100644 --- a/docs/internals/builtins/date/gmdate.md +++ b/docs/internals/builtins/date/gmdate.md @@ -2,15 +2,15 @@ title: "gmdate() — internals" description: "Compiler internals for gmdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 73 + order: 90 --- ## `gmdate()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:33](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L33) (`lower_gmdate`) +- **Signature**: [`src/builtins/system/gmdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/gmdate.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:33](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L33) (`lower_gmdate`) - **Function symbol**: `lower_gmdate()` @@ -29,7 +29,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function gmdate(string $format, int $timestamp): string +function gmdate(string $format, int $timestamp = null): string ``` ## What the type checker enforces @@ -39,4 +39,3 @@ function gmdate(string $format, int $timestamp): string ## 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 2ec3bc56be..e711735072 100644 --- a/docs/internals/builtins/date/gmmktime.md +++ b/docs/internals/builtins/date/gmmktime.md @@ -2,15 +2,15 @@ title: "gmmktime() — internals" description: "Compiler internals for gmmktime(): lowering path, type checks, and runtime helpers." sidebar: - order: 74 + order: 91 --- ## `gmmktime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:151](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L151) (`lower_gmmktime`) +- **Signature**: [`src/builtins/system/gmmktime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/gmmktime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:151](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L151) (`lower_gmmktime`) - **Function symbol**: `lower_gmmktime()` @@ -40,4 +40,3 @@ function gmmktime(int $hour, int $minute, int $second, int $month, int $day, int ## 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 6d1fbd9624..fb9cb930cc 100644 --- a/docs/internals/builtins/date/hrtime.md +++ b/docs/internals/builtins/date/hrtime.md @@ -2,15 +2,15 @@ title: "hrtime() — internals" description: "Compiler internals for hrtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 75 + order: 92 --- ## `hrtime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:247](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L247) (`lower_hrtime`) +- **Signature**: [`src/builtins/system/hrtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/hrtime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:247](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L247) (`lower_hrtime`) - **Function symbol**: `lower_hrtime()` @@ -31,7 +31,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function hrtime(bool $as_number): mixed +function hrtime(bool $as_number = false): mixed ``` ## What the type checker enforces @@ -41,4 +41,3 @@ function hrtime(bool $as_number): mixed ## 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 0ec5a9b471..38d517a792 100644 --- a/docs/internals/builtins/date/localtime.md +++ b/docs/internals/builtins/date/localtime.md @@ -2,15 +2,15 @@ title: "localtime() — internals" description: "Compiler internals for localtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 76 + order: 93 --- ## `localtime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:220](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L220) (`lower_localtime`) +- **Signature**: [`src/builtins/system/localtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/localtime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:220](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L220) (`lower_localtime`) - **Function symbol**: `lower_localtime()` @@ -32,7 +32,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function localtime(int $timestamp, bool $associative): array +function localtime(int $timestamp = -1, bool $associative = false): array ``` ## What the type checker enforces @@ -42,4 +42,3 @@ function localtime(int $timestamp, bool $associative): array ## 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 4aa630697a..cf145e0101 100644 --- a/docs/internals/builtins/date/microtime.md +++ b/docs/internals/builtins/date/microtime.md @@ -2,15 +2,15 @@ title: "microtime() — internals" description: "Compiler internals for microtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 77 + order: 94 --- ## `microtime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:111](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L111) (`lower_microtime`) +- **Signature**: [`src/builtins/system/microtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/microtime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:111](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L111) (`lower_microtime`) - **Function symbol**: `lower_microtime()` @@ -35,7 +35,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function microtime(bool $as_float): int +function microtime(bool $as_float = false): mixed ``` ## What the type checker enforces @@ -45,4 +45,3 @@ function microtime(bool $as_float): int ## 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 b464dd2d2b..58eb3e8b67 100644 --- a/docs/internals/builtins/date/mktime.md +++ b/docs/internals/builtins/date/mktime.md @@ -2,15 +2,15 @@ title: "mktime() — internals" description: "Compiler internals for mktime(): lowering path, type checks, and runtime helpers." sidebar: - order: 78 + order: 95 --- ## `mktime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:140](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L140) (`lower_mktime`) +- **Signature**: [`src/builtins/system/mktime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/mktime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:140](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L140) (`lower_mktime`) - **Function symbol**: `lower_mktime()` @@ -38,4 +38,3 @@ function mktime(int $hour, int $minute, int $second, int $month, int $day, int $ ## 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 7b16234b40..8075a0c1e5 100644 --- a/docs/internals/builtins/date/strtotime.md +++ b/docs/internals/builtins/date/strtotime.md @@ -2,15 +2,15 @@ title: "strtotime() — internals" description: "Compiler internals for strtotime(): lowering path, type checks, and runtime helpers." sidebar: - order: 79 + order: 96 --- ## `strtotime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:487](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L487) (`lower_strtotime`) +- **Signature**: [`src/builtins/system/strtotime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/strtotime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:487](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L487) (`lower_strtotime`) - **Function symbol**: `lower_strtotime()` @@ -32,7 +32,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function strtotime(string $datetime, int $baseTimestamp): mixed +function strtotime(string $datetime, int $baseTimestamp = null): mixed ``` ## What the type checker enforces @@ -42,4 +42,3 @@ function strtotime(string $datetime, int $baseTimestamp): mixed ## 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 512717f62a..6a5cb71832 100644 --- a/docs/internals/builtins/date/time.md +++ b/docs/internals/builtins/date/time.md @@ -2,15 +2,15 @@ title: "time() — internals" description: "Compiler internals for time(): lowering path, type checks, and runtime helpers." sidebar: - order: 80 + order: 97 --- ## `time()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:615](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L615) (`lower_time`) +- **Signature**: [`src/builtins/system/time.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/time.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:615](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L615) (`lower_time`) - **Function symbol**: `lower_time()` @@ -36,4 +36,3 @@ function time(): int ## 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 854f86e7a5..9c4395d000 100644 --- a/docs/internals/builtins/filesystem/basename.md +++ b/docs/internals/builtins/filesystem/basename.md @@ -2,15 +2,15 @@ title: "basename() — internals" description: "Compiler internals for basename(): lowering path, type checks, and runtime helpers." sidebar: - order: 81 + order: 98 --- ## `basename()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3893](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3893) (`lower_basename`) +- **Signature**: [`src/builtins/io/basename.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/basename.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4536](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4536) (`lower_basename`) - **Function symbol**: `lower_basename()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function basename(string $path, string $suffix): string +function basename(string $path, string $suffix = ''): string ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function basename(string $path, string $suffix): string ## 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 a691b4ffe5..29f810231c 100644 --- a/docs/internals/builtins/filesystem/chdir.md +++ b/docs/internals/builtins/filesystem/chdir.md @@ -2,15 +2,15 @@ title: "chdir() — internals" description: "Compiler internals for chdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 82 + order: 99 --- ## `chdir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3795](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3795) (`lower_chdir`) +- **Signature**: [`src/builtins/io/chdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chdir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4438](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4438) (`lower_chdir`) - **Function symbol**: `lower_chdir()` @@ -40,4 +40,3 @@ function chdir(string $directory): bool ## 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 efb67cbf05..088d98a680 100644 --- a/docs/internals/builtins/filesystem/chgrp.md +++ b/docs/internals/builtins/filesystem/chgrp.md @@ -2,15 +2,15 @@ title: "chgrp() — internals" description: "Compiler internals for chgrp(): lowering path, type checks, and runtime helpers." sidebar: - order: 83 + order: 100 --- ## `chgrp()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3835](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3835) (`lower_chgrp`) +- **Signature**: [`src/builtins/io/chgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chgrp.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4478](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4478) (`lower_chgrp`) - **Function symbol**: `lower_chgrp()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function chgrp(string $filename, int $group): bool +function chgrp(string $filename, string $group): bool ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function chgrp(string $filename, int $group): bool ## 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 ec128c5d43..3b5e76e916 100644 --- a/docs/internals/builtins/filesystem/chmod.md +++ b/docs/internals/builtins/filesystem/chmod.md @@ -2,15 +2,15 @@ title: "chmod() — internals" description: "Compiler internals for chmod(): lowering path, type checks, and runtime helpers." sidebar: - order: 84 + order: 101 --- ## `chmod()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3825](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3825) (`lower_chmod`) +- **Signature**: [`src/builtins/io/chmod.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chmod.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4468](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4468) (`lower_chmod`) - **Function symbol**: `lower_chmod()` @@ -35,4 +35,3 @@ function chmod(string $filename, int $permissions): bool ## 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 b8b83876b7..ac86b3b4e9 100644 --- a/docs/internals/builtins/filesystem/chown.md +++ b/docs/internals/builtins/filesystem/chown.md @@ -2,15 +2,15 @@ title: "chown() — internals" description: "Compiler internals for chown(): lowering path, type checks, and runtime helpers." sidebar: - order: 85 + order: 102 --- ## `chown()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3830](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3830) (`lower_chown`) +- **Signature**: [`src/builtins/io/chown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chown.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4473](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4473) (`lower_chown`) - **Function symbol**: `lower_chown()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function chown(string $filename, int $user): bool +function chown(string $filename, string $user): bool ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function chown(string $filename, int $user): bool ## 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 45ff7c3bd8..2417f80455 100644 --- a/docs/internals/builtins/filesystem/clearstatcache.md +++ b/docs/internals/builtins/filesystem/clearstatcache.md @@ -2,15 +2,15 @@ title: "clearstatcache() — internals" description: "Compiler internals for clearstatcache(): lowering path, type checks, and runtime helpers." sidebar: - order: 86 + order: 103 --- ## `clearstatcache()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4935](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4935) (`lower_clearstatcache`) +- **Signature**: [`src/builtins/io/clearstatcache.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/clearstatcache.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5578](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5578) (`lower_clearstatcache`) - **Function symbol**: `lower_clearstatcache()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function clearstatcache(bool $clear_realpath_cache, string $filename): void +function clearstatcache(bool $clear_realpath_cache = false, string $filename = ''): void ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function clearstatcache(bool $clear_realpath_cache, string $filename): void ## 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 3ae63b649a..dfc16a31fa 100644 --- a/docs/internals/builtins/filesystem/copy.md +++ b/docs/internals/builtins/filesystem/copy.md @@ -2,15 +2,15 @@ title: "copy() — internals" description: "Compiler internals for copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 87 + order: 104 --- ## `copy()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3800](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3800) (`lower_copy`) +- **Signature**: [`src/builtins/io/copy.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/copy.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4443](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4443) (`lower_copy`) - **Function symbol**: `lower_copy()` @@ -29,14 +29,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function copy(string $from, string $to, mixed $context): bool +function copy(string $from, string $to): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 2 arguments. ## 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 3586eeeb02..6c38a29489 100644 --- a/docs/internals/builtins/filesystem/dirname.md +++ b/docs/internals/builtins/filesystem/dirname.md @@ -2,15 +2,15 @@ title: "dirname() — internals" description: "Compiler internals for dirname(): lowering path, type checks, and runtime helpers." sidebar: - order: 88 + order: 105 --- ## `dirname()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3932](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3932) (`lower_dirname`) +- **Signature**: [`src/builtins/io/dirname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/dirname.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4575](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4575) (`lower_dirname`) - **Function symbol**: `lower_dirname()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function dirname(string $path, int $levels): string +function dirname(string $path, int $levels = 1): string ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function dirname(string $path, int $levels): string ## 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 38eb6e1bf4..8243b6f2e8 100644 --- a/docs/internals/builtins/filesystem/disk_free_space.md +++ b/docs/internals/builtins/filesystem/disk_free_space.md @@ -2,15 +2,15 @@ title: "disk_free_space() — internals" description: "Compiler internals for disk_free_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 89 + order: 106 --- ## `disk_free_space()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3149](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3149) (`lower_disk_free_space`) +- **Signature**: [`src/builtins/io/disk_free_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/disk_free_space.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3370](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3370) (`lower_disk_free_space`) - **Function symbol**: `lower_disk_free_space()` @@ -36,4 +36,3 @@ function disk_free_space(string $directory): float ## 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 c3a3aba1fb..85d7f85e99 100644 --- a/docs/internals/builtins/filesystem/disk_total_space.md +++ b/docs/internals/builtins/filesystem/disk_total_space.md @@ -2,15 +2,15 @@ title: "disk_total_space() — internals" description: "Compiler internals for disk_total_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 90 + order: 107 --- ## `disk_total_space()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3157](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3157) (`lower_disk_total_space`) +- **Signature**: [`src/builtins/io/disk_total_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/disk_total_space.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3378](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3378) (`lower_disk_total_space`) - **Function symbol**: `lower_disk_total_space()` @@ -36,4 +36,3 @@ function disk_total_space(string $directory): float ## 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 843bf435be..a73bca3467 100644 --- a/docs/internals/builtins/filesystem/file_exists.md +++ b/docs/internals/builtins/filesystem/file_exists.md @@ -2,15 +2,15 @@ title: "file_exists() — internals" description: "Compiler internals for file_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 91 + order: 108 --- ## `file_exists()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3756](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3756) (`lower_file_exists`) +- **Signature**: [`src/builtins/io/file_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_exists.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4399](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4399) (`lower_file_exists`) - **Function symbol**: `lower_file_exists()` @@ -37,4 +37,3 @@ function file_exists(string $filename): bool ## 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 86d9c6c6fa..7233c8305e 100644 --- a/docs/internals/builtins/filesystem/fileatime.md +++ b/docs/internals/builtins/filesystem/fileatime.md @@ -2,15 +2,15 @@ title: "fileatime() — internals" description: "Compiler internals for fileatime(): lowering path, type checks, and runtime helpers." sidebar: - order: 92 + order: 109 --- ## `fileatime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4825](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4825) (`lower_fileatime`) +- **Signature**: [`src/builtins/io/fileatime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileatime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5468](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5468) (`lower_fileatime`) - **Function symbol**: `lower_fileatime()` @@ -39,4 +39,3 @@ function fileatime(string $filename): mixed ## 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 f5e40499b5..7731c01108 100644 --- a/docs/internals/builtins/filesystem/filectime.md +++ b/docs/internals/builtins/filesystem/filectime.md @@ -2,15 +2,15 @@ title: "filectime() — internals" description: "Compiler internals for filectime(): lowering path, type checks, and runtime helpers." sidebar: - order: 93 + order: 110 --- ## `filectime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4833](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4833) (`lower_filectime`) +- **Signature**: [`src/builtins/io/filectime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filectime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5476](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5476) (`lower_filectime`) - **Function symbol**: `lower_filectime()` @@ -39,4 +39,3 @@ function filectime(string $filename): mixed ## 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 cfdba2ed3d..ac29c14fc7 100644 --- a/docs/internals/builtins/filesystem/filegroup.md +++ b/docs/internals/builtins/filesystem/filegroup.md @@ -2,15 +2,15 @@ title: "filegroup() — internals" description: "Compiler internals for filegroup(): lowering path, type checks, and runtime helpers." sidebar: - order: 94 + order: 111 --- ## `filegroup()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4857](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4857) (`lower_filegroup`) +- **Signature**: [`src/builtins/io/filegroup.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filegroup.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5500](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5500) (`lower_filegroup`) - **Function symbol**: `lower_filegroup()` @@ -39,4 +39,3 @@ function filegroup(string $filename): mixed ## 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 7db6c6a68a..c7a99169fb 100644 --- a/docs/internals/builtins/filesystem/fileinode.md +++ b/docs/internals/builtins/filesystem/fileinode.md @@ -2,15 +2,15 @@ title: "fileinode() — internals" description: "Compiler internals for fileinode(): lowering path, type checks, and runtime helpers." sidebar: - order: 95 + order: 112 --- ## `fileinode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4865](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4865) (`lower_fileinode`) +- **Signature**: [`src/builtins/io/fileinode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileinode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5508](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5508) (`lower_fileinode`) - **Function symbol**: `lower_fileinode()` @@ -39,4 +39,3 @@ function fileinode(string $filename): mixed ## 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 02a2436f7b..6a800f621d 100644 --- a/docs/internals/builtins/filesystem/filemtime.md +++ b/docs/internals/builtins/filesystem/filemtime.md @@ -2,15 +2,15 @@ title: "filemtime() — internals" description: "Compiler internals for filemtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 96 + order: 113 --- ## `filemtime()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4789](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4789) (`lower_filemtime`) +- **Signature**: [`src/builtins/io/filemtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filemtime.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5432](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5432) (`lower_filemtime`) - **Function symbol**: `lower_filemtime()` @@ -40,4 +40,3 @@ function filemtime(string $filename): int ## 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 ec8843b075..e2be28445b 100644 --- a/docs/internals/builtins/filesystem/fileowner.md +++ b/docs/internals/builtins/filesystem/fileowner.md @@ -2,15 +2,15 @@ title: "fileowner() — internals" description: "Compiler internals for fileowner(): lowering path, type checks, and runtime helpers." sidebar: - order: 97 + order: 114 --- ## `fileowner()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4849](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4849) (`lower_fileowner`) +- **Signature**: [`src/builtins/io/fileowner.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileowner.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5492](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5492) (`lower_fileowner`) - **Function symbol**: `lower_fileowner()` @@ -38,4 +38,3 @@ function fileowner(string $filename): mixed ## 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 44c46a4746..cf6a95e897 100644 --- a/docs/internals/builtins/filesystem/fileperms.md +++ b/docs/internals/builtins/filesystem/fileperms.md @@ -2,15 +2,15 @@ title: "fileperms() — internals" description: "Compiler internals for fileperms(): lowering path, type checks, and runtime helpers." sidebar: - order: 98 + order: 115 --- ## `fileperms()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4841](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4841) (`lower_fileperms`) +- **Signature**: [`src/builtins/io/fileperms.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileperms.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5484](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5484) (`lower_fileperms`) - **Function symbol**: `lower_fileperms()` @@ -39,4 +39,3 @@ function fileperms(string $filename): mixed ## 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 f70542477c..38060c85ff 100644 --- a/docs/internals/builtins/filesystem/filesize.md +++ b/docs/internals/builtins/filesystem/filesize.md @@ -2,15 +2,15 @@ title: "filesize() — internals" description: "Compiler internals for filesize(): lowering path, type checks, and runtime helpers." sidebar: - order: 99 + order: 116 --- ## `filesize()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4781](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4781) (`lower_filesize`) +- **Signature**: [`src/builtins/io/filesize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filesize.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5424](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5424) (`lower_filesize`) - **Function symbol**: `lower_filesize()` @@ -39,4 +39,3 @@ function filesize(string $filename): int ## 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 ccbea7f9a9..c22edde1f6 100644 --- a/docs/internals/builtins/filesystem/filetype.md +++ b/docs/internals/builtins/filesystem/filetype.md @@ -2,15 +2,15 @@ title: "filetype() — internals" description: "Compiler internals for filetype(): lowering path, type checks, and runtime helpers." sidebar: - order: 100 + order: 117 --- ## `filetype()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4873](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4873) (`lower_filetype`) +- **Signature**: [`src/builtins/io/filetype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filetype.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5516](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5516) (`lower_filetype`) - **Function symbol**: `lower_filetype()` @@ -38,4 +38,3 @@ function filetype(string $filename): mixed ## 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 fc023122d6..1d00eb2c06 100644 --- a/docs/internals/builtins/filesystem/fnmatch.md +++ b/docs/internals/builtins/filesystem/fnmatch.md @@ -2,15 +2,15 @@ title: "fnmatch() — internals" description: "Compiler internals for fnmatch(): lowering path, type checks, and runtime helpers." sidebar: - order: 101 + order: 118 --- ## `fnmatch()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3960](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3960) (`lower_fnmatch`) +- **Signature**: [`src/builtins/io/fnmatch.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fnmatch.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4603](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4603) (`lower_fnmatch`) - **Function symbol**: `lower_fnmatch()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function fnmatch(string $pattern, string $filename, int $flags): bool +function fnmatch(string $pattern, string $filename, int $flags = 0): bool ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function fnmatch(string $pattern, string $filename, int $flags): bool ## 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 1653a8184a..9243fb1ab3 100644 --- a/docs/internals/builtins/filesystem/getcwd.md +++ b/docs/internals/builtins/filesystem/getcwd.md @@ -2,15 +2,15 @@ title: "getcwd() — internals" description: "Compiler internals for getcwd(): lowering path, type checks, and runtime helpers." sidebar: - order: 102 + order: 119 --- ## `getcwd()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4753](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4753) (`lower_getcwd`) +- **Signature**: [`src/builtins/io/getcwd.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getcwd.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5396](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5396) (`lower_getcwd`) - **Function symbol**: `lower_getcwd()` @@ -37,4 +37,3 @@ function getcwd(): string ## 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 c2e14e58db..0fac463983 100644 --- a/docs/internals/builtins/filesystem/getenv.md +++ b/docs/internals/builtins/filesystem/getenv.md @@ -2,15 +2,15 @@ title: "getenv() — internals" description: "Compiler internals for getenv(): lowering path, type checks, and runtime helpers." sidebar: - order: 103 + order: 120 --- ## `getenv()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:645](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L645) (`lower_getenv`) +- **Signature**: [`src/builtins/system/getenv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/getenv.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:645](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L645) (`lower_getenv`) - **Function symbol**: `lower_getenv()` @@ -26,14 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function getenv(string $name, bool $local_only): mixed +function getenv(string $name): mixed ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 6d6e582b00..f55d70dc80 100644 --- a/docs/internals/builtins/filesystem/glob.md +++ b/docs/internals/builtins/filesystem/glob.md @@ -2,15 +2,15 @@ title: "glob() — internals" description: "Compiler internals for glob(): lowering path, type checks, and runtime helpers." sidebar: - order: 104 + order: 121 --- ## `glob()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3820](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3820) (`lower_glob`) +- **Signature**: [`src/builtins/io/glob.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/glob.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4463](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4463) (`lower_glob`) - **Function symbol**: `lower_glob()` @@ -26,14 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function glob(string $pattern, int $flags): array +function glob(string $pattern): array ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 07e7e3ab02..8f699adcbb 100644 --- a/docs/internals/builtins/filesystem/is_dir.md +++ b/docs/internals/builtins/filesystem/is_dir.md @@ -2,15 +2,15 @@ title: "is_dir() — internals" description: "Compiler internals for is_dir(): lowering path, type checks, and runtime helpers." sidebar: - order: 105 + order: 122 --- ## `is_dir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4957](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4957) (`lower_is_dir`) +- **Signature**: [`src/builtins/io/is_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_dir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5600](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5600) (`lower_is_dir`) - **Function symbol**: `lower_is_dir()` @@ -38,4 +38,3 @@ function is_dir(string $filename): bool ## 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 584811a677..9550fdfcb2 100644 --- a/docs/internals/builtins/filesystem/is_executable.md +++ b/docs/internals/builtins/filesystem/is_executable.md @@ -2,15 +2,15 @@ title: "is_executable() — internals" description: "Compiler internals for is_executable(): lowering path, type checks, and runtime helpers." sidebar: - order: 106 + order: 123 --- ## `is_executable()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4989](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4989) (`lower_is_executable`) +- **Signature**: [`src/builtins/io/is_executable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_executable.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5632](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5632) (`lower_is_executable`) - **Function symbol**: `lower_is_executable()` @@ -39,4 +39,3 @@ function is_executable(string $filename): bool ## 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 a9b04ae600..4b2a35ea45 100644 --- a/docs/internals/builtins/filesystem/is_file.md +++ b/docs/internals/builtins/filesystem/is_file.md @@ -2,15 +2,15 @@ title: "is_file() — internals" description: "Compiler internals for is_file(): lowering path, type checks, and runtime helpers." sidebar: - order: 107 + order: 124 --- ## `is_file()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4949](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4949) (`lower_is_file`) +- **Signature**: [`src/builtins/io/is_file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_file.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5592](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5592) (`lower_is_file`) - **Function symbol**: `lower_is_file()` @@ -38,4 +38,3 @@ function is_file(string $filename): bool ## 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 2810bbd883..26df330e09 100644 --- a/docs/internals/builtins/filesystem/is_link.md +++ b/docs/internals/builtins/filesystem/is_link.md @@ -2,15 +2,15 @@ title: "is_link() — internals" description: "Compiler internals for is_link(): lowering path, type checks, and runtime helpers." sidebar: - order: 108 + order: 125 --- ## `is_link()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4997](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4997) (`lower_is_link`) +- **Signature**: [`src/builtins/io/is_link.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_link.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5640](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5640) (`lower_is_link`) - **Function symbol**: `lower_is_link()` @@ -39,4 +39,3 @@ function is_link(string $filename): bool ## 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 ada5dc3167..0e61b1965f 100644 --- a/docs/internals/builtins/filesystem/is_readable.md +++ b/docs/internals/builtins/filesystem/is_readable.md @@ -2,15 +2,15 @@ title: "is_readable() — internals" description: "Compiler internals for is_readable(): lowering path, type checks, and runtime helpers." sidebar: - order: 109 + order: 126 --- ## `is_readable()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4965](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4965) (`lower_is_readable`) +- **Signature**: [`src/builtins/io/is_readable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_readable.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5608](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5608) (`lower_is_readable`) - **Function symbol**: `lower_is_readable()` @@ -38,4 +38,3 @@ function is_readable(string $filename): bool ## 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 ebe3f17694..f20c80490c 100644 --- a/docs/internals/builtins/filesystem/is_writable.md +++ b/docs/internals/builtins/filesystem/is_writable.md @@ -2,15 +2,15 @@ title: "is_writable() — internals" description: "Compiler internals for is_writable(): lowering path, type checks, and runtime helpers." sidebar: - order: 110 + order: 127 --- ## `is_writable()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4973](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4973) (`lower_is_writable`) +- **Signature**: [`src/builtins/io/is_writable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_writable.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5616](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5616) (`lower_is_writable`) - **Function symbol**: `lower_is_writable()` @@ -38,4 +38,3 @@ function is_writable(string $filename): bool ## 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 cb88216284..906ddbb7fe 100644 --- a/docs/internals/builtins/filesystem/is_writeable.md +++ b/docs/internals/builtins/filesystem/is_writeable.md @@ -2,15 +2,15 @@ title: "is_writeable() — internals" description: "Compiler internals for is_writeable(): lowering path, type checks, and runtime helpers." sidebar: - order: 111 + order: 128 --- ## `is_writeable()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4981](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4981) (`lower_is_writeable`) +- **Signature**: [`src/builtins/io/is_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_writeable.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5624](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5624) (`lower_is_writeable`) - **Function symbol**: `lower_is_writeable()` @@ -38,4 +38,3 @@ function is_writeable(string $filename): bool ## 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 7ebb947b94..054d954188 100644 --- a/docs/internals/builtins/filesystem/lchgrp.md +++ b/docs/internals/builtins/filesystem/lchgrp.md @@ -2,15 +2,15 @@ title: "lchgrp() — internals" description: "Compiler internals for lchgrp(): lowering path, type checks, and runtime helpers." sidebar: - order: 112 + order: 129 --- ## `lchgrp()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3845](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3845) (`lower_lchgrp`) +- **Signature**: [`src/builtins/io/lchgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lchgrp.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4488](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4488) (`lower_lchgrp`) - **Function symbol**: `lower_lchgrp()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function lchgrp(string $filename, int $group): bool +function lchgrp(string $filename, string $group): bool ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function lchgrp(string $filename, int $group): bool ## 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 8bc4487266..d4f2cf70d6 100644 --- a/docs/internals/builtins/filesystem/lchown.md +++ b/docs/internals/builtins/filesystem/lchown.md @@ -2,15 +2,15 @@ title: "lchown() — internals" description: "Compiler internals for lchown(): lowering path, type checks, and runtime helpers." sidebar: - order: 113 + order: 130 --- ## `lchown()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3840](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3840) (`lower_lchown`) +- **Signature**: [`src/builtins/io/lchown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lchown.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4483](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4483) (`lower_lchown`) - **Function symbol**: `lower_lchown()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function lchown(string $filename, int $user): bool +function lchown(string $filename, string $user): bool ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function lchown(string $filename, int $user): bool ## 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 8e07bc9b6e..4e2421a4cd 100644 --- a/docs/internals/builtins/filesystem/link.md +++ b/docs/internals/builtins/filesystem/link.md @@ -2,15 +2,15 @@ title: "link() — internals" description: "Compiler internals for link(): lowering path, type checks, and runtime helpers." sidebar: - order: 114 + order: 131 --- ## `link()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4810](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4810) (`lower_link`) +- **Signature**: [`src/builtins/io/link.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/link.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5453](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5453) (`lower_link`) - **Function symbol**: `lower_link()` @@ -39,4 +39,3 @@ function link(string $target, string $link): bool ## 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 d62186b9e1..bec27ff255 100644 --- a/docs/internals/builtins/filesystem/linkinfo.md +++ b/docs/internals/builtins/filesystem/linkinfo.md @@ -2,15 +2,15 @@ title: "linkinfo() — internals" description: "Compiler internals for linkinfo(): lowering path, type checks, and runtime helpers." sidebar: - order: 115 + order: 132 --- ## `linkinfo()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4797](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4797) (`lower_linkinfo`) +- **Signature**: [`src/builtins/io/linkinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/linkinfo.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5440](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5440) (`lower_linkinfo`) - **Function symbol**: `lower_linkinfo()` @@ -39,4 +39,3 @@ function linkinfo(string $path): int ## 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 bc2bf1c008..1adfae4bb3 100644 --- a/docs/internals/builtins/filesystem/lstat.md +++ b/docs/internals/builtins/filesystem/lstat.md @@ -2,15 +2,15 @@ title: "lstat() — internals" description: "Compiler internals for lstat(): lowering path, type checks, and runtime helpers." sidebar: - order: 116 + order: 133 --- ## `lstat()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4891](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4891) (`lower_lstat`) +- **Signature**: [`src/builtins/io/lstat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lstat.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5534](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5534) (`lower_lstat`) - **Function symbol**: `lower_lstat()` @@ -37,4 +37,3 @@ function lstat(string $filename): mixed ## 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 b5bab2aee2..4bc31dbde1 100644 --- a/docs/internals/builtins/filesystem/mkdir.md +++ b/docs/internals/builtins/filesystem/mkdir.md @@ -2,15 +2,15 @@ title: "mkdir() — internals" description: "Compiler internals for mkdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 117 + order: 134 --- ## `mkdir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3785](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3785) (`lower_mkdir`) +- **Signature**: [`src/builtins/io/mkdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/mkdir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4428](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4428) (`lower_mkdir`) - **Function symbol**: `lower_mkdir()` @@ -30,14 +30,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function mkdir(string $directory, int $permissions, bool $recursive, bool $context): bool +function mkdir(string $directory): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 4 arguments. +- **Arity**: takes exactly 1 argument. ## 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 f276e07d5f..ebd9883a0c 100644 --- a/docs/internals/builtins/filesystem/pathinfo.md +++ b/docs/internals/builtins/filesystem/pathinfo.md @@ -2,15 +2,15 @@ title: "pathinfo() — internals" description: "Compiler internals for pathinfo(): lowering path, type checks, and runtime helpers." sidebar: - order: 118 + order: 135 --- ## `pathinfo()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4001](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4001) (`lower_pathinfo`) +- **Signature**: [`src/builtins/io/pathinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pathinfo.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4644](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4644) (`lower_pathinfo`) - **Function symbol**: `lower_pathinfo()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function pathinfo(string $path, int $flags): mixed +function pathinfo(string $path, int $flags = 15): array ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function pathinfo(string $path, int $flags): mixed ## 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 a6e1716242..39db571ebf 100644 --- a/docs/internals/builtins/filesystem/putenv.md +++ b/docs/internals/builtins/filesystem/putenv.md @@ -2,15 +2,15 @@ title: "putenv() — internals" description: "Compiler internals for putenv(): lowering path, type checks, and runtime helpers." sidebar: - order: 119 + order: 136 --- ## `putenv()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:657](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L657) (`lower_putenv`) +- **Signature**: [`src/builtins/system/putenv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/putenv.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:657](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L657) (`lower_putenv`) - **Function symbol**: `lower_putenv()` @@ -36,4 +36,3 @@ function putenv(string $assignment): bool ## 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 594785f43e..ba9ab3f8b9 100644 --- a/docs/internals/builtins/filesystem/readfile.md +++ b/docs/internals/builtins/filesystem/readfile.md @@ -2,15 +2,15 @@ title: "readfile() — internals" description: "Compiler internals for readfile(): lowering path, type checks, and runtime helpers." sidebar: - order: 120 + order: 137 --- ## `readfile()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:193](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L193) (`lower_readfile`) +- **Signature**: [`src/builtins/io/readfile.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readfile.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:300](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L300) (`lower_readfile`) - **Function symbol**: `lower_readfile()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function readfile(string $filename, bool $use_include_path, mixed $context): mixed +function readfile(string $filename): mixed ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 1 argument. ## 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 cd37d759cd..9b67cd14b3 100644 --- a/docs/internals/builtins/filesystem/readlink.md +++ b/docs/internals/builtins/filesystem/readlink.md @@ -2,15 +2,15 @@ title: "readlink() — internals" description: "Compiler internals for readlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 121 + order: 138 --- ## `readlink()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4815](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4815) (`lower_readlink`) +- **Signature**: [`src/builtins/io/readlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readlink.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5458](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5458) (`lower_readlink`) - **Function symbol**: `lower_readlink()` @@ -39,4 +39,3 @@ function readlink(string $path): mixed ## 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 c47a307d45..eaee752b33 100644 --- a/docs/internals/builtins/filesystem/realpath.md +++ b/docs/internals/builtins/filesystem/realpath.md @@ -2,15 +2,15 @@ title: "realpath() — internals" description: "Compiler internals for realpath(): lowering path, type checks, and runtime helpers." sidebar: - order: 122 + order: 139 --- ## `realpath()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3465](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3465) (`lower_realpath`) +- **Signature**: [`src/builtins/io/realpath.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3690](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3690) (`lower_realpath`) - **Function symbol**: `lower_realpath()` @@ -36,4 +36,3 @@ function realpath(string $path): mixed ## 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 9dec55a0ba..e32cb410b5 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_get.md +++ b/docs/internals/builtins/filesystem/realpath_cache_get.md @@ -2,15 +2,15 @@ title: "realpath_cache_get() — internals" description: "Compiler internals for realpath_cache_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 123 + order: 140 --- ## `realpath_cache_get()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3475](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3475) (`lower_realpath_cache_get`) +- **Signature**: [`src/builtins/io/realpath_cache_get.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath_cache_get.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3700](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3700) (`lower_realpath_cache_get`) - **Function symbol**: `lower_realpath_cache_get()` @@ -35,4 +35,3 @@ function realpath_cache_get(): array ## 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 19b0b4e179..113cb67e63 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_size.md +++ b/docs/internals/builtins/filesystem/realpath_cache_size.md @@ -2,15 +2,15 @@ title: "realpath_cache_size() — internals" description: "Compiler internals for realpath_cache_size(): lowering path, type checks, and runtime helpers." sidebar: - order: 124 + order: 141 --- ## `realpath_cache_size()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3485](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3485) (`lower_realpath_cache_size`) +- **Signature**: [`src/builtins/io/realpath_cache_size.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath_cache_size.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3710](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3710) (`lower_realpath_cache_size`) - **Function symbol**: `lower_realpath_cache_size()` @@ -35,4 +35,3 @@ function realpath_cache_size(): int ## 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 b743a82d5c..33acbcfc62 100644 --- a/docs/internals/builtins/filesystem/rename.md +++ b/docs/internals/builtins/filesystem/rename.md @@ -2,15 +2,15 @@ title: "rename() — internals" description: "Compiler internals for rename(): lowering path, type checks, and runtime helpers." sidebar: - order: 125 + order: 142 --- ## `rename()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3805](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3805) (`lower_rename`) +- **Signature**: [`src/builtins/io/rename.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rename.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4448](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4448) (`lower_rename`) - **Function symbol**: `lower_rename()` @@ -28,14 +28,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function rename(string $from, string $to, mixed $context): bool +function rename(string $from, string $to): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 2 arguments. ## 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 7fb455562d..6add2d8250 100644 --- a/docs/internals/builtins/filesystem/rmdir.md +++ b/docs/internals/builtins/filesystem/rmdir.md @@ -2,15 +2,15 @@ title: "rmdir() — internals" description: "Compiler internals for rmdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 126 + order: 143 --- ## `rmdir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3790](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3790) (`lower_rmdir`) +- **Signature**: [`src/builtins/io/rmdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rmdir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4433](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4433) (`lower_rmdir`) - **Function symbol**: `lower_rmdir()` @@ -30,14 +30,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function rmdir(string $directory, mixed $context = null): bool +function rmdir(string $directory): bool ``` ## What the type checker enforces -- **Arity**: takes 1–2 arguments (1 optional). +- **Arity**: takes exactly 1 argument. ## 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 bdceddebf0..1225c0afbe 100644 --- a/docs/internals/builtins/filesystem/scandir.md +++ b/docs/internals/builtins/filesystem/scandir.md @@ -2,15 +2,15 @@ title: "scandir() — internals" description: "Compiler internals for scandir(): lowering path, type checks, and runtime helpers." sidebar: - order: 127 + order: 144 --- ## `scandir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3815](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3815) (`lower_scandir`) +- **Signature**: [`src/builtins/io/scandir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/scandir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4458](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4458) (`lower_scandir`) - **Function symbol**: `lower_scandir()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function scandir(string $directory, int $sorting_order, mixed $context): array +function scandir(string $directory): array ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 1 argument. ## 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 a67e1a45a2..0969fc70a2 100644 --- a/docs/internals/builtins/filesystem/stat.md +++ b/docs/internals/builtins/filesystem/stat.md @@ -2,15 +2,15 @@ title: "stat() — internals" description: "Compiler internals for stat(): lowering path, type checks, and runtime helpers." sidebar: - order: 128 + order: 145 --- ## `stat()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4886](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4886) (`lower_stat`) +- **Signature**: [`src/builtins/io/stat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stat.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5529](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5529) (`lower_stat`) - **Function symbol**: `lower_stat()` @@ -38,4 +38,3 @@ function stat(string $filename): mixed ## 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 c2a0df2600..4930e27f74 100644 --- a/docs/internals/builtins/filesystem/symlink.md +++ b/docs/internals/builtins/filesystem/symlink.md @@ -2,15 +2,15 @@ title: "symlink() — internals" description: "Compiler internals for symlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 129 + order: 146 --- ## `symlink()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4805](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4805) (`lower_symlink`) +- **Signature**: [`src/builtins/io/symlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/symlink.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5448](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5448) (`lower_symlink`) - **Function symbol**: `lower_symlink()` @@ -39,4 +39,3 @@ function symlink(string $target, string $link): bool ## 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 f2e4c85954..7c45622094 100644 --- a/docs/internals/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/internals/builtins/filesystem/sys_get_temp_dir.md @@ -2,15 +2,15 @@ title: "sys_get_temp_dir() — internals" description: "Compiler internals for sys_get_temp_dir(): lowering path, type checks, and runtime helpers." sidebar: - order: 130 + order: 147 --- ## `sys_get_temp_dir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4760](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4760) (`lower_sys_get_temp_dir`) +- **Signature**: [`src/builtins/io/sys_get_temp_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/sys_get_temp_dir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5403](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5403) (`lower_sys_get_temp_dir`) - **Function symbol**: `lower_sys_get_temp_dir()` @@ -36,4 +36,3 @@ function sys_get_temp_dir(): string ## 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 ba83498b41..d1de170ba3 100644 --- a/docs/internals/builtins/filesystem/tempnam.md +++ b/docs/internals/builtins/filesystem/tempnam.md @@ -2,15 +2,15 @@ title: "tempnam() — internals" description: "Compiler internals for tempnam(): lowering path, type checks, and runtime helpers." sidebar: - order: 131 + order: 148 --- ## `tempnam()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3810](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3810) (`lower_tempnam`) +- **Signature**: [`src/builtins/io/tempnam.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/tempnam.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4453](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4453) (`lower_tempnam`) - **Function symbol**: `lower_tempnam()` @@ -38,4 +38,3 @@ function tempnam(string $directory, string $prefix): string ## 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 d9d299c1ad..1c72e490e0 100644 --- a/docs/internals/builtins/filesystem/tmpfile.md +++ b/docs/internals/builtins/filesystem/tmpfile.md @@ -2,15 +2,15 @@ title: "tmpfile() — internals" description: "Compiler internals for tmpfile(): lowering path, type checks, and runtime helpers." sidebar: - order: 132 + order: 149 --- ## `tmpfile()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4773](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4773) (`lower_tmpfile`) +- **Signature**: [`src/builtins/io/tmpfile.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/tmpfile.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5416](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5416) (`lower_tmpfile`) - **Function symbol**: `lower_tmpfile()` @@ -38,4 +38,3 @@ function tmpfile(): mixed ## 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 8d95d2188c..9a4ebb029c 100644 --- a/docs/internals/builtins/filesystem/touch.md +++ b/docs/internals/builtins/filesystem/touch.md @@ -2,15 +2,15 @@ title: "touch() — internals" description: "Compiler internals for touch(): lowering path, type checks, and runtime helpers." sidebar: - order: 133 + order: 150 --- ## `touch()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3880](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3880) (`lower_touch`) +- **Signature**: [`src/builtins/io/touch.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/touch.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4523](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4523) (`lower_touch`) - **Function symbol**: `lower_touch()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function touch(string $filename, int $mtime, int $atime): bool +function touch(string $filename, int $mtime = null, int $atime = null): bool ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function touch(string $filename, int $mtime, int $atime): bool ## 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 c4cbd0d73f..50b45bae18 100644 --- a/docs/internals/builtins/filesystem/umask.md +++ b/docs/internals/builtins/filesystem/umask.md @@ -2,15 +2,15 @@ title: "umask() — internals" description: "Compiler internals for umask(): lowering path, type checks, and runtime helpers." sidebar: - order: 134 + order: 151 --- ## `umask()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3850](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3850) (`lower_umask`) +- **Signature**: [`src/builtins/io/umask.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/umask.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4493](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4493) (`lower_umask`) - **Function symbol**: `lower_umask()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function umask(int $mask): int +function umask(int $mask = null): int ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function umask(int $mask): int ## 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 ee48594f90..fb9df6ff4a 100644 --- a/docs/internals/builtins/filesystem/unlink.md +++ b/docs/internals/builtins/filesystem/unlink.md @@ -2,15 +2,15 @@ title: "unlink() — internals" description: "Compiler internals for unlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 135 + order: 152 --- ## `unlink()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3764](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3764) (`lower_unlink`) +- **Signature**: [`src/builtins/io/unlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/unlink.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:4407](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L4407) (`lower_unlink`) - **Function symbol**: `lower_unlink()` @@ -38,4 +38,3 @@ function unlink(string $filename): bool ## 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 228c19b98a..4a8ad88cc8 100644 --- a/docs/internals/builtins/io/closedir.md +++ b/docs/internals/builtins/io/closedir.md @@ -2,15 +2,15 @@ title: "closedir() — internals" description: "Compiler internals for closedir(): lowering path, type checks, and runtime helpers." sidebar: - order: 136 + order: 153 --- ## `closedir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3351](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3351) (`lower_closedir`) +- **Signature**: [`src/builtins/io/closedir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/closedir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3572](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3572) (`lower_closedir`) - **Function symbol**: `lower_closedir()` @@ -39,4 +39,3 @@ function closedir(resource $dir_handle): void ## 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 b198d7d33d..07d61b2ec2 100644 --- a/docs/internals/builtins/io/fclose.md +++ b/docs/internals/builtins/io/fclose.md @@ -2,15 +2,15 @@ title: "fclose() — internals" description: "Compiler internals for fclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 137 + order: 154 --- ## `fclose()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2488](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2488) (`lower_fclose`) +- **Signature**: [`src/builtins/io/fclose.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fclose.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2707](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2707) (`lower_fclose`) - **Function symbol**: `lower_fclose()` @@ -35,4 +35,3 @@ function fclose(resource $stream): bool ## 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 255d86aa87..43ad181d29 100644 --- a/docs/internals/builtins/io/fdatasync.md +++ b/docs/internals/builtins/io/fdatasync.md @@ -2,15 +2,15 @@ title: "fdatasync() — internals" description: "Compiler internals for fdatasync(): lowering path, type checks, and runtime helpers." sidebar: - order: 138 + order: 155 --- ## `fdatasync()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3083](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3083) (`lower_fdatasync`) +- **Signature**: [`src/builtins/io/fdatasync.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fdatasync.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3304](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3304) (`lower_fdatasync`) - **Function symbol**: `lower_fdatasync()` @@ -36,4 +36,3 @@ function fdatasync(resource $stream): bool ## 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 8b90419d3c..e24c273b01 100644 --- a/docs/internals/builtins/io/feof.md +++ b/docs/internals/builtins/io/feof.md @@ -2,15 +2,15 @@ title: "feof() — internals" description: "Compiler internals for feof(): lowering path, type checks, and runtime helpers." sidebar: - order: 139 + order: 156 --- ## `feof()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2900](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2900) (`lower_feof`) +- **Signature**: [`src/builtins/io/feof.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/feof.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3121](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3121) (`lower_feof`) - **Function symbol**: `lower_feof()` @@ -37,4 +37,3 @@ function feof(resource $stream): bool ## 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 207a4723b8..2fc2d1aade 100644 --- a/docs/internals/builtins/io/fflush.md +++ b/docs/internals/builtins/io/fflush.md @@ -2,15 +2,15 @@ title: "fflush() — internals" description: "Compiler internals for fflush(): lowering path, type checks, and runtime helpers." sidebar: - order: 140 + order: 157 --- ## `fflush()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3045](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3045) (`lower_fflush`) +- **Signature**: [`src/builtins/io/fflush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fflush.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3266](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3266) (`lower_fflush`) - **Function symbol**: `lower_fflush()` @@ -36,4 +36,3 @@ function fflush(resource $stream): bool ## 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 0895a2d6f8..8a79b3f773 100644 --- a/docs/internals/builtins/io/fgetc.md +++ b/docs/internals/builtins/io/fgetc.md @@ -2,15 +2,15 @@ title: "fgetc() — internals" description: "Compiler internals for fgetc(): lowering path, type checks, and runtime helpers." sidebar: - order: 141 + order: 158 --- ## `fgetc()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2759](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2759) (`lower_fgetc`) +- **Signature**: [`src/builtins/io/fgetc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgetc.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2980](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2980) (`lower_fgetc`) - **Function symbol**: `lower_fgetc()` @@ -37,4 +37,3 @@ function fgetc(resource $stream): mixed ## 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 7498ea8a72..9e7afe5ddd 100644 --- a/docs/internals/builtins/io/fgetcsv.md +++ b/docs/internals/builtins/io/fgetcsv.md @@ -2,15 +2,15 @@ title: "fgetcsv() — internals" description: "Compiler internals for fgetcsv(): lowering path, type checks, and runtime helpers." sidebar: - order: 142 + order: 159 --- ## `fgetcsv()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2772](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2772) (`lower_fgetcsv`) +- **Signature**: [`src/builtins/io/fgetcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgetcsv.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2993](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2993) (`lower_fgetcsv`) - **Function symbol**: `lower_fgetcsv()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function fgetcsv(resource $stream, int $length, string $separator, string $enclosure, string $escape): array +function fgetcsv(resource $stream, int $length = null, string $separator = ','): array ``` ## What the type checker enforces -- **Arity**: takes 3–5 arguments (2 optional). +- **Arity**: takes 1–3 arguments (2 optional). ## 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 dcfad29556..b87a1aeb18 100644 --- a/docs/internals/builtins/io/fgets.md +++ b/docs/internals/builtins/io/fgets.md @@ -2,15 +2,15 @@ title: "fgets() — internals" description: "Compiler internals for fgets(): lowering path, type checks, and runtime helpers." sidebar: - order: 143 + order: 160 --- ## `fgets()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2746](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2746) (`lower_fgets`) +- **Signature**: [`src/builtins/io/fgets.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgets.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2967](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2967) (`lower_fgets`) - **Function symbol**: `lower_fgets()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function fgets(resource $stream, int $length): mixed +function fgets(resource $stream): mixed ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 6aa5cf1907..53664951e9 100644 --- a/docs/internals/builtins/io/file.md +++ b/docs/internals/builtins/io/file.md @@ -2,15 +2,15 @@ title: "file() — internals" description: "Compiler internals for file(): lowering path, type checks, and runtime helpers." sidebar: - order: 144 + order: 161 --- ## `file()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3460](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3460) (`lower_file`) +- **Signature**: [`src/builtins/io/file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3685](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3685) (`lower_file`) - **Function symbol**: `lower_file()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function file(string $filename, int $flags, mixed $context): array +function file(string $filename): array ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 1 argument. ## 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 a39f076629..341051a308 100644 --- a/docs/internals/builtins/io/file_get_contents.md +++ b/docs/internals/builtins/io/file_get_contents.md @@ -2,15 +2,15 @@ title: "file_get_contents() — internals" description: "Compiler internals for file_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 145 + order: 162 --- ## `file_get_contents()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:39](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L39) (`lower_file_get_contents`) +- **Signature**: [`src/builtins/io/file_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_get_contents.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:39](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L39) (`lower_file_get_contents`) - **Function symbol**: `lower_file_get_contents()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function file_get_contents(string $filename, bool $use_include_path, mixed $context, int $offset, int $length): mixed +function file_get_contents(string $filename): mixed ``` ## What the type checker enforces -- **Arity**: takes exactly 5 arguments. +- **Arity**: takes exactly 1 argument. ## 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 3334689517..a0dd17d11b 100644 --- a/docs/internals/builtins/io/file_put_contents.md +++ b/docs/internals/builtins/io/file_put_contents.md @@ -2,15 +2,15 @@ title: "file_put_contents() — internals" description: "Compiler internals for file_put_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 146 + order: 163 --- ## `file_put_contents()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3502](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3502) (`lower_file_put_contents`) +- **Signature**: [`src/builtins/io/file_put_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_put_contents.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3727](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3727) (`lower_file_put_contents`) - **Function symbol**: `lower_file_put_contents()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function file_put_contents(string $filename, mixed $data, int $flags = 0, mixed $context = null): int +function file_put_contents(string $filename, string $data): int ``` ## What the type checker enforces -- **Arity**: takes 2–4 arguments (2 optional). +- **Arity**: takes exactly 2 arguments. ## 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 9253d4f6df..05e1b70504 100644 --- a/docs/internals/builtins/io/flock.md +++ b/docs/internals/builtins/io/flock.md @@ -2,15 +2,15 @@ title: "flock() — internals" description: "Compiler internals for flock(): lowering path, type checks, and runtime helpers." sidebar: - order: 147 + order: 164 --- ## `flock()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3088](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3088) (`lower_flock`) +- **Signature**: [`src/builtins/io/flock.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/flock.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3309](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3309) (`lower_flock`) - **Function symbol**: `lower_flock()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function flock(resource $stream, int $operation, bool $would_block): bool +function flock(resource $stream, int $operation, bool $would_block = null): bool ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function flock(resource $stream, int $operation, bool $would_block): bool ## 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 9caf8c0787..f4af5776b2 100644 --- a/docs/internals/builtins/io/fopen.md +++ b/docs/internals/builtins/io/fopen.md @@ -2,15 +2,15 @@ title: "fopen() — internals" description: "Compiler internals for fopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 148 + order: 165 --- ## `fopen()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:233](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L233) (`lower_fopen`) +- **Signature**: [`src/builtins/io/fopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fopen.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:340](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L340) (`lower_fopen`) - **Function symbol**: `lower_fopen()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function fopen(string $filename, string $mode, bool $use_include_path, mixed $context): mixed +function fopen(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function fopen(string $filename, string $mode, bool $use_include_path, mixed $co ## 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 366176778b..fc5dd43810 100644 --- a/docs/internals/builtins/io/fpassthru.md +++ b/docs/internals/builtins/io/fpassthru.md @@ -2,15 +2,15 @@ title: "fpassthru() — internals" description: "Compiler internals for fpassthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 149 + order: 166 --- ## `fpassthru()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2806](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2806) (`lower_fpassthru`) +- **Signature**: [`src/builtins/io/fpassthru.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fpassthru.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3027](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3027) (`lower_fpassthru`) - **Function symbol**: `lower_fpassthru()` @@ -37,4 +37,3 @@ function fpassthru(resource $stream): int ## 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 9a6dd0c3b4..25c68e9a11 100644 --- a/docs/internals/builtins/io/fprintf.md +++ b/docs/internals/builtins/io/fprintf.md @@ -2,15 +2,15 @@ title: "fprintf() — internals" description: "Compiler internals for fprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 150 + order: 167 --- ## `fprintf()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2641](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2641) (`lower_fprintf`) +- **Signature**: [`src/builtins/io/fprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fprintf.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2862](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2862) (`lower_fprintf`) - **Function symbol**: `lower_fprintf()` @@ -37,4 +37,3 @@ function fprintf(resource $stream, string $format, ...$values): int ## 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 88cfb17351..88071b7e26 100644 --- a/docs/internals/builtins/io/fputcsv.md +++ b/docs/internals/builtins/io/fputcsv.md @@ -2,15 +2,15 @@ title: "fputcsv() — internals" description: "Compiler internals for fputcsv(): lowering path, type checks, and runtime helpers." sidebar: - order: 151 + order: 168 --- ## `fputcsv()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2784](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2784) (`lower_fputcsv`) +- **Signature**: [`src/builtins/io/fputcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fputcsv.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3005](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3005) (`lower_fputcsv`) - **Function symbol**: `lower_fputcsv()` @@ -26,14 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function fputcsv(resource $stream, array $fields, string $separator = ',', string $enclosure = '"', string $escape = '\\', string $eol = '\n'): int +function fputcsv(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int ``` ## What the type checker enforces -- **Arity**: takes 2–6 arguments (4 optional). +- **Arity**: takes 2–4 arguments (2 optional). ## 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 5469f8cd75..3718fd98c9 100644 --- a/docs/internals/builtins/io/fread.md +++ b/docs/internals/builtins/io/fread.md @@ -2,15 +2,15 @@ title: "fread() — internals" description: "Compiler internals for fread(): lowering path, type checks, and runtime helpers." sidebar: - order: 152 + order: 169 --- ## `fread()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2595](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2595) (`lower_fread`) +- **Signature**: [`src/builtins/io/fread.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fread.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2816](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2816) (`lower_fread`) - **Function symbol**: `lower_fread()` @@ -36,4 +36,3 @@ function fread(resource $stream, int $length): string ## 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 a011eac1cc..ac174d4b98 100644 --- a/docs/internals/builtins/io/fscanf.md +++ b/docs/internals/builtins/io/fscanf.md @@ -2,15 +2,15 @@ title: "fscanf() — internals" description: "Compiler internals for fscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 153 + order: 170 --- ## `fscanf()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2717](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2717) (`lower_fscanf`) +- **Signature**: [`src/builtins/io/fscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fscanf.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2938](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2938) (`lower_fscanf`) - **Function symbol**: `lower_fscanf()` @@ -38,4 +38,3 @@ function fscanf(resource $stream, string $format, ...$vars): array ## 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 934711515c..ce92f7c77f 100644 --- a/docs/internals/builtins/io/fseek.md +++ b/docs/internals/builtins/io/fseek.md @@ -2,15 +2,15 @@ title: "fseek() — internals" description: "Compiler internals for fseek(): lowering path, type checks, and runtime helpers." sidebar: - order: 154 + order: 171 --- ## `fseek()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2951](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2951) (`lower_fseek`) +- **Signature**: [`src/builtins/io/fseek.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fseek.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3172](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3172) (`lower_fseek`) - **Function symbol**: `lower_fseek()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function fseek(resource $stream, int $offset, int $whence): int +function fseek(resource $stream, int $offset, int $whence = 0): int ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function fseek(resource $stream, int $offset, int $whence): int ## 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 8d2660c612..b92d1d8b31 100644 --- a/docs/internals/builtins/io/fstat.md +++ b/docs/internals/builtins/io/fstat.md @@ -2,15 +2,15 @@ title: "fstat() — internals" description: "Compiler internals for fstat(): lowering path, type checks, and runtime helpers." sidebar: - order: 155 + order: 172 --- ## `fstat()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:4896](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L4896) (`lower_fstat`) +- **Signature**: [`src/builtins/io/fstat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fstat.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:5539](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L5539) (`lower_fstat`) - **Function symbol**: `lower_fstat()` @@ -36,4 +36,3 @@ function fstat(resource $stream): mixed ## 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 b51b8e59eb..5338aab7cd 100644 --- a/docs/internals/builtins/io/fsync.md +++ b/docs/internals/builtins/io/fsync.md @@ -2,15 +2,15 @@ title: "fsync() — internals" description: "Compiler internals for fsync(): lowering path, type checks, and runtime helpers." sidebar: - order: 156 + order: 173 --- ## `fsync()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3040](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3040) (`lower_fsync`) +- **Signature**: [`src/builtins/io/fsync.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fsync.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3261](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3261) (`lower_fsync`) - **Function symbol**: `lower_fsync()` @@ -37,4 +37,3 @@ function fsync(resource $stream): bool ## 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 7905e87db3..fce93e4672 100644 --- a/docs/internals/builtins/io/ftell.md +++ b/docs/internals/builtins/io/ftell.md @@ -2,15 +2,15 @@ title: "ftell() — internals" description: "Compiler internals for ftell(): lowering path, type checks, and runtime helpers." sidebar: - order: 157 + order: 174 --- ## `ftell()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2912](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2912) (`lower_ftell`) +- **Signature**: [`src/builtins/io/ftell.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ftell.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3133](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3133) (`lower_ftell`) - **Function symbol**: `lower_ftell()` @@ -36,4 +36,3 @@ function ftell(resource $stream): int ## 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 223ee36ee7..8a71ce2a37 100644 --- a/docs/internals/builtins/io/ftruncate.md +++ b/docs/internals/builtins/io/ftruncate.md @@ -2,15 +2,15 @@ title: "ftruncate() — internals" description: "Compiler internals for ftruncate(): lowering path, type checks, and runtime helpers." sidebar: - order: 158 + order: 175 --- ## `ftruncate()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2989](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2989) (`lower_ftruncate`) +- **Signature**: [`src/builtins/io/ftruncate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ftruncate.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3210](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3210) (`lower_ftruncate`) - **Function symbol**: `lower_ftruncate()` @@ -35,4 +35,3 @@ function ftruncate(resource $stream, int $size): bool ## 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 1e221c0391..85551c288d 100644 --- a/docs/internals/builtins/io/fwrite.md +++ b/docs/internals/builtins/io/fwrite.md @@ -2,15 +2,15 @@ title: "fwrite() — internals" description: "Compiler internals for fwrite(): lowering path, type checks, and runtime helpers." sidebar: - order: 159 + order: 176 --- ## `fwrite()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2617](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2617) (`lower_fwrite`) +- **Signature**: [`src/builtins/io/fwrite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fwrite.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2838](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2838) (`lower_fwrite`) - **Function symbol**: `lower_fwrite()` @@ -26,14 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function fwrite(resource $stream, string $data, int $length): int +function fwrite(resource $stream, string $data): int ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 2 arguments. ## 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 ebea3fe9b4..49ad6a17cc 100644 --- a/docs/internals/builtins/io/gethostbyaddr.md +++ b/docs/internals/builtins/io/gethostbyaddr.md @@ -2,15 +2,15 @@ title: "gethostbyaddr() — internals" description: "Compiler internals for gethostbyaddr(): lowering path, type checks, and runtime helpers." sidebar: - order: 160 + order: 177 --- ## `gethostbyaddr()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3210](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3210) (`lower_gethostbyaddr`) +- **Signature**: [`src/builtins/io/gethostbyaddr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostbyaddr.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3431](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3431) (`lower_gethostbyaddr`) - **Function symbol**: `lower_gethostbyaddr()` @@ -37,4 +37,3 @@ function gethostbyaddr(string $ip): mixed ## 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 63c159f132..74394b4e6b 100644 --- a/docs/internals/builtins/io/gethostbyname.md +++ b/docs/internals/builtins/io/gethostbyname.md @@ -2,15 +2,15 @@ title: "gethostbyname() — internals" description: "Compiler internals for gethostbyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 161 + order: 178 --- ## `gethostbyname()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3198](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3198) (`lower_gethostbyname`) +- **Signature**: [`src/builtins/io/gethostbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostbyname.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3419](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3419) (`lower_gethostbyname`) - **Function symbol**: `lower_gethostbyname()` @@ -37,4 +37,3 @@ function gethostbyname(string $hostname): string ## 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 f28d5c4846..f5dbcabf0e 100644 --- a/docs/internals/builtins/io/gethostname.md +++ b/docs/internals/builtins/io/gethostname.md @@ -2,15 +2,15 @@ title: "gethostname() — internals" description: "Compiler internals for gethostname(): lowering path, type checks, and runtime helpers." sidebar: - order: 162 + order: 179 --- ## `gethostname()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3188](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3188) (`lower_gethostname`) +- **Signature**: [`src/builtins/io/gethostname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostname.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3409](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3409) (`lower_gethostname`) - **Function symbol**: `lower_gethostname()` @@ -38,4 +38,3 @@ function gethostname(): string ## 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 0d86583f36..85c99a16db 100644 --- a/docs/internals/builtins/io/getprotobyname.md +++ b/docs/internals/builtins/io/getprotobyname.md @@ -2,15 +2,15 @@ title: "getprotobyname() — internals" description: "Compiler internals for getprotobyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 163 + order: 180 --- ## `getprotobyname()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3223](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3223) (`lower_getprotobyname`) +- **Signature**: [`src/builtins/io/getprotobyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getprotobyname.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3444](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3444) (`lower_getprotobyname`) - **Function symbol**: `lower_getprotobyname()` @@ -36,4 +36,3 @@ function getprotobyname(string $protocol): mixed ## 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 9372def8e6..f2603bff3f 100644 --- a/docs/internals/builtins/io/getprotobynumber.md +++ b/docs/internals/builtins/io/getprotobynumber.md @@ -2,15 +2,15 @@ title: "getprotobynumber() — internals" description: "Compiler internals for getprotobynumber(): lowering path, type checks, and runtime helpers." sidebar: - order: 164 + order: 181 --- ## `getprotobynumber()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3246](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3246) (`lower_getprotobynumber`) +- **Signature**: [`src/builtins/io/getprotobynumber.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getprotobynumber.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3467](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3467) (`lower_getprotobynumber`) - **Function symbol**: `lower_getprotobynumber()` @@ -36,4 +36,3 @@ function getprotobynumber(int $protocol): mixed ## 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 dfac1b3625..cf829bb720 100644 --- a/docs/internals/builtins/io/getservbyname.md +++ b/docs/internals/builtins/io/getservbyname.md @@ -2,15 +2,15 @@ title: "getservbyname() — internals" description: "Compiler internals for getservbyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 165 + order: 182 --- ## `getservbyname()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3265](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3265) (`lower_getservbyname`) +- **Signature**: [`src/builtins/io/getservbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getservbyname.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3486](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3486) (`lower_getservbyname`) - **Function symbol**: `lower_getservbyname()` @@ -36,4 +36,3 @@ function getservbyname(string $service, string $protocol): mixed ## 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 58046bbac1..1e37fd6dda 100644 --- a/docs/internals/builtins/io/getservbyport.md +++ b/docs/internals/builtins/io/getservbyport.md @@ -2,15 +2,15 @@ title: "getservbyport() — internals" description: "Compiler internals for getservbyport(): lowering path, type checks, and runtime helpers." sidebar: - order: 166 + order: 183 --- ## `getservbyport()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3296](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3296) (`lower_getservbyport`) +- **Signature**: [`src/builtins/io/getservbyport.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getservbyport.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3517](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3517) (`lower_getservbyport`) - **Function symbol**: `lower_getservbyport()` @@ -36,4 +36,3 @@ function getservbyport(int $port, string $protocol): mixed ## 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 cbc99e8293..7c594feab1 100644 --- a/docs/internals/builtins/io/hash_file.md +++ b/docs/internals/builtins/io/hash_file.md @@ -2,15 +2,15 @@ title: "hash_file() — internals" description: "Compiler internals for hash_file(): lowering path, type checks, and runtime helpers." sidebar: - order: 167 + order: 184 --- ## `hash_file()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:180](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L180) (`lower_hash_file`) +- **Signature**: [`src/builtins/io/hash_file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/hash_file.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:287](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L287) (`lower_hash_file`) - **Function symbol**: `lower_hash_file()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function hash_file(string $algo, string $filename, bool $binary = false, array $options = []): mixed +function hash_file(string $algo, string $filename, bool $binary = false): mixed ``` ## What the type checker enforces -- **Arity**: takes 2–4 arguments (2 optional). +- **Arity**: takes 2–3 arguments (1 optional). ## 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 0b9983dc92..26d9d4b37e 100644 --- a/docs/internals/builtins/io/opendir.md +++ b/docs/internals/builtins/io/opendir.md @@ -2,15 +2,15 @@ title: "opendir() — internals" description: "Compiler internals for opendir(): lowering path, type checks, and runtime helpers." sidebar: - order: 168 + order: 185 --- ## `opendir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3326](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3326) (`lower_opendir`) +- **Signature**: [`src/builtins/io/opendir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/opendir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3547](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3547) (`lower_opendir`) - **Function symbol**: `lower_opendir()` @@ -38,4 +38,3 @@ function opendir(string $directory): mixed ## 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 bf0f3a143d..b6c1f0da7e 100644 --- a/docs/internals/builtins/io/readdir.md +++ b/docs/internals/builtins/io/readdir.md @@ -2,15 +2,15 @@ title: "readdir() — internals" description: "Compiler internals for readdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 169 + order: 186 --- ## `readdir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3336](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3336) (`lower_readdir`) +- **Signature**: [`src/builtins/io/readdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readdir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3557](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3557) (`lower_readdir`) - **Function symbol**: `lower_readdir()` @@ -39,4 +39,3 @@ function readdir(resource $dir_handle): mixed ## 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 ae06516ee4..6f63dbcf51 100644 --- a/docs/internals/builtins/io/rewind.md +++ b/docs/internals/builtins/io/rewind.md @@ -2,15 +2,15 @@ title: "rewind() — internals" description: "Compiler internals for rewind(): lowering path, type checks, and runtime helpers." sidebar: - order: 170 + order: 187 --- ## `rewind()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2975](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2975) (`lower_rewind`) +- **Signature**: [`src/builtins/io/rewind.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rewind.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3196](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3196) (`lower_rewind`) - **Function symbol**: `lower_rewind()` @@ -35,4 +35,3 @@ function rewind(resource $stream): bool ## 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 4d49681959..185059fde0 100644 --- a/docs/internals/builtins/io/rewinddir.md +++ b/docs/internals/builtins/io/rewinddir.md @@ -2,15 +2,15 @@ title: "rewinddir() — internals" description: "Compiler internals for rewinddir(): lowering path, type checks, and runtime helpers." sidebar: - order: 171 + order: 188 --- ## `rewinddir()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3365](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3365) (`lower_rewinddir`) +- **Signature**: [`src/builtins/io/rewinddir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rewinddir.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3588](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3588) (`lower_rewinddir`) - **Function symbol**: `lower_rewinddir()` @@ -37,4 +37,3 @@ function rewinddir(resource $dir_handle): void ## 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 72e71e5842..6bb8d17674 100644 --- a/docs/internals/builtins/io/stream_bucket_make_writeable.md +++ b/docs/internals/builtins/io/stream_bucket_make_writeable.md @@ -2,15 +2,15 @@ title: "stream_bucket_make_writeable() — internals" description: "Compiler internals for stream_bucket_make_writeable(): lowering path, type checks, and runtime helpers." sidebar: - order: 172 + order: 189 --- ## `stream_bucket_make_writeable()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1768](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1768) (`lower_stream_bucket_make_writeable`) +- **Signature**: [`src/builtins/io/stream_bucket_make_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_make_writeable.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1987](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1987) (`lower_stream_bucket_make_writeable`) - **Function symbol**: `lower_stream_bucket_make_writeable()` @@ -36,4 +36,3 @@ function stream_bucket_make_writeable(mixed $brigade): mixed ## 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 773e434245..568382c658 100644 --- a/docs/internals/builtins/io/stream_bucket_new.md +++ b/docs/internals/builtins/io/stream_bucket_new.md @@ -2,15 +2,15 @@ title: "stream_bucket_new() — internals" description: "Compiler internals for stream_bucket_new(): lowering path, type checks, and runtime helpers." sidebar: - order: 173 + order: 190 --- ## `stream_bucket_new()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1751](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1751) (`lower_stream_bucket_new`) +- **Signature**: [`src/builtins/io/stream_bucket_new.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_new.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1970](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1970) (`lower_stream_bucket_new`) - **Function symbol**: `lower_stream_bucket_new()` @@ -35,4 +35,3 @@ function stream_bucket_new(resource $stream, string $buffer): mixed ## 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 c105395341..294aed3021 100644 --- a/docs/internals/builtins/io/stream_context_create.md +++ b/docs/internals/builtins/io/stream_context_create.md @@ -2,15 +2,15 @@ title: "stream_context_create() — internals" description: "Compiler internals for stream_context_create(): lowering path, type checks, and runtime helpers." sidebar: - order: 174 + order: 191 --- ## `stream_context_create()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:951](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L951) (`lower_stream_context_create`) +- **Signature**: [`src/builtins/io/stream_context_create.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_create.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1064](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1064) (`lower_stream_context_create`) - **Function symbol**: `lower_stream_context_create()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_context_create(array $options, array $params): mixed +function stream_context_create(array $options = null, array $params = null): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_context_create(array $options, array $params): mixed ## 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 10f0138839..e8fa0170ef 100644 --- a/docs/internals/builtins/io/stream_context_get_default.md +++ b/docs/internals/builtins/io/stream_context_get_default.md @@ -2,15 +2,15 @@ title: "stream_context_get_default() — internals" description: "Compiler internals for stream_context_get_default(): lowering path, type checks, and runtime helpers." sidebar: - order: 175 + order: 192 --- ## `stream_context_get_default()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:965](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L965) (`lower_stream_context_get_default`) +- **Signature**: [`src/builtins/io/stream_context_get_default.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_get_default.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1078](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1078) (`lower_stream_context_get_default`) - **Function symbol**: `lower_stream_context_get_default()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_context_get_default(array $options): mixed +function stream_context_get_default(array $options = null): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_context_get_default(array $options): mixed ## 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 1c60e44a29..1bc88ab6fc 100644 --- a/docs/internals/builtins/io/stream_context_get_options.md +++ b/docs/internals/builtins/io/stream_context_get_options.md @@ -2,15 +2,15 @@ title: "stream_context_get_options() — internals" description: "Compiler internals for stream_context_get_options(): lowering path, type checks, and runtime helpers." sidebar: - order: 176 + order: 193 --- ## `stream_context_get_options()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1139](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1139) (`lower_stream_context_get_options`) +- **Signature**: [`src/builtins/io/stream_context_get_options.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_get_options.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1252](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1252) (`lower_stream_context_get_options`) - **Function symbol**: `lower_stream_context_get_options()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function stream_context_get_options(resource $stream_or_context): array +function stream_context_get_options(resource $context): array ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function stream_context_get_options(resource $stream_or_context): array ## 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 d6b1897077..a336ce53ac 100644 --- a/docs/internals/builtins/io/stream_context_get_params.md +++ b/docs/internals/builtins/io/stream_context_get_params.md @@ -2,15 +2,15 @@ title: "stream_context_get_params() — internals" description: "Compiler internals for stream_context_get_params(): lowering path, type checks, and runtime helpers." sidebar: - order: 177 + order: 194 --- ## `stream_context_get_params()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1178](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1178) (`lower_stream_context_get_params`) +- **Signature**: [`src/builtins/io/stream_context_get_params.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_get_params.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1291](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1291) (`lower_stream_context_get_params`) - **Function symbol**: `lower_stream_context_get_params()` @@ -35,4 +35,3 @@ function stream_context_get_params(resource $context): array ## 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 d9e388a080..11bc262f4a 100644 --- a/docs/internals/builtins/io/stream_context_set_default.md +++ b/docs/internals/builtins/io/stream_context_set_default.md @@ -2,15 +2,15 @@ title: "stream_context_set_default() — internals" description: "Compiler internals for stream_context_set_default(): lowering path, type checks, and runtime helpers." sidebar: - order: 178 + order: 195 --- ## `stream_context_set_default()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:975](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L975) (`lower_stream_context_set_default`) +- **Signature**: [`src/builtins/io/stream_context_set_default.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_set_default.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1088](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1088) (`lower_stream_context_set_default`) - **Function symbol**: `lower_stream_context_set_default()` @@ -35,4 +35,3 @@ function stream_context_set_default(array $options): mixed ## 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 10970cad66..fc81a0beaf 100644 --- a/docs/internals/builtins/io/stream_context_set_option.md +++ b/docs/internals/builtins/io/stream_context_set_option.md @@ -2,15 +2,15 @@ title: "stream_context_set_option() — internals" description: "Compiler internals for stream_context_set_option(): lowering path, type checks, and runtime helpers." sidebar: - order: 179 + order: 196 --- ## `stream_context_set_option()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:985](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L985) (`lower_stream_context_set_option`) +- **Signature**: [`src/builtins/io/stream_context_set_option.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_set_option.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1098](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1098) (`lower_stream_context_set_option`) - **Function symbol**: `lower_stream_context_set_option()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_context_set_option(resource $context, string $wrapper_or_options, string $option_name, mixed $value): bool +function stream_context_set_option(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_context_set_option(resource $context, string $wrapper_or_options ## 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 a655098c77..adacd430ba 100644 --- a/docs/internals/builtins/io/stream_context_set_params.md +++ b/docs/internals/builtins/io/stream_context_set_params.md @@ -2,15 +2,15 @@ title: "stream_context_set_params() — internals" description: "Compiler internals for stream_context_set_params(): lowering path, type checks, and runtime helpers." sidebar: - order: 180 + order: 197 --- ## `stream_context_set_params()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1005](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1005) (`lower_stream_context_set_params`) +- **Signature**: [`src/builtins/io/stream_context_set_params.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_set_params.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1118](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1118) (`lower_stream_context_set_params`) - **Function symbol**: `lower_stream_context_set_params()` @@ -35,4 +35,3 @@ function stream_context_set_params(resource $context, array $params): bool ## 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 64a2c7be53..c33e7ed443 100644 --- a/docs/internals/builtins/io/stream_copy_to_stream.md +++ b/docs/internals/builtins/io/stream_copy_to_stream.md @@ -2,15 +2,15 @@ title: "stream_copy_to_stream() — internals" description: "Compiler internals for stream_copy_to_stream(): lowering path, type checks, and runtime helpers." sidebar: - order: 181 + order: 198 --- ## `stream_copy_to_stream()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1247](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1247) (`lower_stream_copy_to_stream`) +- **Signature**: [`src/builtins/io/stream_copy_to_stream.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_copy_to_stream.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1360](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1360) (`lower_stream_copy_to_stream`) - **Function symbol**: `lower_stream_copy_to_stream()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_copy_to_stream(resource $from, resource $to, int $length, int $offset): mixed +function stream_copy_to_stream(resource $from, resource $to, int $length = null, int $offset = -1): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_copy_to_stream(resource $from, resource $to, int $length, int $o ## 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 f4a9b49a0d..4871ac00d3 100644 --- a/docs/internals/builtins/io/stream_filter_register.md +++ b/docs/internals/builtins/io/stream_filter_register.md @@ -2,15 +2,15 @@ title: "stream_filter_register() — internals" description: "Compiler internals for stream_filter_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 182 + order: 199 --- ## `stream_filter_register()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1408](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1408) (`lower_stream_filter_register`) +- **Signature**: [`src/builtins/io/stream_filter_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_register.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1521](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1521) (`lower_stream_filter_register`) - **Function symbol**: `lower_stream_filter_register()` @@ -36,4 +36,3 @@ function stream_filter_register(string $filter_name, string $class): bool ## 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 bd8b9a29b5..4259c6188a 100644 --- a/docs/internals/builtins/io/stream_filter_remove.md +++ b/docs/internals/builtins/io/stream_filter_remove.md @@ -2,15 +2,15 @@ title: "stream_filter_remove() — internals" description: "Compiler internals for stream_filter_remove(): lowering path, type checks, and runtime helpers." sidebar: - order: 183 + order: 200 --- ## `stream_filter_remove()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1720](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1720) (`lower_stream_filter_remove`) +- **Signature**: [`src/builtins/io/stream_filter_remove.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_remove.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1939](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1939) (`lower_stream_filter_remove`) - **Function symbol**: `lower_stream_filter_remove()` @@ -36,4 +36,3 @@ function stream_filter_remove(resource $stream_filter): bool ## 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 e209c1eb10..2354839a01 100644 --- a/docs/internals/builtins/io/stream_get_contents.md +++ b/docs/internals/builtins/io/stream_get_contents.md @@ -2,15 +2,15 @@ title: "stream_get_contents() — internals" description: "Compiler internals for stream_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 184 + order: 201 --- ## `stream_get_contents()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1188](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1188) (`lower_stream_get_contents`) +- **Signature**: [`src/builtins/io/stream_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_contents.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1301](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1301) (`lower_stream_get_contents`) - **Function symbol**: `lower_stream_get_contents()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_get_contents(resource $stream, int $length, int $offset): mixed +function stream_get_contents(resource $stream, int $length = null, int $offset = -1): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_get_contents(resource $stream, int $length, int $offset): mixed ## 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 eed69910a5..eac2d3dabc 100644 --- a/docs/internals/builtins/io/stream_get_filters.md +++ b/docs/internals/builtins/io/stream_get_filters.md @@ -2,15 +2,15 @@ title: "stream_get_filters() — internals" description: "Compiler internals for stream_get_filters(): lowering path, type checks, and runtime helpers." sidebar: - order: 185 + order: 202 --- ## `stream_get_filters()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1380](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1380) (`lower_stream_get_filters`) +- **Signature**: [`src/builtins/io/stream_get_filters.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_filters.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1493](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1493) (`lower_stream_get_filters`) - **Function symbol**: `lower_stream_get_filters()` @@ -35,4 +35,3 @@ function stream_get_filters(): array ## 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 6d73f000e3..52b01cbfb2 100644 --- a/docs/internals/builtins/io/stream_get_line.md +++ b/docs/internals/builtins/io/stream_get_line.md @@ -2,15 +2,15 @@ title: "stream_get_line() — internals" description: "Compiler internals for stream_get_line(): lowering path, type checks, and runtime helpers." sidebar: - order: 186 + order: 203 --- ## `stream_get_line()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1280](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1280) (`lower_stream_get_line`) +- **Signature**: [`src/builtins/io/stream_get_line.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_line.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1393](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1393) (`lower_stream_get_line`) - **Function symbol**: `lower_stream_get_line()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_get_line(resource $stream, int $length, string $ending): string +function stream_get_line(resource $stream, int $length, string $ending = ''): string ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_get_line(resource $stream, int $length, string $ending): string ## 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 198fe9752b..c22e797cce 100644 --- a/docs/internals/builtins/io/stream_get_meta_data.md +++ b/docs/internals/builtins/io/stream_get_meta_data.md @@ -2,15 +2,15 @@ title: "stream_get_meta_data() — internals" description: "Compiler internals for stream_get_meta_data(): lowering path, type checks, and runtime helpers." sidebar: - order: 187 + order: 204 --- ## `stream_get_meta_data()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1333](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1333) (`lower_stream_get_meta_data`) +- **Signature**: [`src/builtins/io/stream_get_meta_data.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_meta_data.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1446](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1446) (`lower_stream_get_meta_data`) - **Function symbol**: `lower_stream_get_meta_data()` @@ -36,4 +36,3 @@ function stream_get_meta_data(resource $stream): array ## 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 424af49efe..8c08491264 100644 --- a/docs/internals/builtins/io/stream_get_transports.md +++ b/docs/internals/builtins/io/stream_get_transports.md @@ -2,15 +2,15 @@ title: "stream_get_transports() — internals" description: "Compiler internals for stream_get_transports(): lowering path, type checks, and runtime helpers." sidebar: - order: 188 + order: 205 --- ## `stream_get_transports()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1364](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1364) (`lower_stream_get_transports`) +- **Signature**: [`src/builtins/io/stream_get_transports.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_transports.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1477](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1477) (`lower_stream_get_transports`) - **Function symbol**: `lower_stream_get_transports()` @@ -35,4 +35,3 @@ function stream_get_transports(): array ## 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 ddb2137eb4..f83d658932 100644 --- a/docs/internals/builtins/io/stream_get_wrappers.md +++ b/docs/internals/builtins/io/stream_get_wrappers.md @@ -2,15 +2,15 @@ title: "stream_get_wrappers() — internals" description: "Compiler internals for stream_get_wrappers(): lowering path, type checks, and runtime helpers." sidebar: - order: 189 + order: 206 --- ## `stream_get_wrappers()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1348](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1348) (`lower_stream_get_wrappers`) +- **Signature**: [`src/builtins/io/stream_get_wrappers.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_wrappers.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1461](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1461) (`lower_stream_get_wrappers`) - **Function symbol**: `lower_stream_get_wrappers()` @@ -35,4 +35,3 @@ function stream_get_wrappers(): array ## 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 fdc05712a5..2a28e1703b 100644 --- a/docs/internals/builtins/io/stream_is_local.md +++ b/docs/internals/builtins/io/stream_is_local.md @@ -2,15 +2,15 @@ title: "stream_is_local() — internals" description: "Compiler internals for stream_is_local(): lowering path, type checks, and runtime helpers." sidebar: - order: 190 + order: 207 --- ## `stream_is_local()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1884](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1884) (`lower_stream_is_local`) +- **Signature**: [`src/builtins/io/stream_is_local.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_is_local.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2103](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2103) (`lower_stream_is_local`) - **Function symbol**: `lower_stream_is_local()` @@ -35,4 +35,3 @@ function stream_is_local(resource $stream): bool ## 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 212b5aa456..a5ee682754 100644 --- a/docs/internals/builtins/io/stream_isatty.md +++ b/docs/internals/builtins/io/stream_isatty.md @@ -2,15 +2,15 @@ title: "stream_isatty() — internals" description: "Compiler internals for stream_isatty(): lowering path, type checks, and runtime helpers." sidebar: - order: 191 + order: 208 --- ## `stream_isatty()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1908](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1908) (`lower_stream_isatty`) +- **Signature**: [`src/builtins/io/stream_isatty.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_isatty.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2127](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2127) (`lower_stream_isatty`) - **Function symbol**: `lower_stream_isatty()` @@ -36,4 +36,3 @@ function stream_isatty(resource $stream): bool ## 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 fbbfae287d..12122e031c 100644 --- a/docs/internals/builtins/io/stream_resolve_include_path.md +++ b/docs/internals/builtins/io/stream_resolve_include_path.md @@ -2,15 +2,15 @@ title: "stream_resolve_include_path() — internals" description: "Compiler internals for stream_resolve_include_path(): lowering path, type checks, and runtime helpers." sidebar: - order: 192 + order: 209 --- ## `stream_resolve_include_path()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2142](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2142) (`lower_stream_resolve_include_path`) +- **Signature**: [`src/builtins/io/stream_resolve_include_path.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_resolve_include_path.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2361](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2361) (`lower_stream_resolve_include_path`) - **Function symbol**: `lower_stream_resolve_include_path()` @@ -37,4 +37,3 @@ function stream_resolve_include_path(string $filename): mixed ## 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 af28d818a7..d5c0fc7e99 100644 --- a/docs/internals/builtins/io/stream_select.md +++ b/docs/internals/builtins/io/stream_select.md @@ -2,15 +2,15 @@ title: "stream_select() — internals" description: "Compiler internals for stream_select(): lowering path, type checks, and runtime helpers." sidebar: - order: 193 + order: 210 --- ## `stream_select()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2097](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2097) (`lower_stream_select`) +- **Signature**: [`src/builtins/io/stream_select.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_select.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2316](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2316) (`lower_stream_select`) - **Function symbol**: `lower_stream_select()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_select(array $read, array $write, array $except, int $seconds, int $microseconds): int +function stream_select(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function stream_select(array $read, array $write, array $except, int $seconds, i ## 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 098b585e38..c2971c593a 100644 --- a/docs/internals/builtins/io/stream_set_blocking.md +++ b/docs/internals/builtins/io/stream_set_blocking.md @@ -2,15 +2,15 @@ title: "stream_set_blocking() — internals" description: "Compiler internals for stream_set_blocking(): lowering path, type checks, and runtime helpers." sidebar: - order: 194 + order: 211 --- ## `stream_set_blocking()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1923](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1923) (`lower_stream_set_blocking`) +- **Signature**: [`src/builtins/io/stream_set_blocking.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_blocking.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2142](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2142) (`lower_stream_set_blocking`) - **Function symbol**: `lower_stream_set_blocking()` @@ -37,4 +37,3 @@ function stream_set_blocking(resource $stream, bool $enable): bool ## 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 29f2af81cf..3ebdaabd13 100644 --- a/docs/internals/builtins/io/stream_set_chunk_size.md +++ b/docs/internals/builtins/io/stream_set_chunk_size.md @@ -2,15 +2,15 @@ title: "stream_set_chunk_size() — internals" description: "Compiler internals for stream_set_chunk_size(): lowering path, type checks, and runtime helpers." sidebar: - order: 195 + order: 212 --- ## `stream_set_chunk_size()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1975](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1975) (`lower_stream_set_chunk_size`) +- **Signature**: [`src/builtins/io/stream_set_chunk_size.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_chunk_size.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2194](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2194) (`lower_stream_set_chunk_size`) - **Function symbol**: `lower_stream_set_chunk_size()` @@ -35,4 +35,3 @@ function stream_set_chunk_size(resource $stream, int $size): int ## 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 998b5671ad..e955cf8ecc 100644 --- a/docs/internals/builtins/io/stream_set_read_buffer.md +++ b/docs/internals/builtins/io/stream_set_read_buffer.md @@ -2,15 +2,15 @@ title: "stream_set_read_buffer() — internals" description: "Compiler internals for stream_set_read_buffer(): lowering path, type checks, and runtime helpers." sidebar: - order: 196 + order: 213 --- ## `stream_set_read_buffer()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2035](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2035) (`lower_stream_set_buffer`) +- **Signature**: [`src/builtins/io/stream_set_read_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_read_buffer.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2254](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2254) (`lower_stream_set_buffer`) - **Function symbol**: `lower_stream_set_buffer()` @@ -35,4 +35,3 @@ function stream_set_read_buffer(resource $stream, int $size): int ## 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 25b96ac7de..945078a811 100644 --- a/docs/internals/builtins/io/stream_set_timeout.md +++ b/docs/internals/builtins/io/stream_set_timeout.md @@ -2,15 +2,15 @@ title: "stream_set_timeout() — internals" description: "Compiler internals for stream_set_timeout(): lowering path, type checks, and runtime helpers." sidebar: - order: 197 + order: 214 --- ## `stream_set_timeout()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2048](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2048) (`lower_stream_set_timeout`) +- **Signature**: [`src/builtins/io/stream_set_timeout.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_timeout.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2267](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2267) (`lower_stream_set_timeout`) - **Function symbol**: `lower_stream_set_timeout()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_set_timeout(resource $stream, int $seconds, int $microseconds): bool +function stream_set_timeout(resource $stream, int $seconds, int $microseconds = 0): bool ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_set_timeout(resource $stream, int $seconds, int $microseconds): ## 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 8848461a88..5687cee45a 100644 --- a/docs/internals/builtins/io/stream_set_write_buffer.md +++ b/docs/internals/builtins/io/stream_set_write_buffer.md @@ -2,15 +2,15 @@ title: "stream_set_write_buffer() — internals" description: "Compiler internals for stream_set_write_buffer(): lowering path, type checks, and runtime helpers." sidebar: - order: 198 + order: 215 --- ## `stream_set_write_buffer()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2035](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2035) (`lower_stream_set_buffer`) +- **Signature**: [`src/builtins/io/stream_set_write_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_write_buffer.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2254](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2254) (`lower_stream_set_buffer`) - **Function symbol**: `lower_stream_set_buffer()` @@ -35,4 +35,3 @@ function stream_set_write_buffer(resource $stream, int $size): int ## 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 cffac1881b..15bc1cd888 100644 --- a/docs/internals/builtins/io/stream_socket_accept.md +++ b/docs/internals/builtins/io/stream_socket_accept.md @@ -2,15 +2,15 @@ title: "stream_socket_accept() — internals" description: "Compiler internals for stream_socket_accept(): lowering path, type checks, and runtime helpers." sidebar: - order: 199 + order: 216 --- ## `stream_socket_accept()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2217](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2217) (`lower_stream_socket_accept`) +- **Signature**: [`src/builtins/io/stream_socket_accept.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_accept.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2436](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2436) (`lower_stream_socket_accept`) - **Function symbol**: `lower_stream_socket_accept()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function stream_socket_accept(resource $socket, float $timeout, string $peer_name): mixed +function stream_socket_accept(resource $socket, float $timeout = null, string $peer_name = null): mixed ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function stream_socket_accept(resource $socket, float $timeout, string $peer_nam ## 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 71e8ce0fa2..86a0b421a4 100644 --- a/docs/internals/builtins/io/stream_socket_client.md +++ b/docs/internals/builtins/io/stream_socket_client.md @@ -2,15 +2,15 @@ title: "stream_socket_client() — internals" description: "Compiler internals for stream_socket_client(): lowering path, type checks, and runtime helpers." sidebar: - order: 200 + order: 217 --- ## `stream_socket_client()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2178](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2178) (`lower_stream_socket_client`) +- **Signature**: [`src/builtins/io/stream_socket_client.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_client.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2397](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2397) (`lower_stream_socket_client`) - **Function symbol**: `lower_stream_socket_client()` @@ -27,15 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function stream_socket_client(string $address, int $error_code, int $error_message, string $timeout, float $flags): mixed +function stream_socket_client(string $address): mixed ``` ## What the type checker enforces -- **Arity**: takes exactly 5 arguments. -- **By-reference parameters**: `$error_code`, `$error_message`. +- **Arity**: takes exactly 1 argument. ## 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 b822ae31db..1b2277fb07 100644 --- a/docs/internals/builtins/io/stream_socket_enable_crypto.md +++ b/docs/internals/builtins/io/stream_socket_enable_crypto.md @@ -2,15 +2,15 @@ title: "stream_socket_enable_crypto() — internals" description: "Compiler internals for stream_socket_enable_crypto(): lowering path, type checks, and runtime helpers." sidebar: - order: 201 + order: 218 --- ## `stream_socket_enable_crypto()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2328](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2328) (`lower_stream_socket_enable_crypto`) +- **Signature**: [`src/builtins/io/stream_socket_enable_crypto.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_enable_crypto.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2547](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2547) (`lower_stream_socket_enable_crypto`) - **Function symbol**: `lower_stream_socket_enable_crypto()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method, resource $session_stream): bool +function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto ## 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 678c984d21..0108165a9d 100644 --- a/docs/internals/builtins/io/stream_socket_get_name.md +++ b/docs/internals/builtins/io/stream_socket_get_name.md @@ -2,15 +2,15 @@ title: "stream_socket_get_name() — internals" description: "Compiler internals for stream_socket_get_name(): lowering path, type checks, and runtime helpers." sidebar: - order: 202 + order: 219 --- ## `stream_socket_get_name()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2277](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2277) (`lower_stream_socket_get_name`) +- **Signature**: [`src/builtins/io/stream_socket_get_name.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_get_name.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2496](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2496) (`lower_stream_socket_get_name`) - **Function symbol**: `lower_stream_socket_get_name()` @@ -36,4 +36,3 @@ function stream_socket_get_name(resource $socket, bool $remote): mixed ## 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 901e21d473..d0cca12362 100644 --- a/docs/internals/builtins/io/stream_socket_pair.md +++ b/docs/internals/builtins/io/stream_socket_pair.md @@ -2,15 +2,15 @@ title: "stream_socket_pair() — internals" description: "Compiler internals for stream_socket_pair(): lowering path, type checks, and runtime helpers." sidebar: - order: 203 + order: 220 --- ## `stream_socket_pair()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2246](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2246) (`lower_stream_socket_pair`) +- **Signature**: [`src/builtins/io/stream_socket_pair.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_pair.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2465](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2465) (`lower_stream_socket_pair`) - **Function symbol**: `lower_stream_socket_pair()` @@ -36,4 +36,3 @@ function stream_socket_pair(int $domain, int $type, int $protocol): mixed ## 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 d58325726a..12ccb5cbcc 100644 --- a/docs/internals/builtins/io/stream_socket_recvfrom.md +++ b/docs/internals/builtins/io/stream_socket_recvfrom.md @@ -2,15 +2,15 @@ title: "stream_socket_recvfrom() — internals" description: "Compiler internals for stream_socket_recvfrom(): lowering path, type checks, and runtime helpers." sidebar: - order: 204 + order: 221 --- ## `stream_socket_recvfrom()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2380](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2380) (`lower_stream_socket_recvfrom`) +- **Signature**: [`src/builtins/io/stream_socket_recvfrom.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_recvfrom.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2599](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2599) (`lower_stream_socket_recvfrom`) - **Function symbol**: `lower_stream_socket_recvfrom()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_socket_recvfrom(resource $socket, int $length, int $flags, string $address): mixed +function stream_socket_recvfrom(resource $socket, int $length, int $flags = 0, string $address = ''): mixed ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function stream_socket_recvfrom(resource $socket, int $length, int $flags, strin ## 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 5f29216821..0350af291b 100644 --- a/docs/internals/builtins/io/stream_socket_sendto.md +++ b/docs/internals/builtins/io/stream_socket_sendto.md @@ -2,15 +2,15 @@ title: "stream_socket_sendto() — internals" description: "Compiler internals for stream_socket_sendto(): lowering path, type checks, and runtime helpers." sidebar: - order: 205 + order: 222 --- ## `stream_socket_sendto()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2422](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2422) (`lower_stream_socket_sendto`) +- **Signature**: [`src/builtins/io/stream_socket_sendto.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_sendto.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2641](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2641) (`lower_stream_socket_sendto`) - **Function symbol**: `lower_stream_socket_sendto()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_socket_sendto(resource $socket, string $data, int $flags, string $address): mixed +function stream_socket_sendto(resource $socket, string $data, int $flags = 0, string $address = ''): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function stream_socket_sendto(resource $socket, string $data, int $flags, string ## 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 51e3a05bb3..86922f9e4e 100644 --- a/docs/internals/builtins/io/stream_socket_server.md +++ b/docs/internals/builtins/io/stream_socket_server.md @@ -2,15 +2,15 @@ title: "stream_socket_server() — internals" description: "Compiler internals for stream_socket_server(): lowering path, type checks, and runtime helpers." sidebar: - order: 206 + order: 223 --- ## `stream_socket_server()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2155](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2155) (`lower_stream_socket_server`) +- **Signature**: [`src/builtins/io/stream_socket_server.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_server.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2374](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2374) (`lower_stream_socket_server`) - **Function symbol**: `lower_stream_socket_server()` @@ -26,15 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function stream_socket_server(string $address, int $error_code, int $error_message): mixed +function stream_socket_server(string $address): mixed ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. -- **By-reference parameters**: `$error_code`, `$error_message`. +- **Arity**: takes exactly 1 argument. ## 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 7920cb0291..e5eb60e571 100644 --- a/docs/internals/builtins/io/stream_socket_shutdown.md +++ b/docs/internals/builtins/io/stream_socket_shutdown.md @@ -2,15 +2,15 @@ title: "stream_socket_shutdown() — internals" description: "Compiler internals for stream_socket_shutdown(): lowering path, type checks, and runtime helpers." sidebar: - order: 207 + order: 224 --- ## `stream_socket_shutdown()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2303](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2303) (`lower_stream_socket_shutdown`) +- **Signature**: [`src/builtins/io/stream_socket_shutdown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_shutdown.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2522](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2522) (`lower_stream_socket_shutdown`) - **Function symbol**: `lower_stream_socket_shutdown()` @@ -36,4 +36,3 @@ function stream_socket_shutdown(resource $stream, int $mode): bool ## 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 a735df223d..82a20d07f7 100644 --- a/docs/internals/builtins/io/stream_supports_lock.md +++ b/docs/internals/builtins/io/stream_supports_lock.md @@ -2,15 +2,15 @@ title: "stream_supports_lock() — internals" description: "Compiler internals for stream_supports_lock(): lowering path, type checks, and runtime helpers." sidebar: - order: 208 + order: 225 --- ## `stream_supports_lock()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:1896](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L1896) (`lower_stream_supports_lock`) +- **Signature**: [`src/builtins/io/stream_supports_lock.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_supports_lock.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2115](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2115) (`lower_stream_supports_lock`) - **Function symbol**: `lower_stream_supports_lock()` @@ -36,4 +36,3 @@ function stream_supports_lock(resource $stream): bool ## 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 65fb148907..4ed40920b3 100644 --- a/docs/internals/builtins/io/stream_wrapper_register.md +++ b/docs/internals/builtins/io/stream_wrapper_register.md @@ -2,15 +2,15 @@ title: "stream_wrapper_register() — internals" description: "Compiler internals for stream_wrapper_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 209 + order: 226 --- ## `stream_wrapper_register()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:887](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L887) (`lower_stream_wrapper_register`) +- **Signature**: [`src/builtins/io/stream_wrapper_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_register.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1000](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1000) (`lower_stream_wrapper_register`) - **Function symbol**: `lower_stream_wrapper_register()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function stream_wrapper_register(string $protocol, string $class, int $flags): bool +function stream_wrapper_register(string $protocol, string $class, int $flags = 0): bool ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function stream_wrapper_register(string $protocol, string $class, int $flags): b ## 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 9bb85aa7d5..8e82185c1a 100644 --- a/docs/internals/builtins/io/stream_wrapper_restore.md +++ b/docs/internals/builtins/io/stream_wrapper_restore.md @@ -2,15 +2,15 @@ title: "stream_wrapper_restore() — internals" description: "Compiler internals for stream_wrapper_restore(): lowering path, type checks, and runtime helpers." sidebar: - order: 210 + order: 227 --- ## `stream_wrapper_restore()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:939](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L939) (`lower_stream_wrapper_restore`) +- **Signature**: [`src/builtins/io/stream_wrapper_restore.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_restore.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1052](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1052) (`lower_stream_wrapper_restore`) - **Function symbol**: `lower_stream_wrapper_restore()` @@ -35,4 +35,3 @@ function stream_wrapper_restore(string $protocol): bool ## 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 3cbc4cfddc..aaa6b3b57b 100644 --- a/docs/internals/builtins/io/stream_wrapper_unregister.md +++ b/docs/internals/builtins/io/stream_wrapper_unregister.md @@ -2,15 +2,15 @@ title: "stream_wrapper_unregister() — internals" description: "Compiler internals for stream_wrapper_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 211 + order: 228 --- ## `stream_wrapper_unregister()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:917](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L917) (`lower_stream_wrapper_unregister`) +- **Signature**: [`src/builtins/io/stream_wrapper_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_unregister.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1030](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1030) (`lower_stream_wrapper_unregister`) - **Function symbol**: `lower_stream_wrapper_unregister()` @@ -36,4 +36,3 @@ function stream_wrapper_unregister(string $protocol): bool ## 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 9543234e03..3a7fdd8e50 100644 --- a/docs/internals/builtins/io/vfprintf.md +++ b/docs/internals/builtins/io/vfprintf.md @@ -2,15 +2,15 @@ title: "vfprintf() — internals" description: "Compiler internals for vfprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 212 + order: 229 --- ## `vfprintf()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:2677](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L2677) (`lower_vfprintf`) +- **Signature**: [`src/builtins/io/vfprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/vfprintf.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2898](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2898) (`lower_vfprintf`) - **Function symbol**: `lower_vfprintf()` @@ -37,4 +37,3 @@ function vfprintf(resource $stream, string $format, array $values): int ## 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 f27c39bd5f..9c7bdef87c 100644 --- a/docs/internals/builtins/json/json_decode.md +++ b/docs/internals/builtins/json/json_decode.md @@ -2,15 +2,15 @@ title: "json_decode() — internals" description: "Compiler internals for json_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 213 + order: 230 --- ## `json_decode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/json.rs`:30](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/json.rs#L30) (`lower_json_decode`) +- **Signature**: [`src/builtins/system/json_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_decode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/json.rs`:30](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/json.rs#L30) (`lower_json_decode`) - **Function symbol**: `lower_json_decode()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function json_decode(string $json, bool $associative, int $depth, int $flags): mixed +function json_decode(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function json_decode(string $json, bool $associative, int $depth, int $flags): m ## 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 363ee53b45..0b4a9d768a 100644 --- a/docs/internals/builtins/json/json_encode.md +++ b/docs/internals/builtins/json/json_encode.md @@ -2,15 +2,15 @@ title: "json_encode() — internals" description: "Compiler internals for json_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 214 + order: 231 --- ## `json_encode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/json.rs`:52](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/json.rs#L52) (`lower_json_encode`) +- **Signature**: [`src/builtins/system/json_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_encode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/json.rs`:52](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/json.rs#L52) (`lower_json_encode`) - **Function symbol**: `lower_json_encode()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function json_encode(mixed $value, int $flags, int $depth): string +function json_encode(mixed $value, int $flags = 0, int $depth = 512): string ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function json_encode(mixed $value, int $flags, int $depth): string ## 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 51c232863f..0e1d486891 100644 --- a/docs/internals/builtins/json/json_last_error.md +++ b/docs/internals/builtins/json/json_last_error.md @@ -2,15 +2,15 @@ title: "json_last_error() — internals" description: "Compiler internals for json_last_error(): lowering path, type checks, and runtime helpers." sidebar: - order: 215 + order: 232 --- ## `json_last_error()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/json.rs`:70](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/json.rs#L70) (`lower_json_last_error`) +- **Signature**: [`src/builtins/system/json_last_error.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_last_error.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/json.rs`:70](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/json.rs#L70) (`lower_json_last_error`) - **Function symbol**: `lower_json_last_error()` @@ -36,4 +36,3 @@ function json_last_error(): int ## 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 1697ef9b23..cb96b19c43 100644 --- a/docs/internals/builtins/json/json_last_error_msg.md +++ b/docs/internals/builtins/json/json_last_error_msg.md @@ -2,15 +2,15 @@ title: "json_last_error_msg() — internals" description: "Compiler internals for json_last_error_msg(): lowering path, type checks, and runtime helpers." sidebar: - order: 216 + order: 233 --- ## `json_last_error_msg()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/json.rs`:85](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/json.rs#L85) (`lower_json_last_error_msg`) +- **Signature**: [`src/builtins/system/json_last_error_msg.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_last_error_msg.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/json.rs`:85](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/json.rs#L85) (`lower_json_last_error_msg`) - **Function symbol**: `lower_json_last_error_msg()` @@ -37,4 +37,3 @@ function json_last_error_msg(): string ## 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 b9caf8b8e4..3fad80ebc2 100644 --- a/docs/internals/builtins/json/json_validate.md +++ b/docs/internals/builtins/json/json_validate.md @@ -2,15 +2,15 @@ title: "json_validate() — internals" description: "Compiler internals for json_validate(): lowering path, type checks, and runtime helpers." sidebar: - order: 217 + order: 234 --- ## `json_validate()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/json.rs`:95](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/json.rs#L95) (`lower_json_validate`) +- **Signature**: [`src/builtins/system/json_validate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_validate.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/json.rs`:95](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/json.rs#L95) (`lower_json_validate`) - **Function symbol**: `lower_json_validate()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function json_validate(string $json, int $depth, int $flags): bool +function json_validate(string $json, int $depth = 512, int $flags = 0): bool ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function json_validate(string $json, int $depth, int $flags): bool ## 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 f7301e4f7b..7922313ede 100644 --- a/docs/internals/builtins/math/abs.md +++ b/docs/internals/builtins/math/abs.md @@ -2,15 +2,15 @@ title: "abs() — internals" description: "Compiler internals for abs(): lowering path, type checks, and runtime helpers." sidebar: - order: 218 + order: 235 --- ## `abs()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:43](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L43) (`lower_abs`) +- **Signature**: [`src/builtins/math/abs.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/abs.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:43](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L43) (`lower_abs`) - **Function symbol**: `lower_abs()` @@ -36,4 +36,3 @@ function abs(int $num): mixed ## 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 26347fa3dc..1d800b1477 100644 --- a/docs/internals/builtins/math/acos.md +++ b/docs/internals/builtins/math/acos.md @@ -2,18 +2,22 @@ title: "acos() — internals" description: "Compiler internals for acos(): lowering path, type checks, and runtime helpers." sidebar: - order: 219 + order: 236 --- ## `acos()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/acos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/acos.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function acos(float $num): float ## 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 acb654de50..849ba063e2 100644 --- a/docs/internals/builtins/math/asin.md +++ b/docs/internals/builtins/math/asin.md @@ -2,18 +2,22 @@ title: "asin() — internals" description: "Compiler internals for asin(): lowering path, type checks, and runtime helpers." sidebar: - order: 220 + order: 237 --- ## `asin()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/asin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/asin.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function asin(float $num): float ## 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 2b8295d3f5..f60bbb0fe2 100644 --- a/docs/internals/builtins/math/atan.md +++ b/docs/internals/builtins/math/atan.md @@ -2,18 +2,22 @@ title: "atan() — internals" description: "Compiler internals for atan(): lowering path, type checks, and runtime helpers." sidebar: - order: 221 + order: 238 --- ## `atan()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/atan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/atan.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function atan(float $num): float ## 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 3f75ba226e..e45ecf6a8c 100644 --- a/docs/internals/builtins/math/atan2.md +++ b/docs/internals/builtins/math/atan2.md @@ -2,15 +2,15 @@ title: "atan2() — internals" description: "Compiler internals for atan2(): lowering path, type checks, and runtime helpers." sidebar: - order: 222 + order: 239 --- ## `atan2()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/libm.rs`:35](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/libm.rs#L35) (`lower_atan2`) +- **Signature**: [`src/builtins/math/atan2.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/atan2.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:35](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L35) (`lower_atan2`) - **Function symbol**: `lower_atan2()` @@ -35,4 +35,3 @@ function atan2(float $y, float $x): float ## 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 6c923654c2..cfaa80576d 100644 --- a/docs/internals/builtins/math/ceil.md +++ b/docs/internals/builtins/math/ceil.md @@ -2,15 +2,15 @@ title: "ceil() — internals" description: "Compiler internals for ceil(): lowering path, type checks, and runtime helpers." sidebar: - order: 223 + order: 240 --- ## `ceil()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:75](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L75) (`lower_ceil`) +- **Signature**: [`src/builtins/math/ceil.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/ceil.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:75](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L75) (`lower_ceil`) - **Function symbol**: `lower_ceil()` @@ -35,4 +35,3 @@ function ceil(float $num): float ## 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 d4d7b706c4..ca4f8a19b3 100644 --- a/docs/internals/builtins/math/clamp.md +++ b/docs/internals/builtins/math/clamp.md @@ -2,15 +2,15 @@ title: "clamp() — internals" description: "Compiler internals for clamp(): lowering path, type checks, and runtime helpers." sidebar: - order: 224 + order: 241 --- ## `clamp()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:80](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L80) (`lower_clamp`) +- **Signature**: [`src/builtins/math/clamp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/clamp.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:80](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L80) (`lower_clamp`) - **Function symbol**: `lower_clamp()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function clamp(int $value, int $min, int $max): string +function clamp(int $value, int $min, int $max): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function clamp(int $value, int $min, int $max): string ## 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 3f2608e3f2..0e995e6cc9 100644 --- a/docs/internals/builtins/math/cos.md +++ b/docs/internals/builtins/math/cos.md @@ -2,18 +2,22 @@ title: "cos() — internals" description: "Compiler internals for cos(): lowering path, type checks, and runtime helpers." sidebar: - order: 225 + order: 242 --- ## `cos()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/cos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/cos.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function cos(float $num): float ## 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 20e816ee2f..1ce7156dbe 100644 --- a/docs/internals/builtins/math/cosh.md +++ b/docs/internals/builtins/math/cosh.md @@ -2,18 +2,22 @@ title: "cosh() — internals" description: "Compiler internals for cosh(): lowering path, type checks, and runtime helpers." sidebar: - order: 226 + order: 243 --- ## `cosh()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/cosh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/cosh.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function cosh(float $num): float ## 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 4e8b49d1fa..57aa0dc01e 100644 --- a/docs/internals/builtins/math/deg2rad.md +++ b/docs/internals/builtins/math/deg2rad.md @@ -2,15 +2,15 @@ title: "deg2rad() — internals" description: "Compiler internals for deg2rad(): lowering path, type checks, and runtime helpers." sidebar: - order: 227 + order: 244 --- ## `deg2rad()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/libm.rs`:75](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/libm.rs#L75) (`lower_deg2rad`) +- **Signature**: [`src/builtins/math/deg2rad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/deg2rad.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:75](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L75) (`lower_deg2rad`) - **Function symbol**: `lower_deg2rad()` @@ -35,4 +35,3 @@ function deg2rad(float $num): float ## 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 be3eda0e50..8e55435630 100644 --- a/docs/internals/builtins/math/exp.md +++ b/docs/internals/builtins/math/exp.md @@ -2,18 +2,22 @@ title: "exp() — internals" description: "Compiler internals for exp(): lowering path, type checks, and runtime helpers." sidebar: - order: 228 + order: 245 --- ## `exp()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/exp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/exp.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function exp(float $num): float ## 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 2430877b10..2779c26376 100644 --- a/docs/internals/builtins/math/fdiv.md +++ b/docs/internals/builtins/math/fdiv.md @@ -2,15 +2,15 @@ title: "fdiv() — internals" description: "Compiler internals for fdiv(): lowering path, type checks, and runtime helpers." sidebar: - order: 229 + order: 246 --- ## `fdiv()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/binary.rs`:60](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/binary.rs#L60) (`lower_fdiv`) +- **Signature**: [`src/builtins/math/fdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/fdiv.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/binary.rs`:67](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/binary.rs#L67) (`lower_fdiv`) - **Function symbol**: `lower_fdiv()` @@ -35,4 +35,3 @@ function fdiv(float $num1, float $num2): float ## 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 547ba440e9..c197992643 100644 --- a/docs/internals/builtins/math/floor.md +++ b/docs/internals/builtins/math/floor.md @@ -2,15 +2,15 @@ title: "floor() — internals" description: "Compiler internals for floor(): lowering path, type checks, and runtime helpers." sidebar: - order: 230 + order: 247 --- ## `floor()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:70](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L70) (`lower_floor`) +- **Signature**: [`src/builtins/math/floor.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/floor.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:70](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L70) (`lower_floor`) - **Function symbol**: `lower_floor()` @@ -35,4 +35,3 @@ function floor(float $num): float ## 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 1b3dc05637..9ad291188d 100644 --- a/docs/internals/builtins/math/fmod.md +++ b/docs/internals/builtins/math/fmod.md @@ -2,15 +2,15 @@ title: "fmod() — internals" description: "Compiler internals for fmod(): lowering path, type checks, and runtime helpers." sidebar: - order: 231 + order: 248 --- ## `fmod()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/binary.rs`:85](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/binary.rs#L85) (`lower_fmod`) +- **Signature**: [`src/builtins/math/fmod.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/fmod.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/binary.rs`:92](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/binary.rs#L92) (`lower_fmod`) - **Function symbol**: `lower_fmod()` @@ -35,4 +35,3 @@ function fmod(float $num1, float $num2): float ## 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 ad0fe933b0..37b485883f 100644 --- a/docs/internals/builtins/math/hypot.md +++ b/docs/internals/builtins/math/hypot.md @@ -2,15 +2,15 @@ title: "hypot() — internals" description: "Compiler internals for hypot(): lowering path, type checks, and runtime helpers." sidebar: - order: 232 + order: 249 --- ## `hypot()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/libm.rs`:43](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/libm.rs#L43) (`lower_hypot`) +- **Signature**: [`src/builtins/math/hypot.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/hypot.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:43](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L43) (`lower_hypot`) - **Function symbol**: `lower_hypot()` @@ -35,4 +35,3 @@ function hypot(float $x, float $y): float ## 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 71703b1fdb..8121f26dc3 100644 --- a/docs/internals/builtins/math/intdiv.md +++ b/docs/internals/builtins/math/intdiv.md @@ -2,15 +2,15 @@ title: "intdiv() — internals" description: "Compiler internals for intdiv(): lowering path, type checks, and runtime helpers." sidebar: - order: 233 + order: 250 --- ## `intdiv()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/binary.rs`:21](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/binary.rs#L21) (`lower_intdiv`) +- **Signature**: [`src/builtins/math/intdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/intdiv.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/binary.rs`:23](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/binary.rs#L23) (`lower_intdiv`) - **Function symbol**: `lower_intdiv()` @@ -35,4 +35,3 @@ function intdiv(int $num1, int $num2): int ## 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 dff7e9964f..1ce482dba4 100644 --- a/docs/internals/builtins/math/is_finite.md +++ b/docs/internals/builtins/math/is_finite.md @@ -2,15 +2,15 @@ title: "is_finite() — internals" description: "Compiler internals for is_finite(): lowering path, type checks, and runtime helpers." sidebar: - order: 234 + order: 251 --- ## `is_finite()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:169](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L169) (`lower_is_finite`) +- **Signature**: [`src/builtins/types/is_finite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_finite.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:169](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L169) (`lower_is_finite`) - **Function symbol**: `lower_is_finite()` @@ -35,4 +35,3 @@ function is_finite(float $num): bool ## 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 a4bda4349e..341d462e3f 100644 --- a/docs/internals/builtins/math/is_infinite.md +++ b/docs/internals/builtins/math/is_infinite.md @@ -2,15 +2,15 @@ title: "is_infinite() — internals" description: "Compiler internals for is_infinite(): lowering path, type checks, and runtime helpers." sidebar: - order: 235 + order: 252 --- ## `is_infinite()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:132](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L132) (`lower_is_infinite`) +- **Signature**: [`src/builtins/types/is_infinite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_infinite.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:132](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L132) (`lower_is_infinite`) - **Function symbol**: `lower_is_infinite()` @@ -35,4 +35,3 @@ function is_infinite(float $num): bool ## 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 014931d616..3d54cef485 100644 --- a/docs/internals/builtins/math/is_nan.md +++ b/docs/internals/builtins/math/is_nan.md @@ -2,15 +2,15 @@ title: "is_nan() — internals" description: "Compiler internals for is_nan(): lowering path, type checks, and runtime helpers." sidebar: - order: 236 + order: 253 --- ## `is_nan()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:113](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L113) (`lower_is_nan`) +- **Signature**: [`src/builtins/types/is_nan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_nan.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:113](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L113) (`lower_is_nan`) - **Function symbol**: `lower_is_nan()` @@ -35,4 +35,3 @@ function is_nan(float $num): bool ## 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 1606887942..11999ebe30 100644 --- a/docs/internals/builtins/math/log.md +++ b/docs/internals/builtins/math/log.md @@ -2,15 +2,15 @@ title: "log() — internals" description: "Compiler internals for log(): lowering path, type checks, and runtime helpers." sidebar: - order: 237 + order: 254 --- ## `log()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/libm.rs`:51](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/libm.rs#L51) (`lower_log`) +- **Signature**: [`src/builtins/math/log.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:51](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L51) (`lower_log`) - **Function symbol**: `lower_log()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function log(float $num, float $base): float +function log(float $num, float $base = 2.718281828459045): float ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function log(float $num, float $base): float ## 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 afba603649..b02032e611 100644 --- a/docs/internals/builtins/math/log10.md +++ b/docs/internals/builtins/math/log10.md @@ -2,18 +2,22 @@ title: "log10() — internals" description: "Compiler internals for log10(): lowering path, type checks, and runtime helpers." sidebar: - order: 238 + order: 255 --- ## `log10()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/log10.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log10.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function log10(float $num): float ## 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 6426d8dca1..ffa1dbca15 100644 --- a/docs/internals/builtins/math/log2.md +++ b/docs/internals/builtins/math/log2.md @@ -2,18 +2,22 @@ title: "log2() — internals" description: "Compiler internals for log2(): lowering path, type checks, and runtime helpers." sidebar: - order: 239 + order: 256 --- ## `log2()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/log2.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log2.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function log2(float $num): float ## 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 e2a8df6d0c..0a31bbee79 100644 --- a/docs/internals/builtins/math/max.md +++ b/docs/internals/builtins/math/max.md @@ -2,15 +2,15 @@ title: "max() — internals" description: "Compiler internals for max(): lowering path, type checks, and runtime helpers." sidebar: - order: 240 + order: 257 --- ## `max()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:204](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L204) (`lower_min_max`) +- **Signature**: [`src/builtins/math/max.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/max.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:204](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L204) (`lower_min_max`) - **Function symbol**: `lower_min_max()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function max(mixed $value, ...$values): float +function max(mixed $value, ...$values): mixed ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function max(mixed $value, ...$values): float ## 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 f9fe453464..d39f3d4b3e 100644 --- a/docs/internals/builtins/math/min.md +++ b/docs/internals/builtins/math/min.md @@ -2,15 +2,15 @@ title: "min() — internals" description: "Compiler internals for min(): lowering path, type checks, and runtime helpers." sidebar: - order: 241 + order: 258 --- ## `min()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:204](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L204) (`lower_min_max`) +- **Signature**: [`src/builtins/math/min.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/min.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:204](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L204) (`lower_min_max`) - **Function symbol**: `lower_min_max()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function min(mixed $value, ...$values): float +function min(mixed $value, ...$values): mixed ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function min(mixed $value, ...$values): float ## 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 c7f3edcc13..468eb0ea11 100644 --- a/docs/internals/builtins/math/mt_rand.md +++ b/docs/internals/builtins/math/mt_rand.md @@ -2,21 +2,26 @@ title: "mt_rand() — internals" description: "Compiler internals for mt_rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 242 + order: 259 --- ## `mt_rand()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/mt_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/mt_rand.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/random.rs`:21](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/random.rs#L21) (`lower_rand`) +- **Function symbol**: `lower_rand()` +### Lowering notes + +- Lowers `rand()` and `mt_rand()` with either zero args or an inclusive range. + ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_random_u32` ## Signature summary @@ -31,4 +36,3 @@ function mt_rand(int $min, int $max): int ## 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 d08cd290c8..16b2a09935 100644 --- a/docs/internals/builtins/math/pi.md +++ b/docs/internals/builtins/math/pi.md @@ -2,21 +2,21 @@ title: "pi() — internals" description: "Compiler internals for pi(): lowering path, type checks, and runtime helpers." sidebar: - order: 243 + order: 260 --- ## `pi()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:596](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L596) (`lower_pi`) +- **Signature**: [`src/builtins/math/pi.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/pi.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:240](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L240) (`lower_pi`) - **Function symbol**: `lower_pi()` ### Lowering notes -- Lowers `pi()` as the same data-section float constant used by the legacy backend. +- Lowers `pi()` as a data-section float constant. ## Runtime helpers @@ -35,4 +35,3 @@ function pi(): float ## 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 f3a8668d08..e0996e87bf 100644 --- a/docs/internals/builtins/math/pow.md +++ b/docs/internals/builtins/math/pow.md @@ -2,15 +2,15 @@ title: "pow() — internals" description: "Compiler internals for pow(): lowering path, type checks, and runtime helpers." sidebar: - order: 244 + order: 261 --- ## `pow()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/binary.rs`:114](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/binary.rs#L114) (`lower_pow`) +- **Signature**: [`src/builtins/math/pow.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/pow.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/binary.rs`:121](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/binary.rs#L121) (`lower_pow`) - **Function symbol**: `lower_pow()` @@ -35,4 +35,3 @@ function pow(float $num, float $exponent): float ## 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 c3130e4dfe..621a148d0f 100644 --- a/docs/internals/builtins/math/rad2deg.md +++ b/docs/internals/builtins/math/rad2deg.md @@ -2,15 +2,15 @@ title: "rad2deg() — internals" description: "Compiler internals for rad2deg(): lowering path, type checks, and runtime helpers." sidebar: - order: 245 + order: 262 --- ## `rad2deg()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/libm.rs`:83](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/libm.rs#L83) (`lower_rad2deg`) +- **Signature**: [`src/builtins/math/rad2deg.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/rad2deg.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:83](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L83) (`lower_rad2deg`) - **Function symbol**: `lower_rad2deg()` @@ -35,4 +35,3 @@ function rad2deg(float $num): float ## 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 0f0d368aa7..6aa48de0d2 100644 --- a/docs/internals/builtins/math/rand.md +++ b/docs/internals/builtins/math/rand.md @@ -2,21 +2,26 @@ title: "rand() — internals" description: "Compiler internals for rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 246 + order: 263 --- ## `rand()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/rand.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/random.rs`:21](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/random.rs#L21) (`lower_rand`) +- **Function symbol**: `lower_rand()` +### Lowering notes + +- Lowers `rand()` and `mt_rand()` with either zero args or an inclusive range. + ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_random_u32` ## Signature summary @@ -31,4 +36,3 @@ function rand(int $min, int $max): int ## Cross-references - [User reference for `rand()`](../../../php/builtins/math/rand.md) - diff --git a/docs/internals/builtins/math/random_bytes.md b/docs/internals/builtins/math/random_bytes.md new file mode 100644 index 0000000000..8c32fe8a32 --- /dev/null +++ b/docs/internals/builtins/math/random_bytes.md @@ -0,0 +1,44 @@ +--- +title: "random_bytes() — internals" +description: "Compiler internals for random_bytes(): lowering path, type checks, and runtime helpers." +sidebar: + order: 264 +--- + +## `random_bytes()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/math/random_bytes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/random_bytes.rs) +- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/random.rs`:58](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/random.rs#L58) (`lower_random_bytes`) +- **Function symbol**: `lower_random_bytes()` + + +### Lowering notes + +- Lowers `random_bytes()` into an owned CSPRNG binary string of the given length. +- Materializes the single length operand as an integer, passes it to the +- `__rt_random_bytes` runtime helper (length in `x0` on AArch64, `rdi` on +- x86_64), and stores the returned owned string result (`x1`/`x2` on AArch64, +- `rax`/`rdx` on x86_64) into the instruction's result slot. The runtime helper +- owns allocation, the cryptographic fill, and the fatal paths for a length +- below 1 or an unavailable entropy source. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_random_bytes` + +## Signature summary + +```php +function random_bytes(int $length): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Cross-references + +- [User reference for `random_bytes()`](../../../php/builtins/math/random_bytes.md) diff --git a/docs/internals/builtins/math/random_int.md b/docs/internals/builtins/math/random_int.md index 59b9d8c246..90c988932a 100644 --- a/docs/internals/builtins/math/random_int.md +++ b/docs/internals/builtins/math/random_int.md @@ -2,15 +2,15 @@ title: "random_int() — internals" description: "Compiler internals for random_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 247 + order: 264 --- ## `random_int()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math/random.rs`:40](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math/random.rs#L40) (`lower_random_int`) +- **Signature**: [`src/builtins/math/random_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/random_int.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/random.rs`:40](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/random.rs#L40) (`lower_random_int`) - **Function symbol**: `lower_random_int()` @@ -35,4 +35,3 @@ function random_int(int $min, int $max): int ## 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 13ab162501..7cdea6d462 100644 --- a/docs/internals/builtins/math/round.md +++ b/docs/internals/builtins/math/round.md @@ -2,15 +2,15 @@ title: "round() — internals" description: "Compiler internals for round(): lowering path, type checks, and runtime helpers." sidebar: - order: 248 + order: 266 --- ## `round()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:186](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L186) (`lower_round`) +- **Signature**: [`src/builtins/math/round.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/round.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:186](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L186) (`lower_round`) - **Function symbol**: `lower_round()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function round(float $num, int $precision): float +function round(float $num, int $precision = 0): float ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function round(float $num, int $precision): float ## 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 c7ae73c578..ea3754ab6c 100644 --- a/docs/internals/builtins/math/sin.md +++ b/docs/internals/builtins/math/sin.md @@ -2,18 +2,22 @@ title: "sin() — internals" description: "Compiler internals for sin(): lowering path, type checks, and runtime helpers." sidebar: - order: 249 + order: 267 --- ## `sin()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/sin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sin.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function sin(float $num): float ## 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 a163be2535..486c00031d 100644 --- a/docs/internals/builtins/math/sinh.md +++ b/docs/internals/builtins/math/sinh.md @@ -2,18 +2,22 @@ title: "sinh() — internals" description: "Compiler internals for sinh(): lowering path, type checks, and runtime helpers." sidebar: - order: 250 + order: 268 --- ## `sinh()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/sinh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sinh.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function sinh(float $num): float ## 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 6d4136edb1..c185414a37 100644 --- a/docs/internals/builtins/math/sqrt.md +++ b/docs/internals/builtins/math/sqrt.md @@ -2,15 +2,15 @@ title: "sqrt() — internals" description: "Compiler internals for sqrt(): lowering path, type checks, and runtime helpers." sidebar: - order: 251 + order: 269 --- ## `sqrt()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/math.rs`:97](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/math.rs#L97) (`lower_sqrt`) +- **Signature**: [`src/builtins/math/sqrt.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sqrt.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math.rs`:97](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math.rs#L97) (`lower_sqrt`) - **Function symbol**: `lower_sqrt()` @@ -35,4 +35,3 @@ function sqrt(float $num): float ## 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 51c42b04e1..6794cf0a54 100644 --- a/docs/internals/builtins/math/tan.md +++ b/docs/internals/builtins/math/tan.md @@ -2,18 +2,22 @@ title: "tan() — internals" description: "Compiler internals for tan(): lowering path, type checks, and runtime helpers." sidebar: - order: 252 + order: 270 --- ## `tan()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/tan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/tan.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function tan(float $num): float ## 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 3163cce60e..c4c88eb396 100644 --- a/docs/internals/builtins/math/tanh.md +++ b/docs/internals/builtins/math/tanh.md @@ -2,18 +2,22 @@ title: "tanh() — internals" description: "Compiler internals for tanh(): lowering path, type checks, and runtime helpers." sidebar: - order: 253 + order: 271 --- ## `tanh()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/math/tanh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/tanh.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/math/libm.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/math/libm.rs#L22) (`lower_unary_libm`) +- **Function symbol**: `lower_unary_libm()` +### Lowering notes + +- Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function tanh(float $num): float ## 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 69e9edf6fe..1dda6a2d93 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: 257 + order: 272 --- ## `buffer_new()` — internals @@ -31,4 +31,3 @@ function buffer_new(int $length): mixed ## Cross-references - [User reference for `buffer_new()`](../../../php/builtins/misc/buffer_new.md) - diff --git a/docs/internals/builtins/misc/call_user_func.md b/docs/internals/builtins/misc/call_user_func.md deleted file mode 100644 index ef30267e5e..0000000000 --- a/docs/internals/builtins/misc/call_user_func.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: "call_user_func() — internals" -description: "Compiler internals for call_user_func(): lowering path, type checks, and runtime helpers." -sidebar: - order: 258 ---- - -## `call_user_func()` — internals - -## Where it lives - -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.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 call_user_func(callable $callback, ...$args): mixed -``` - -## What the type checker enforces - -- **Arity**: takes exactly 1 argument. -- **Variadic**: collects excess arguments into `$args`. - -## Cross-references - -- [User reference for `call_user_func()`](../../../php/builtins/misc/call_user_func.md) - diff --git a/docs/internals/builtins/misc/call_user_func_array.md b/docs/internals/builtins/misc/call_user_func_array.md deleted file mode 100644 index a558856fad..0000000000 --- a/docs/internals/builtins/misc/call_user_func_array.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "call_user_func_array() — internals" -description: "Compiler internals for call_user_func_array(): lowering path, type checks, and runtime helpers." -sidebar: - order: 259 ---- - -## `call_user_func_array()` — internals - -## Where it lives - -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.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 call_user_func_array(callable $callback, array $args): mixed -``` - -## What the type checker enforces - -- **Arity**: takes exactly 2 arguments. - -## Cross-references - -- [User reference for `call_user_func_array()`](../../../php/builtins/misc/call_user_func_array.md) - diff --git a/docs/internals/builtins/misc/define.md b/docs/internals/builtins/misc/define.md index 09a870ad7b..a9e5610097 100644 --- a/docs/internals/builtins/misc/define.md +++ b/docs/internals/builtins/misc/define.md @@ -2,21 +2,21 @@ title: "define() — internals" description: "Compiler internals for define(): lowering path, type checks, and runtime helpers." sidebar: - order: 260 + order: 273 --- ## `define()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:549](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L549) (`lower_define`) +- **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`) - **Function symbol**: `lower_define()` ### Lowering notes -- Lowers `define("NAME", value)` with the legacy duplicate-name runtime guard. +- Lowers `define("NAME", value)` with the duplicate-name runtime guard. ## Runtime helpers @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function define(string $constant_name, mixed $value, bool $case_insensitive): bool +function define(string $constant_name, mixed $value): bool ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 2 arguments. ## 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 50f2a3ac93..73775cde67 100644 --- a/docs/internals/builtins/misc/defined.md +++ b/docs/internals/builtins/misc/defined.md @@ -2,15 +2,15 @@ title: "defined() — internals" description: "Compiler internals for defined(): lowering path, type checks, and runtime helpers." sidebar: - order: 261 + order: 274 --- ## `defined()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:744](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L744) (`lower_defined`) +- **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`) - **Function symbol**: `lower_defined()` @@ -35,4 +35,3 @@ function defined(string $constant_name): bool ## 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 9de298217f..4584c0290f 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: 262 + order: 275 --- ## `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_ir/lower_inst/builtins.rs`:1096](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L1096) (`lower_empty`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:618](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L618) (`lower_empty`) - **Function symbol**: `lower_empty()` @@ -36,4 +36,3 @@ function empty(mixed $value): bool ## 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 db1cb11bac..d57a5fe98c 100644 --- a/docs/internals/builtins/misc/header.md +++ b/docs/internals/builtins/misc/header.md @@ -2,15 +2,15 @@ title: "header() — internals" description: "Compiler internals for header(): lowering path, type checks, and runtime helpers." sidebar: - order: 263 + order: 276 --- ## `header()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:289](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L289) (`lower_header`) +- **Signature**: [`src/builtins/system/header.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/header.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:289](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L289) (`lower_header`) - **Function symbol**: `lower_header()` @@ -31,7 +31,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function header(mixed $header, mixed $replace, mixed $response_code): void +function header(string $header, bool $replace = true, int $response_code = 0): void ``` ## What the type checker enforces @@ -41,4 +41,3 @@ function header(mixed $header, mixed $replace, mixed $response_code): void ## 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 f4e81d3959..d42da3c558 100644 --- a/docs/internals/builtins/misc/http_response_code.md +++ b/docs/internals/builtins/misc/http_response_code.md @@ -2,15 +2,15 @@ title: "http_response_code() — internals" description: "Compiler internals for http_response_code(): lowering path, type checks, and runtime helpers." sidebar: - order: 264 + order: 277 --- ## `http_response_code()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:264](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L264) (`lower_http_response_code`) +- **Signature**: [`src/builtins/system/http_response_code.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/http_response_code.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:264](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L264) (`lower_http_response_code`) - **Function symbol**: `lower_http_response_code()` @@ -30,7 +30,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function http_response_code(mixed $response_code): int +function http_response_code(int $response_code = 0): int ``` ## What the type checker enforces @@ -40,4 +40,3 @@ function http_response_code(mixed $response_code): int ## 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 4ad04b8410..b580985d37 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: 265 + order: 278 --- ## `isset()` — 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_ir/lower_inst/builtins/isset.rs`:24](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/isset.rs#L24) (`lower_isset`) +- **Lowering**: [`src/codegen/lower_inst/builtins/isset.rs`:24](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/isset.rs#L24) (`lower_isset`) - **Function symbol**: `lower_isset()` @@ -36,4 +36,3 @@ function isset(mixed $var, ...$vars): bool ## 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 5146a53839..0e02bd290c 100644 --- a/docs/internals/builtins/misc/php_uname.md +++ b/docs/internals/builtins/misc/php_uname.md @@ -2,15 +2,15 @@ title: "php_uname() — internals" description: "Compiler internals for php_uname(): lowering path, type checks, and runtime helpers." sidebar: - order: 266 + order: 279 --- ## `php_uname()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:672](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L672) (`lower_php_uname`) +- **Signature**: [`src/builtins/system/php_uname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/php_uname.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:672](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L672) (`lower_php_uname`) - **Function symbol**: `lower_php_uname()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function php_uname(string $mode): string +function php_uname(string $mode = 'a'): string ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function php_uname(string $mode): string ## 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 0244314646..455c9c1aa7 100644 --- a/docs/internals/builtins/misc/phpversion.md +++ b/docs/internals/builtins/misc/phpversion.md @@ -2,15 +2,15 @@ title: "phpversion() — internals" description: "Compiler internals for phpversion(): lowering path, type checks, and runtime helpers." sidebar: - order: 267 + order: 280 --- ## `phpversion()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:734](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L734) (`lower_phpversion`) +- **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`) - **Function symbol**: `lower_phpversion()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function phpversion(string $extension = null): string +function phpversion(): string ``` ## What the type checker enforces -- **Arity**: takes 0–1 arguments (1 optional). +- **Arity**: takes no arguments. ## 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 e54c254b0c..98a9ccf0f0 100644 --- a/docs/internals/builtins/misc/print_r.md +++ b/docs/internals/builtins/misc/print_r.md @@ -2,15 +2,15 @@ title: "print_r() — internals" description: "Compiler internals for print_r(): lowering path, type checks, and runtime helpers." sidebar: - order: 268 + order: 281 --- ## `print_r()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/debug.rs`:24](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/debug.rs#L24) (`lower_print_r`) +- **Signature**: [`src/builtins/io/print_r.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/print_r.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/debug.rs`:24](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/debug.rs#L24) (`lower_print_r`) - **Function symbol**: `lower_print_r()` @@ -25,15 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function print_r(...$values): void +function print_r(mixed $value): void ``` ## What the type checker enforces -- **Arity**: takes no arguments. -- **Variadic**: collects excess arguments into `$values`. +- **Arity**: takes exactly 1 argument. ## 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 new file mode 100644 index 0000000000..58e96c3e1d --- /dev/null +++ b/docs/internals/builtins/misc/serialize.md @@ -0,0 +1,43 @@ +--- +title: "serialize() — internals" +description: "Compiler internals for serialize(): lowering path, type checks, and runtime helpers." +sidebar: + order: 282 +--- + +## `serialize()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/system/serialize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/serialize.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/serialize.rs`:33](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/serialize.rs#L33) (`lower_serialize`) +- **Function symbol**: `lower_serialize()` + + +### Lowering notes + +- Lowers `serialize($value)` into the shared serialize runtime helper. +- Scalar static types are formatted directly through `__rt_serialize_value`; a +- Mixed/Union argument is unboxed and dispatched by `__rt_serialize_mixed`. +- Non-scalar static types (arrays/objects) are not yet supported and are rejected. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_serialize_begin` +- `__rt_serialize_mixed` +- `__rt_serialize_value` + +## Signature summary + +```php +function serialize(mixed $value): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## 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 new file mode 100644 index 0000000000..ce2667acfd --- /dev/null +++ b/docs/internals/builtins/misc/unserialize.md @@ -0,0 +1,43 @@ +--- +title: "unserialize() — internals" +description: "Compiler internals for unserialize(): lowering path, type checks, and runtime helpers." +sidebar: + order: 283 +--- + +## `unserialize()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/system/unserialize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/unserialize.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/serialize.rs`:164](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/serialize.rs#L164) (`lower_unserialize`) +- **Function symbol**: `lower_unserialize()` + + +### Lowering notes + +- Lowers `unserialize($data, $options?)` into the shared unserialize runtime helper. +- The source string is parsed by `__rt_unserialize_mixed`; a null result pointer +- (parse error or unsupported wire form) is boxed as PHP `false`. The optional +- `$options` argument is accepted but currently ignored. + +## Runtime helpers + +The following runtime helpers are referenced: +- `__rt_mixed_cast_string` +- `__rt_unserialize_begin` +- `__rt_unserialize_mixed` + +## Signature summary + +```php +function unserialize(string $data, mixed $options = []): mixed +``` + +## What the type checker enforces + +- **Arity**: takes 1–2 arguments (1 optional). + +## 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 d530ea5f9d..cfa29e94fa 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: 269 + order: 284 --- ## `unset()` — 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_ir/lower_inst/builtins/types.rs`:48](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L48) (`lower_unset_builtin`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:48](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L48) (`lower_unset_builtin`) - **Function symbol**: `lower_unset_builtin()` @@ -36,4 +36,3 @@ function unset(mixed $var, ...$vars): void ## 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 375de96f66..2a9981d77b 100644 --- a/docs/internals/builtins/misc/var_dump.md +++ b/docs/internals/builtins/misc/var_dump.md @@ -2,15 +2,15 @@ title: "var_dump() — internals" description: "Compiler internals for var_dump(): lowering path, type checks, and runtime helpers." sidebar: - order: 270 + order: 285 --- ## `var_dump()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/debug.rs`:35](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/debug.rs#L35) (`lower_var_dump`) +- **Signature**: [`src/builtins/io/var_dump.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/var_dump.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/debug.rs`:35](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/debug.rs#L35) (`lower_var_dump`) - **Function symbol**: `lower_var_dump()` @@ -25,15 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function var_dump(...$values): void +function var_dump(mixed $value): void ``` ## What the type checker enforces -- **Arity**: takes no arguments. -- **Variadic**: collects excess arguments into `$values`. +- **Arity**: takes exactly 1 argument. ## 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 1f77ef5b5b..33eafec841 100644 --- a/docs/internals/builtins/pointer/ptr.md +++ b/docs/internals/builtins/pointer/ptr.md @@ -2,15 +2,15 @@ title: "ptr() — internals" description: "Compiler internals for ptr(): lowering path, type checks, and runtime helpers." sidebar: - order: 271 + order: 286 --- ## `ptr()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:25](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L25) (`lower_ptr`) +- **Signature**: [`src/builtins/pointers/ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:25](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L25) (`lower_ptr`) - **Function symbol**: `lower_ptr()` @@ -35,4 +35,3 @@ function ptr(mixed $value): mixed ## 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 dcc82ef40b..bd233c1b2f 100644 --- a/docs/internals/builtins/pointer/ptr_get.md +++ b/docs/internals/builtins/pointer/ptr_get.md @@ -2,15 +2,15 @@ title: "ptr_get() — internals" description: "Compiler internals for ptr_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 272 + order: 287 --- ## `ptr_get()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:109](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L109) (`lower_ptr_get`) +- **Signature**: [`src/builtins/pointers/ptr_get.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_get.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:109](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L109) (`lower_ptr_get`) - **Function symbol**: `lower_ptr_get()` @@ -35,4 +35,3 @@ function ptr_get(pointer $pointer): int ## 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 6bbf36553d..fef096c1d9 100644 --- a/docs/internals/builtins/pointer/ptr_is_null.md +++ b/docs/internals/builtins/pointer/ptr_is_null.md @@ -2,15 +2,15 @@ title: "ptr_is_null() — internals" description: "Compiler internals for ptr_is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 273 + order: 288 --- ## `ptr_is_null()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:56](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L56) (`lower_ptr_is_null`) +- **Signature**: [`src/builtins/pointers/ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_is_null.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:56](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L56) (`lower_ptr_is_null`) - **Function symbol**: `lower_ptr_is_null()` @@ -35,4 +35,3 @@ function ptr_is_null(pointer $pointer): bool ## 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 ced645e9d5..18835ffd50 100644 --- a/docs/internals/builtins/pointer/ptr_null.md +++ b/docs/internals/builtins/pointer/ptr_null.md @@ -2,15 +2,15 @@ title: "ptr_null() — internals" description: "Compiler internals for ptr_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 274 + order: 289 --- ## `ptr_null()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:49](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L49) (`lower_ptr_null`) +- **Signature**: [`src/builtins/pointers/ptr_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_null.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:49](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L49) (`lower_ptr_null`) - **Function symbol**: `lower_ptr_null()` @@ -35,4 +35,3 @@ function ptr_null(): mixed ## 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 89d582d276..22bfae92ac 100644 --- a/docs/internals/builtins/pointer/ptr_offset.md +++ b/docs/internals/builtins/pointer/ptr_offset.md @@ -2,15 +2,15 @@ title: "ptr_offset() — internals" description: "Compiler internals for ptr_offset(): lowering path, type checks, and runtime helpers." sidebar: - order: 275 + order: 290 --- ## `ptr_offset()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:86](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L86) (`lower_ptr_offset`) +- **Signature**: [`src/builtins/pointers/ptr_offset.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_offset.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:86](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L86) (`lower_ptr_offset`) - **Function symbol**: `lower_ptr_offset()` @@ -35,4 +35,3 @@ function ptr_offset(pointer $pointer, int $offset): mixed ## 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 982fc21296..e3d14d3f76 100644 --- a/docs/internals/builtins/pointer/ptr_read16.md +++ b/docs/internals/builtins/pointer/ptr_read16.md @@ -2,15 +2,15 @@ title: "ptr_read16() — internals" description: "Compiler internals for ptr_read16(): lowering path, type checks, and runtime helpers." sidebar: - order: 276 + order: 291 --- ## `ptr_read16()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:124](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L124) (`lower_ptr_read16`) +- **Signature**: [`src/builtins/pointers/ptr_read16.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read16.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:124](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L124) (`lower_ptr_read16`) - **Function symbol**: `lower_ptr_read16()` @@ -36,4 +36,3 @@ function ptr_read16(pointer $pointer): int ## 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 bd376ef247..1a815882ee 100644 --- a/docs/internals/builtins/pointer/ptr_read32.md +++ b/docs/internals/builtins/pointer/ptr_read32.md @@ -2,15 +2,15 @@ title: "ptr_read32() — internals" description: "Compiler internals for ptr_read32(): lowering path, type checks, and runtime helpers." sidebar: - order: 277 + order: 292 --- ## `ptr_read32()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:129](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L129) (`lower_ptr_read32`) +- **Signature**: [`src/builtins/pointers/ptr_read32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read32.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:129](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L129) (`lower_ptr_read32`) - **Function symbol**: `lower_ptr_read32()` @@ -36,4 +36,3 @@ function ptr_read32(pointer $pointer): int ## 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 3d4acf7caa..5770540c95 100644 --- a/docs/internals/builtins/pointer/ptr_read8.md +++ b/docs/internals/builtins/pointer/ptr_read8.md @@ -2,15 +2,15 @@ title: "ptr_read8() — internals" description: "Compiler internals for ptr_read8(): lowering path, type checks, and runtime helpers." sidebar: - order: 278 + order: 293 --- ## `ptr_read8()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:119](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L119) (`lower_ptr_read8`) +- **Signature**: [`src/builtins/pointers/ptr_read8.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read8.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:119](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L119) (`lower_ptr_read8`) - **Function symbol**: `lower_ptr_read8()` @@ -35,4 +35,3 @@ function ptr_read8(pointer $pointer): int ## 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 50e8b0cd90..d7e8257cd8 100644 --- a/docs/internals/builtins/pointer/ptr_read_string.md +++ b/docs/internals/builtins/pointer/ptr_read_string.md @@ -2,15 +2,15 @@ title: "ptr_read_string() — internals" description: "Compiler internals for ptr_read_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 279 + order: 294 --- ## `ptr_read_string()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:134](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L134) (`lower_ptr_read_string`) +- **Signature**: [`src/builtins/pointers/ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read_string.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:134](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L134) (`lower_ptr_read_string`) - **Function symbol**: `lower_ptr_read_string()` @@ -36,4 +36,3 @@ function ptr_read_string(pointer $pointer, int $length): string ## 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 9e2acbfdf7..3b86f5c22c 100644 --- a/docs/internals/builtins/pointer/ptr_set.md +++ b/docs/internals/builtins/pointer/ptr_set.md @@ -2,15 +2,15 @@ title: "ptr_set() — internals" description: "Compiler internals for ptr_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 280 + order: 295 --- ## `ptr_set()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:114](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L114) (`lower_ptr_set`) +- **Signature**: [`src/builtins/pointers/ptr_set.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_set.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:114](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L114) (`lower_ptr_set`) - **Function symbol**: `lower_ptr_set()` @@ -35,4 +35,3 @@ function ptr_set(pointer $pointer, mixed $value): void ## 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 91a149dcbd..cf5fb0e3cf 100644 --- a/docs/internals/builtins/pointer/ptr_sizeof.md +++ b/docs/internals/builtins/pointer/ptr_sizeof.md @@ -2,15 +2,15 @@ title: "ptr_sizeof() — internals" description: "Compiler internals for ptr_sizeof(): lowering path, type checks, and runtime helpers." sidebar: - order: 281 + order: 296 --- ## `ptr_sizeof()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:75](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L75) (`lower_ptr_sizeof`) +- **Signature**: [`src/builtins/pointers/ptr_sizeof.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_sizeof.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:75](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L75) (`lower_ptr_sizeof`) - **Function symbol**: `lower_ptr_sizeof()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function ptr_sizeof(string $type): mixed +function ptr_sizeof(string $type): int ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function ptr_sizeof(string $type): mixed ## 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 32ebfd1f48..f7a50dd294 100644 --- a/docs/internals/builtins/pointer/ptr_write16.md +++ b/docs/internals/builtins/pointer/ptr_write16.md @@ -2,15 +2,15 @@ title: "ptr_write16() — internals" description: "Compiler internals for ptr_write16(): lowering path, type checks, and runtime helpers." sidebar: - order: 282 + order: 297 --- ## `ptr_write16()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:161](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L161) (`lower_ptr_write16`) +- **Signature**: [`src/builtins/pointers/ptr_write16.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write16.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:161](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L161) (`lower_ptr_write16`) - **Function symbol**: `lower_ptr_write16()` @@ -36,4 +36,3 @@ function ptr_write16(pointer $pointer, int $value): void ## 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 8b6845a590..bdb4202848 100644 --- a/docs/internals/builtins/pointer/ptr_write32.md +++ b/docs/internals/builtins/pointer/ptr_write32.md @@ -2,15 +2,15 @@ title: "ptr_write32() — internals" description: "Compiler internals for ptr_write32(): lowering path, type checks, and runtime helpers." sidebar: - order: 283 + order: 298 --- ## `ptr_write32()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:166](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L166) (`lower_ptr_write32`) +- **Signature**: [`src/builtins/pointers/ptr_write32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write32.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:166](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L166) (`lower_ptr_write32`) - **Function symbol**: `lower_ptr_write32()` @@ -36,4 +36,3 @@ function ptr_write32(pointer $pointer, int $value): void ## 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 8946480827..8b2c9267a4 100644 --- a/docs/internals/builtins/pointer/ptr_write8.md +++ b/docs/internals/builtins/pointer/ptr_write8.md @@ -2,15 +2,15 @@ title: "ptr_write8() — internals" description: "Compiler internals for ptr_write8(): lowering path, type checks, and runtime helpers." sidebar: - order: 284 + order: 299 --- ## `ptr_write8()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:156](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L156) (`lower_ptr_write8`) +- **Signature**: [`src/builtins/pointers/ptr_write8.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write8.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:156](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L156) (`lower_ptr_write8`) - **Function symbol**: `lower_ptr_write8()` @@ -35,4 +35,3 @@ function ptr_write8(pointer $pointer, int $value): void ## 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 fced27d4cd..4d719ec3d6 100644 --- a/docs/internals/builtins/pointer/ptr_write_string.md +++ b/docs/internals/builtins/pointer/ptr_write_string.md @@ -2,15 +2,15 @@ title: "ptr_write_string() — internals" description: "Compiler internals for ptr_write_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 285 + order: 300 --- ## `ptr_write_string()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/pointers.rs`:171](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/pointers.rs#L171) (`lower_ptr_write_string`) +- **Signature**: [`src/builtins/pointers/ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write_string.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/pointers.rs`:171](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/pointers.rs#L171) (`lower_ptr_write_string`) - **Function symbol**: `lower_ptr_write_string()` @@ -36,4 +36,3 @@ function ptr_write_string(pointer $pointer, string $string): int ## Cross-references - [User reference for `ptr_write_string()`](../../../php/builtins/pointer/ptr_write_string.md) - diff --git a/docs/internals/builtins/process/die.md b/docs/internals/builtins/process/die.md index 2dc4acbc48..a1c0c531ee 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: 286 + order: 301 --- ## `die()` — internals @@ -31,4 +31,3 @@ function die(int $status): void ## 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 0d39321c3c..44432db163 100644 --- a/docs/internals/builtins/process/exec.md +++ b/docs/internals/builtins/process/exec.md @@ -2,15 +2,15 @@ title: "exec() — internals" description: "Compiler internals for exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 287 + order: 302 --- ## `exec()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:690](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L690) (`lower_exec`) +- **Signature**: [`src/builtins/system/exec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/exec.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:690](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L690) (`lower_exec`) - **Function symbol**: `lower_exec()` @@ -25,15 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function exec(string $command, array $output, int $result_code): string +function exec(string $command): string ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. -- **By-reference parameters**: `$output`, `$result_code`. +- **Arity**: takes exactly 1 argument. ## 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 eea2adb3b2..a768b10347 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: 288 + order: 303 --- ## `exit()` — internals @@ -31,4 +31,3 @@ function exit(int $status): void ## 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 43789b8549..06276d88f6 100644 --- a/docs/internals/builtins/process/passthru.md +++ b/docs/internals/builtins/process/passthru.md @@ -2,15 +2,15 @@ title: "passthru() — internals" description: "Compiler internals for passthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 289 + order: 304 --- ## `passthru()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:714](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L714) (`lower_passthru`) +- **Signature**: [`src/builtins/system/passthru.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/passthru.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:714](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L714) (`lower_passthru`) - **Function symbol**: `lower_passthru()` @@ -27,15 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function passthru(string $command, int $result_code): void +function passthru(string $command): void ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. -- **By-reference parameters**: `$result_code`. +- **Arity**: takes exactly 1 argument. ## 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 f8a495d3b5..a48b4f5cf9 100644 --- a/docs/internals/builtins/process/pclose.md +++ b/docs/internals/builtins/process/pclose.md @@ -2,15 +2,15 @@ title: "pclose() — internals" description: "Compiler internals for pclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 290 + order: 305 --- ## `pclose()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3407](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3407) (`lower_pclose`) +- **Signature**: [`src/builtins/io/pclose.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pclose.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3630](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3630) (`lower_pclose`) - **Function symbol**: `lower_pclose()` @@ -36,4 +36,3 @@ function pclose(resource $handle): int ## 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 0ffa3ab135..f7411ab5b0 100644 --- a/docs/internals/builtins/process/popen.md +++ b/docs/internals/builtins/process/popen.md @@ -2,15 +2,15 @@ title: "popen() — internals" description: "Compiler internals for popen(): lowering path, type checks, and runtime helpers." sidebar: - order: 291 + order: 306 --- ## `popen()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:3379](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L3379) (`lower_popen`) +- **Signature**: [`src/builtins/io/popen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/popen.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3602](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3602) (`lower_popen`) - **Function symbol**: `lower_popen()` @@ -36,4 +36,3 @@ function popen(string $command, string $mode): mixed ## 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 7e35fbc3fe..1fcff99246 100644 --- a/docs/internals/builtins/process/readline.md +++ b/docs/internals/builtins/process/readline.md @@ -2,15 +2,15 @@ title: "readline() — internals" description: "Compiler internals for readline(): lowering path, type checks, and runtime helpers." sidebar: - order: 292 + order: 307 --- ## `readline()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/io.rs`:203](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/io.rs#L203) (`lower_readline`) +- **Signature**: [`src/builtins/io/readline.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readline.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:310](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L310) (`lower_readline`) - **Function symbol**: `lower_readline()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function readline(string $prompt): mixed +function readline(string $prompt = null): mixed ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function readline(string $prompt): mixed ## 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 fdcd90c079..a93a1590a2 100644 --- a/docs/internals/builtins/process/shell_exec.md +++ b/docs/internals/builtins/process/shell_exec.md @@ -2,15 +2,15 @@ title: "shell_exec() — internals" description: "Compiler internals for shell_exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 293 + order: 308 --- ## `shell_exec()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:698](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L698) (`lower_shell_exec`) +- **Signature**: [`src/builtins/system/shell_exec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/shell_exec.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:698](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L698) (`lower_shell_exec`) - **Function symbol**: `lower_shell_exec()` @@ -35,4 +35,3 @@ function shell_exec(string $command): string ## 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 eeffa0102c..47d9f295b1 100644 --- a/docs/internals/builtins/process/sleep.md +++ b/docs/internals/builtins/process/sleep.md @@ -2,15 +2,15 @@ title: "sleep() — internals" description: "Compiler internals for sleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 294 + order: 309 --- ## `sleep()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:473](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L473) (`lower_sleep`) +- **Signature**: [`src/builtins/system/sleep.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/sleep.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:473](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L473) (`lower_sleep`) - **Function symbol**: `lower_sleep()` @@ -36,4 +36,3 @@ function sleep(int $seconds): int ## 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 29f3917f0e..11f10b11b8 100644 --- a/docs/internals/builtins/process/system.md +++ b/docs/internals/builtins/process/system.md @@ -2,21 +2,21 @@ title: "system() — internals" description: "Compiler internals for system(): lowering path, type checks, and runtime helpers." sidebar: - order: 295 + order: 310 --- ## `system()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:706](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L706) (`lower_system`) +- **Signature**: [`src/builtins/system/system.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/system.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:706](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L706) (`lower_system`) - **Function symbol**: `lower_system()` ### Lowering notes -- Lowers `system(command)` through libc `system()` and returns the legacy empty string result. +- Lowers `system(command)` through libc `system()` and returns the compiler's empty string result. ## Runtime helpers @@ -26,15 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function system(string $command, int $result_code): string +function system(string $command): string ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. -- **By-reference parameters**: `$result_code`. +- **Arity**: takes exactly 1 argument. ## 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 3368c4957c..062ece31b8 100644 --- a/docs/internals/builtins/process/usleep.md +++ b/docs/internals/builtins/process/usleep.md @@ -2,15 +2,15 @@ title: "usleep() — internals" description: "Compiler internals for usleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 296 + order: 311 --- ## `usleep()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/system.rs`:625](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/system.rs#L625) (`lower_usleep`) +- **Signature**: [`src/builtins/system/usleep.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/usleep.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/system.rs`:625](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/system.rs#L625) (`lower_usleep`) - **Function symbol**: `lower_usleep()` @@ -36,4 +36,3 @@ function usleep(int $microseconds): void ## Cross-references - [User reference for `usleep()`](../../../php/builtins/process/usleep.md) - diff --git a/docs/internals/builtins/regex/preg_match.md b/docs/internals/builtins/regex/preg_match.md index 41368a5da4..3ddfb2d52f 100644 --- a/docs/internals/builtins/regex/preg_match.md +++ b/docs/internals/builtins/regex/preg_match.md @@ -2,15 +2,15 @@ title: "preg_match() — internals" description: "Compiler internals for preg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 297 + order: 312 --- ## `preg_match()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/regex.rs`:28](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/regex.rs#L28) (`lower_preg_match`) +- **Signature**: [`src/builtins/system/preg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_match.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/regex.rs`:28](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/regex.rs#L28) (`lower_preg_match`) - **Function symbol**: `lower_preg_match()` @@ -22,12 +22,13 @@ sidebar: The following runtime helpers are referenced: - `__rt_preg_match` +- `__rt_preg_match_all` - `__rt_preg_match_capture` ## Signature summary ```php -function preg_match(string $pattern, string $subject, array $matches): int +function preg_match(string $pattern, string $subject, array $matches = []): int ``` ## What the type checker enforces @@ -38,4 +39,3 @@ function preg_match(string $pattern, string $subject, array $matches): int ## 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 6817788715..95d701c2b3 100644 --- a/docs/internals/builtins/regex/preg_match_all.md +++ b/docs/internals/builtins/regex/preg_match_all.md @@ -2,15 +2,15 @@ title: "preg_match_all() — internals" description: "Compiler internals for preg_match_all(): lowering path, type checks, and runtime helpers." sidebar: - order: 298 + order: 313 --- ## `preg_match_all()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/regex.rs`:52](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/regex.rs#L52) (`lower_preg_match_all`) +- **Signature**: [`src/builtins/system/preg_match_all.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_match_all.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/regex.rs`:49](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/regex.rs#L49) (`lower_preg_match_all`) - **Function symbol**: `lower_preg_match_all()` @@ -22,19 +22,18 @@ sidebar: The following runtime helpers are referenced: - `__rt_preg_match_all` +- `__rt_preg_replace` ## Signature summary ```php -function preg_match_all(string $pattern, string $subject, array $matches): int +function preg_match_all(string $pattern, string $subject): int ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. -- **By-reference parameters**: `$matches`. +- **Arity**: takes exactly 2 arguments. ## 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 205f9d2a82..7b2b8b2a04 100644 --- a/docs/internals/builtins/regex/preg_replace.md +++ b/docs/internals/builtins/regex/preg_replace.md @@ -2,15 +2,15 @@ title: "preg_replace() — internals" description: "Compiler internals for preg_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 299 + order: 314 --- ## `preg_replace()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/regex.rs`:65](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/regex.rs#L65) (`lower_preg_replace`) +- **Signature**: [`src/builtins/system/preg_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_replace.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/regex.rs`:62](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/regex.rs#L62) (`lower_preg_replace`) - **Function symbol**: `lower_preg_replace()` @@ -26,15 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function preg_replace(string $pattern, string $replacement, string $subject, int $limit = -1, int $count = null): string +function preg_replace(string $pattern, string $replacement, string $subject): string ``` ## What the type checker enforces -- **Arity**: takes 3–5 arguments (2 optional). -- **By-reference parameters**: `$count`. +- **Arity**: takes exactly 3 arguments. ## 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 d9bf700beb..32af71799a 100644 --- a/docs/internals/builtins/regex/preg_replace_callback.md +++ b/docs/internals/builtins/regex/preg_replace_callback.md @@ -2,15 +2,15 @@ title: "preg_replace_callback() — internals" description: "Compiler internals for preg_replace_callback(): lowering path, type checks, and runtime helpers." sidebar: - order: 300 + order: 315 --- ## `preg_replace_callback()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/regex.rs`:90](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/regex.rs#L90) (`lower_preg_replace_callback`) +- **Signature**: [`src/builtins/callables/preg_replace_callback.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/preg_replace_callback.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/regex.rs`:84](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/regex.rs#L84) (`lower_preg_replace_callback`) - **Function symbol**: `lower_preg_replace_callback()` @@ -26,15 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function preg_replace_callback(string $pattern, callable $callback, string $subject, int $limit = -1, int $count = null, int $flags = 0): array +function preg_replace_callback(string $pattern, callable $callback, string $subject): string ``` ## What the type checker enforces -- **Arity**: takes 3–6 arguments (3 optional). -- **By-reference parameters**: `$count`. +- **Arity**: takes exactly 3 arguments. ## 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 c5cdbfc66b..d938228255 100644 --- a/docs/internals/builtins/regex/preg_split.md +++ b/docs/internals/builtins/regex/preg_split.md @@ -2,15 +2,15 @@ title: "preg_split() — internals" description: "Compiler internals for preg_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 301 + order: 316 --- ## `preg_split()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/regex.rs`:374](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/regex.rs#L374) (`lower_preg_split`) +- **Signature**: [`src/builtins/system/preg_split.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_split.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/regex.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/regex.rs#L388) (`lower_preg_split`) - **Function symbol**: `lower_preg_split()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function preg_split(string $pattern, string $subject, int $limit, int $flags): array +function preg_split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function preg_split(string $pattern, string $subject, int $limit, int $flags): a ## 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 9fb6274624..af330644c9 100644 --- a/docs/internals/builtins/spl/iterator_apply.md +++ b/docs/internals/builtins/spl/iterator_apply.md @@ -2,15 +2,15 @@ title: "iterator_apply() — internals" description: "Compiler internals for iterator_apply(): lowering path, type checks, and runtime helpers." sidebar: - order: 302 + order: 317 --- ## `iterator_apply()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:289](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L289) (`lower_iterator_apply`) +- **Signature**: [`src/builtins/spl/iterator_apply.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_apply.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:290](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L290) (`lower_iterator_apply`) - **Function symbol**: `lower_iterator_apply()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function iterator_apply(traversable $iterator, callable $callback, array $args): int +function iterator_apply(traversable $iterator, callable $callback, array $args = null): int ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function iterator_apply(traversable $iterator, callable $callback, array $args): ## 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 048229bea5..3369e6d572 100644 --- a/docs/internals/builtins/spl/iterator_count.md +++ b/docs/internals/builtins/spl/iterator_count.md @@ -2,15 +2,15 @@ title: "iterator_count() — internals" description: "Compiler internals for iterator_count(): lowering path, type checks, and runtime helpers." sidebar: - order: 303 + order: 318 --- ## `iterator_count()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:236](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L236) (`lower_iterator_count`) +- **Signature**: [`src/builtins/spl/iterator_count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_count.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:237](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L237) (`lower_iterator_count`) - **Function symbol**: `lower_iterator_count()` @@ -35,4 +35,3 @@ function iterator_count(traversable $iterator): int ## 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 5bce3fea3b..36fb10db33 100644 --- a/docs/internals/builtins/spl/iterator_to_array.md +++ b/docs/internals/builtins/spl/iterator_to_array.md @@ -2,15 +2,15 @@ title: "iterator_to_array() — internals" description: "Compiler internals for iterator_to_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 304 + order: 319 --- ## `iterator_to_array()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:265](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L265) (`lower_iterator_to_array`) +- **Signature**: [`src/builtins/spl/iterator_to_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_to_array.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:266](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L266) (`lower_iterator_to_array`) - **Function symbol**: `lower_iterator_to_array()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function iterator_to_array(traversable $iterator, bool $preserve_keys): array +function iterator_to_array(traversable $iterator, bool $preserve_keys = true): array ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function iterator_to_array(traversable $iterator, bool $preserve_keys): array ## 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 d4f2f00d6b..12b8cfb504 100644 --- a/docs/internals/builtins/spl/spl_autoload.md +++ b/docs/internals/builtins/spl/spl_autoload.md @@ -2,15 +2,15 @@ title: "spl_autoload() — internals" description: "Compiler internals for spl_autoload(): lowering path, type checks, and runtime helpers." sidebar: - order: 305 + order: 320 --- ## `spl_autoload()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:150](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L150) (`lower_spl_autoload_void`) +- **Signature**: [`src/builtins/spl/spl_autoload.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:151](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L151) (`lower_spl_autoload_void`) - **Function symbol**: `lower_spl_autoload_void()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function spl_autoload(string $class, string $file_extensions): void +function spl_autoload(string $class, string $file_extensions = null): void ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function spl_autoload(string $class, string $file_extensions): void ## 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 cf316c669f..2ef338a9b1 100644 --- a/docs/internals/builtins/spl/spl_autoload_call.md +++ b/docs/internals/builtins/spl/spl_autoload_call.md @@ -2,15 +2,15 @@ title: "spl_autoload_call() — internals" description: "Compiler internals for spl_autoload_call(): lowering path, type checks, and runtime helpers." sidebar: - order: 306 + order: 321 --- ## `spl_autoload_call()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:150](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L150) (`lower_spl_autoload_void`) +- **Signature**: [`src/builtins/spl/spl_autoload_call.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_call.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:151](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L151) (`lower_spl_autoload_void`) - **Function symbol**: `lower_spl_autoload_void()` @@ -35,4 +35,3 @@ function spl_autoload_call(string $class): void ## 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 aadca45247..0543d518ab 100644 --- a/docs/internals/builtins/spl/spl_autoload_extensions.md +++ b/docs/internals/builtins/spl/spl_autoload_extensions.md @@ -2,21 +2,21 @@ title: "spl_autoload_extensions() — internals" description: "Compiler internals for spl_autoload_extensions(): lowering path, type checks, and runtime helpers." sidebar: - order: 307 + order: 322 --- ## `spl_autoload_extensions()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:177](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L177) (`lower_spl_autoload_extensions`) +- **Signature**: [`src/builtins/spl/spl_autoload_extensions.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_extensions.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:178](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L178) (`lower_spl_autoload_extensions`) - **Function symbol**: `lower_spl_autoload_extensions()` ### Lowering notes -- Lowers `spl_autoload_extensions()` against the legacy mutable extension globals. +- Lowers `spl_autoload_extensions()` against the mutable extension globals. ## Runtime helpers @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function spl_autoload_extensions(string $file_extensions): string +function spl_autoload_extensions(string $file_extensions = null): string ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function spl_autoload_extensions(string $file_extensions): string ## 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 182d22825f..7292c1ec14 100644 --- a/docs/internals/builtins/spl/spl_autoload_functions.md +++ b/docs/internals/builtins/spl/spl_autoload_functions.md @@ -2,15 +2,15 @@ title: "spl_autoload_functions() — internals" description: "Compiler internals for spl_autoload_functions(): lowering path, type checks, and runtime helpers." sidebar: - order: 308 + order: 323 --- ## `spl_autoload_functions()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:166](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L166) (`lower_spl_autoload_functions`) +- **Signature**: [`src/builtins/spl/spl_autoload_functions.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_functions.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:167](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L167) (`lower_spl_autoload_functions`) - **Function symbol**: `lower_spl_autoload_functions()` @@ -35,4 +35,3 @@ function spl_autoload_functions(): array ## 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 f8af69eb06..82819be366 100644 --- a/docs/internals/builtins/spl/spl_autoload_register.md +++ b/docs/internals/builtins/spl/spl_autoload_register.md @@ -2,15 +2,15 @@ title: "spl_autoload_register() — internals" description: "Compiler internals for spl_autoload_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 309 + order: 324 --- ## `spl_autoload_register()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:134](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L134) (`lower_spl_autoload_bool`) +- **Signature**: [`src/builtins/spl/spl_autoload_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_register.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:135](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L135) (`lower_spl_autoload_bool`) - **Function symbol**: `lower_spl_autoload_bool()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function spl_autoload_register(callable $callback, bool $throw, bool $prepend): bool +function spl_autoload_register(callable $callback = null, bool $throw = true, bool $prepend = false): bool ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function spl_autoload_register(callable $callback, bool $throw, bool $prepend): ## 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 1aa51ff1f4..1acd8c791b 100644 --- a/docs/internals/builtins/spl/spl_autoload_unregister.md +++ b/docs/internals/builtins/spl/spl_autoload_unregister.md @@ -2,15 +2,15 @@ title: "spl_autoload_unregister() — internals" description: "Compiler internals for spl_autoload_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 310 + order: 325 --- ## `spl_autoload_unregister()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:134](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L134) (`lower_spl_autoload_bool`) +- **Signature**: [`src/builtins/spl/spl_autoload_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_unregister.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:135](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L135) (`lower_spl_autoload_bool`) - **Function symbol**: `lower_spl_autoload_bool()` @@ -35,4 +35,3 @@ function spl_autoload_unregister(callable $callback): bool ## 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 47424960ea..0cecb8c6c0 100644 --- a/docs/internals/builtins/spl/spl_classes.md +++ b/docs/internals/builtins/spl/spl_classes.md @@ -2,15 +2,15 @@ title: "spl_classes() — internals" description: "Compiler internals for spl_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 311 + order: 326 --- ## `spl_classes()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:205](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L205) (`lower_spl_classes`) +- **Signature**: [`src/builtins/spl/spl_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_classes.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:206](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L206) (`lower_spl_classes`) - **Function symbol**: `lower_spl_classes()` @@ -36,4 +36,3 @@ function spl_classes(): array ## 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 6996ea3891..352d981b05 100644 --- a/docs/internals/builtins/spl/spl_object_hash.md +++ b/docs/internals/builtins/spl/spl_object_hash.md @@ -2,15 +2,15 @@ title: "spl_object_hash() — internals" description: "Compiler internals for spl_object_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 312 + order: 327 --- ## `spl_object_hash()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:225](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L225) (`lower_spl_object_hash`) +- **Signature**: [`src/builtins/spl/spl_object_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_object_hash.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:226](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L226) (`lower_spl_object_hash`) - **Function symbol**: `lower_spl_object_hash()` @@ -36,4 +36,3 @@ function spl_object_hash(object $object): string ## 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 1fdcb7eab0..c2aa448aaa 100644 --- a/docs/internals/builtins/spl/spl_object_id.md +++ b/docs/internals/builtins/spl/spl_object_id.md @@ -2,15 +2,15 @@ title: "spl_object_id() — internals" description: "Compiler internals for spl_object_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 313 + order: 328 --- ## `spl_object_id()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/spl.rs`:215](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/spl.rs#L215) (`lower_spl_object_id`) +- **Signature**: [`src/builtins/spl/spl_object_id.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_object_id.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/spl.rs`:216](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/spl.rs#L216) (`lower_spl_object_id`) - **Function symbol**: `lower_spl_object_id()` @@ -36,4 +36,3 @@ function spl_object_id(object $object): int ## 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 99e2cb1b22..9b8cc53bce 100644 --- a/docs/internals/builtins/streams/fsockopen.md +++ b/docs/internals/builtins/streams/fsockopen.md @@ -2,18 +2,22 @@ title: "fsockopen() — internals" description: "Compiler internals for fsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 314 + order: 329 --- ## `fsockopen()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/io/fsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fsockopen.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3644](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3644) (`lower_fsockopen`) +- **Function symbol**: `lower_fsockopen()` +### Lowering notes + +- Lowers `fsockopen(host, port, errno?, errstr?, timeout?)`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function fsockopen(string $hostname, int $port, int $error_code, string $error_message, float $timeout): mixed +function fsockopen(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed ``` ## What the type checker enforces @@ -32,4 +36,3 @@ function fsockopen(string $hostname, int $port, int $error_code, string $error_m ## 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 44c2689324..ab93c38766 100644 --- a/docs/internals/builtins/streams/pfsockopen.md +++ b/docs/internals/builtins/streams/pfsockopen.md @@ -2,18 +2,22 @@ title: "pfsockopen() — internals" description: "Compiler internals for pfsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 315 + order: 330 --- ## `pfsockopen()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/io/pfsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pfsockopen.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:3644](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L3644) (`lower_fsockopen`) +- **Function symbol**: `lower_fsockopen()` +### Lowering notes + +- Lowers `fsockopen(host, port, errno?, errstr?, timeout?)`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function pfsockopen(string $hostname, int $port, int $error_code, string $error_message, float $timeout): mixed +function pfsockopen(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed ``` ## What the type checker enforces @@ -32,4 +36,3 @@ function pfsockopen(string $hostname, int $port, int $error_code, string $error_ ## 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 5c0a44fed4..76fd3ee3ce 100644 --- a/docs/internals/builtins/streams/stream_bucket_append.md +++ b/docs/internals/builtins/streams/stream_bucket_append.md @@ -2,18 +2,22 @@ title: "stream_bucket_append() — internals" description: "Compiler internals for stream_bucket_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 316 + order: 331 --- ## `stream_bucket_append()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/io/stream_bucket_append.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_append.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2064](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2064) (`lower_stream_bucket_append_or_prepend`) +- **Function symbol**: `lower_stream_bucket_append_or_prepend()` +### Lowering notes + +- Lowers `stream_bucket_append` and `stream_bucket_prepend` over the `_buckets` array. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function stream_bucket_append(mixed $brigade, mixed $bucket): void ## 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 bf6e2fa63c..cf79876f04 100644 --- a/docs/internals/builtins/streams/stream_bucket_prepend.md +++ b/docs/internals/builtins/streams/stream_bucket_prepend.md @@ -2,18 +2,22 @@ title: "stream_bucket_prepend() — internals" description: "Compiler internals for stream_bucket_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 317 + order: 332 --- ## `stream_bucket_prepend()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/io/stream_bucket_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_prepend.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:2064](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L2064) (`lower_stream_bucket_append_or_prepend`) +- **Function symbol**: `lower_stream_bucket_append_or_prepend()` +### Lowering notes + +- Lowers `stream_bucket_append` and `stream_bucket_prepend` over the `_buckets` array. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function stream_bucket_prepend(mixed $brigade, mixed $bucket): void ## 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 367e9e6694..76ee9847ca 100644 --- a/docs/internals/builtins/streams/stream_filter_append.md +++ b/docs/internals/builtins/streams/stream_filter_append.md @@ -2,18 +2,22 @@ title: "stream_filter_append() — internals" description: "Compiler internals for stream_filter_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 318 + order: 333 --- ## `stream_filter_append()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/io/stream_filter_append.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_append.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1550](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1550) (`lower_stream_filter_attach`) +- **Function symbol**: `lower_stream_filter_attach()` +### Lowering notes + +- Lowers `stream_filter_append` and `stream_filter_prepend`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_filter_append(resource $stream, string $filter_name, int $mode, mixed $params): mixed +function stream_filter_append(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function stream_filter_append(resource $stream, string $filter_name, int $mode, ## 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 9c023e3f5d..0eba5d1e4b 100644 --- a/docs/internals/builtins/streams/stream_filter_prepend.md +++ b/docs/internals/builtins/streams/stream_filter_prepend.md @@ -2,18 +2,22 @@ title: "stream_filter_prepend() — internals" description: "Compiler internals for stream_filter_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 319 + order: 334 --- ## `stream_filter_prepend()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/io/stream_filter_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_prepend.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/io.rs`:1550](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/io.rs#L1550) (`lower_stream_filter_attach`) +- **Function symbol**: `lower_stream_filter_attach()` +### Lowering notes + +- Lowers `stream_filter_append` and `stream_filter_prepend`. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function stream_filter_prepend(resource $stream, string $filter_name, int $mode, mixed $params): mixed +function stream_filter_prepend(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function stream_filter_prepend(resource $stream, string $filter_name, int $mode, ## 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 a7ada21270..690a7b9338 100644 --- a/docs/internals/builtins/string/addslashes.md +++ b/docs/internals/builtins/string/addslashes.md @@ -2,15 +2,15 @@ title: "addslashes() — internals" description: "Compiler internals for addslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 320 + order: 335 --- ## `addslashes()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/addslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/addslashes.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function addslashes(string $string): string ## 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 5f5bc3372e..231a64d493 100644 --- a/docs/internals/builtins/string/base64_decode.md +++ b/docs/internals/builtins/string/base64_decode.md @@ -2,15 +2,15 @@ title: "base64_decode() — internals" description: "Compiler internals for base64_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 321 + order: 336 --- ## `base64_decode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/base64_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/base64_decode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function base64_decode(string $string, bool $strict): string +function base64_decode(string $string): string ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 0e658dd981..f4ff51e4ea 100644 --- a/docs/internals/builtins/string/base64_encode.md +++ b/docs/internals/builtins/string/base64_encode.md @@ -2,15 +2,15 @@ title: "base64_encode() — internals" description: "Compiler internals for base64_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 322 + order: 337 --- ## `base64_encode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/base64_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/base64_encode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function base64_encode(string $string): string ## 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 c1ebe36b2b..46c485805e 100644 --- a/docs/internals/builtins/string/bin2hex.md +++ b/docs/internals/builtins/string/bin2hex.md @@ -2,15 +2,15 @@ title: "bin2hex() — internals" description: "Compiler internals for bin2hex(): lowering path, type checks, and runtime helpers." sidebar: - order: 323 + order: 338 --- ## `bin2hex()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/bin2hex.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/bin2hex.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function bin2hex(string $string): string ## 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 91a762cd0a..555fc89fef 100644 --- a/docs/internals/builtins/string/chop.md +++ b/docs/internals/builtins/string/chop.md @@ -2,18 +2,22 @@ title: "chop() — internals" description: "Compiler internals for chop(): lowering path, type checks, and runtime helpers." sidebar: - order: 324 + order: 339 --- ## `chop()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/string/chop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chop.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:112](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L112) (`lower_trim_like`) +- **Function symbol**: `lower_trim_like()` +### Lowering notes + +- Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function chop(string $string, string $characters): string +function chop(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function chop(string $string, string $characters): string ## 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 886f2f619f..fd5e4b8454 100644 --- a/docs/internals/builtins/string/chr.md +++ b/docs/internals/builtins/string/chr.md @@ -2,15 +2,15 @@ title: "chr() — internals" description: "Compiler internals for chr(): lowering path, type checks, and runtime helpers." sidebar: - order: 325 + order: 340 --- ## `chr()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:858](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L858) (`lower_chr`) +- **Signature**: [`src/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chr.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:858](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L858) (`lower_chr`) - **Function symbol**: `lower_chr()` @@ -36,4 +36,3 @@ function chr(int $codepoint): string ## 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 21d0d2273f..86282c894d 100644 --- a/docs/internals/builtins/string/crc32.md +++ b/docs/internals/builtins/string/crc32.md @@ -2,15 +2,15 @@ title: "crc32() — internals" description: "Compiler internals for crc32(): lowering path, type checks, and runtime helpers." sidebar: - order: 326 + order: 341 --- ## `crc32()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:348](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L348) (`lower_crc32`) +- **Signature**: [`src/builtins/string/crc32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/crc32.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:348](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L348) (`lower_crc32`) - **Function symbol**: `lower_crc32()` @@ -39,4 +39,3 @@ function crc32(string $string): int ## 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 ee1e878736..eea918f14b 100644 --- a/docs/internals/builtins/string/explode.md +++ b/docs/internals/builtins/string/explode.md @@ -2,15 +2,15 @@ title: "explode() — internals" description: "Compiler internals for explode(): lowering path, type checks, and runtime helpers." sidebar: - order: 327 + order: 342 --- ## `explode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:151](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L151) (`lower_explode`) +- **Signature**: [`src/builtins/string/explode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/explode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:151](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L151) (`lower_explode`) - **Function symbol**: `lower_explode()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function explode(string $separator, string $string, int $limit): array +function explode(string $separator, string $string, int $limit = PHP_INT_MAX): array ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function explode(string $separator, string $string, int $limit): array ## 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 9fa28db932..17f141bbda 100644 --- a/docs/internals/builtins/string/grapheme_strrev.md +++ b/docs/internals/builtins/string/grapheme_strrev.md @@ -2,15 +2,15 @@ title: "grapheme_strrev() — internals" description: "Compiler internals for grapheme_strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 328 + order: 343 --- ## `grapheme_strrev()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:88](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L88) (`lower_grapheme_strrev`) +- **Signature**: [`src/builtins/string/grapheme_strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/grapheme_strrev.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:88](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L88) (`lower_grapheme_strrev`) - **Function symbol**: `lower_grapheme_strrev()` @@ -37,4 +37,3 @@ function grapheme_strrev(string $string): mixed ## 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 6f3aa0fbd3..eec09f15dc 100644 --- a/docs/internals/builtins/string/gzcompress.md +++ b/docs/internals/builtins/string/gzcompress.md @@ -2,15 +2,15 @@ title: "gzcompress() — internals" description: "Compiler internals for gzcompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 329 + order: 344 --- ## `gzcompress()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:402](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L402) (`lower_gzcompress`) +- **Signature**: [`src/builtins/string/gzcompress.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzcompress.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:402](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L402) (`lower_gzcompress`) - **Function symbol**: `lower_gzcompress()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function gzcompress(string $data, int $level, int $encoding): string +function gzcompress(string $data, int $level = -1): string ``` ## What the type checker enforces -- **Arity**: takes 2–3 arguments (1 optional). +- **Arity**: takes 1–2 arguments (1 optional). ## 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 a0fc254667..1e062282dd 100644 --- a/docs/internals/builtins/string/gzdeflate.md +++ b/docs/internals/builtins/string/gzdeflate.md @@ -2,15 +2,15 @@ title: "gzdeflate() — internals" description: "Compiler internals for gzdeflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 330 + order: 345 --- ## `gzdeflate()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:418](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L418) (`lower_gzdeflate`) +- **Signature**: [`src/builtins/string/gzdeflate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzdeflate.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:418](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L418) (`lower_gzdeflate`) - **Function symbol**: `lower_gzdeflate()` @@ -25,14 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function gzdeflate(string $data, int $level, int $encoding): string +function gzdeflate(string $data, int $level = -1): string ``` ## What the type checker enforces -- **Arity**: takes 2–3 arguments (1 optional). +- **Arity**: takes 1–2 arguments (1 optional). ## 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 f543f8bad0..0a2695fdec 100644 --- a/docs/internals/builtins/string/gzinflate.md +++ b/docs/internals/builtins/string/gzinflate.md @@ -2,15 +2,15 @@ title: "gzinflate() — internals" description: "Compiler internals for gzinflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 331 + order: 346 --- ## `gzinflate()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:436](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L436) (`lower_gzinflate`) +- **Signature**: [`src/builtins/string/gzinflate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzinflate.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:436](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L436) (`lower_gzinflate`) - **Function symbol**: `lower_gzinflate()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function gzinflate(string $data, int $max_length): string +function gzinflate(string $data, int $max_length = 0): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function gzinflate(string $data, int $max_length): string ## 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 2b30331848..bf4f949e70 100644 --- a/docs/internals/builtins/string/gzuncompress.md +++ b/docs/internals/builtins/string/gzuncompress.md @@ -2,15 +2,15 @@ title: "gzuncompress() — internals" description: "Compiler internals for gzuncompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 332 + order: 347 --- ## `gzuncompress()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:457](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L457) (`lower_gzuncompress`) +- **Signature**: [`src/builtins/string/gzuncompress.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzuncompress.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:457](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L457) (`lower_gzuncompress`) - **Function symbol**: `lower_gzuncompress()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function gzuncompress(string $data, int $max_length): string +function gzuncompress(string $data, int $max_length = 0): mixed ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function gzuncompress(string $data, int $max_length): string ## 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 b9e8a60878..c2ed6097fe 100644 --- a/docs/internals/builtins/string/hash.md +++ b/docs/internals/builtins/string/hash.md @@ -2,15 +2,15 @@ title: "hash() — internals" description: "Compiler internals for hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 333 + order: 348 --- ## `hash()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:209](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L209) (`lower_hash`) +- **Signature**: [`src/builtins/string/hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:209](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L209) (`lower_hash`) - **Function symbol**: `lower_hash()` @@ -26,14 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function hash(string $algo, string $data, bool $binary = false, array $options = []): string +function hash(string $algo, string $data, bool $binary = false): string ``` ## What the type checker enforces -- **Arity**: takes 2–4 arguments (2 optional). +- **Arity**: takes 2–3 arguments (1 optional). ## 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 581755cc25..8a612acaee 100644 --- a/docs/internals/builtins/string/hash_algos.md +++ b/docs/internals/builtins/string/hash_algos.md @@ -2,15 +2,15 @@ title: "hash_algos() — internals" description: "Compiler internals for hash_algos(): lowering path, type checks, and runtime helpers." sidebar: - order: 334 + order: 349 --- ## `hash_algos()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:254](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L254) (`lower_hash_algos`) +- **Signature**: [`src/builtins/string/hash_algos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_algos.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:254](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L254) (`lower_hash_algos`) - **Function symbol**: `lower_hash_algos()` @@ -37,4 +37,3 @@ function hash_algos(): array ## 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 56f648fa37..31dcbe408c 100644 --- a/docs/internals/builtins/string/hash_copy.md +++ b/docs/internals/builtins/string/hash_copy.md @@ -2,15 +2,15 @@ title: "hash_copy() — internals" description: "Compiler internals for hash_copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 335 + order: 350 --- ## `hash_copy()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:333](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L333) (`lower_hash_copy`) +- **Signature**: [`src/builtins/string/hash_copy.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_copy.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:333](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L333) (`lower_hash_copy`) - **Function symbol**: `lower_hash_copy()` @@ -39,4 +39,3 @@ function hash_copy(resource $context): mixed ## 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 b8b682f426..090ffbb03f 100644 --- a/docs/internals/builtins/string/hash_equals.md +++ b/docs/internals/builtins/string/hash_equals.md @@ -2,15 +2,15 @@ title: "hash_equals() — internals" description: "Compiler internals for hash_equals(): lowering path, type checks, and runtime helpers." sidebar: - order: 336 + order: 351 --- ## `hash_equals()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:247](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L247) (`lower_hash_equals`) +- **Signature**: [`src/builtins/string/hash_equals.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_equals.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:247](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L247) (`lower_hash_equals`) - **Function symbol**: `lower_hash_equals()` @@ -38,4 +38,3 @@ function hash_equals(string $known_string, string $user_string): bool ## 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 6e55303c6f..b4ce2847b6 100644 --- a/docs/internals/builtins/string/hash_final.md +++ b/docs/internals/builtins/string/hash_final.md @@ -2,15 +2,15 @@ title: "hash_final() — internals" description: "Compiler internals for hash_final(): lowering path, type checks, and runtime helpers." sidebar: - order: 337 + order: 352 --- ## `hash_final()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:302](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L302) (`lower_hash_final`) +- **Signature**: [`src/builtins/string/hash_final.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_final.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:302](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L302) (`lower_hash_final`) - **Function symbol**: `lower_hash_final()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function hash_final(resource $context, bool $binary): string +function hash_final(resource $context, bool $binary = false): string ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function hash_final(resource $context, bool $binary): string ## 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 0889f92e4b..0b27e37b08 100644 --- a/docs/internals/builtins/string/hash_hmac.md +++ b/docs/internals/builtins/string/hash_hmac.md @@ -2,15 +2,15 @@ title: "hash_hmac() — internals" description: "Compiler internals for hash_hmac(): lowering path, type checks, and runtime helpers." sidebar: - order: 338 + order: 353 --- ## `hash_hmac()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:228](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L228) (`lower_hash_hmac`) +- **Signature**: [`src/builtins/string/hash_hmac.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_hmac.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:228](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L228) (`lower_hash_hmac`) - **Function symbol**: `lower_hash_hmac()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function hash_hmac(string $algo, string $data, string $key, bool $binary): string +function hash_hmac(string $algo, string $data, string $key, bool $binary = false): string ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function hash_hmac(string $algo, string $data, string $key, bool $binary): strin ## 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 502d3eb97b..db7ba10da8 100644 --- a/docs/internals/builtins/string/hash_init.md +++ b/docs/internals/builtins/string/hash_init.md @@ -2,15 +2,15 @@ title: "hash_init() — internals" description: "Compiler internals for hash_init(): lowering path, type checks, and runtime helpers." sidebar: - order: 339 + order: 354 --- ## `hash_init()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:266](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L266) (`lower_hash_init`) +- **Signature**: [`src/builtins/string/hash_init.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_init.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:266](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L266) (`lower_hash_init`) - **Function symbol**: `lower_hash_init()` @@ -26,14 +26,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function hash_init(string $algo, int $flags = 0, string $key = '', array $options = []): mixed +function hash_init(string $algo, int $flags = 0, string $key = ''): mixed ``` ## What the type checker enforces -- **Arity**: takes 1–4 arguments (3 optional). +- **Arity**: takes 1–3 arguments (2 optional). ## 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 d690762753..979c991b56 100644 --- a/docs/internals/builtins/string/hash_update.md +++ b/docs/internals/builtins/string/hash_update.md @@ -2,15 +2,15 @@ title: "hash_update() — internals" description: "Compiler internals for hash_update(): lowering path, type checks, and runtime helpers." sidebar: - order: 340 + order: 355 --- ## `hash_update()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:277](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L277) (`lower_hash_update`) +- **Signature**: [`src/builtins/string/hash_update.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_update.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:277](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L277) (`lower_hash_update`) - **Function symbol**: `lower_hash_update()` @@ -36,4 +36,3 @@ function hash_update(resource $context, string $data): bool ## 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 952cf150fb..ddcd104988 100644 --- a/docs/internals/builtins/string/hex2bin.md +++ b/docs/internals/builtins/string/hex2bin.md @@ -2,15 +2,15 @@ title: "hex2bin() — internals" description: "Compiler internals for hex2bin(): lowering path, type checks, and runtime helpers." sidebar: - order: 341 + order: 356 --- ## `hex2bin()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/hex2bin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hex2bin.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function hex2bin(string $string): string ## 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 cfe59af056..89a7f64f26 100644 --- a/docs/internals/builtins/string/html_entity_decode.md +++ b/docs/internals/builtins/string/html_entity_decode.md @@ -2,15 +2,15 @@ title: "html_entity_decode() — internals" description: "Compiler internals for html_entity_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 342 + order: 357 --- ## `html_entity_decode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/html_entity_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/html_entity_decode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function html_entity_decode(string $string, int $flags, string $encoding): string +function html_entity_decode(string $string): string ``` ## What the type checker enforces -- **Arity**: takes exactly 3 arguments. +- **Arity**: takes exactly 1 argument. ## 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 9d19ae0798..4e0cd701ff 100644 --- a/docs/internals/builtins/string/htmlentities.md +++ b/docs/internals/builtins/string/htmlentities.md @@ -2,15 +2,15 @@ title: "htmlentities() — internals" description: "Compiler internals for htmlentities(): lowering path, type checks, and runtime helpers." sidebar: - order: 343 + order: 358 --- ## `htmlentities()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/htmlentities.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/htmlentities.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function htmlentities(string $string, int $flags, string $encoding, bool $double_encode): string +function htmlentities(string $string): string ``` ## What the type checker enforces -- **Arity**: takes exactly 4 arguments. +- **Arity**: takes exactly 1 argument. ## 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 44f63bf6b8..b93173302b 100644 --- a/docs/internals/builtins/string/htmlspecialchars.md +++ b/docs/internals/builtins/string/htmlspecialchars.md @@ -2,15 +2,15 @@ title: "htmlspecialchars() — internals" description: "Compiler internals for htmlspecialchars(): lowering path, type checks, and runtime helpers." sidebar: - order: 344 + order: 359 --- ## `htmlspecialchars()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/htmlspecialchars.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/htmlspecialchars.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function htmlspecialchars(string $string, int $flags, string $encoding, bool $double_encode): string +function htmlspecialchars(string $string): string ``` ## What the type checker enforces -- **Arity**: takes exactly 4 arguments. +- **Arity**: takes exactly 1 argument. ## 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 1dd74500fa..07148e9201 100644 --- a/docs/internals/builtins/string/implode.md +++ b/docs/internals/builtins/string/implode.md @@ -2,15 +2,15 @@ title: "implode() — internals" description: "Compiler internals for implode(): lowering path, type checks, and runtime helpers." sidebar: - order: 345 + order: 360 --- ## `implode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:192](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L192) (`lower_implode`) +- **Signature**: [`src/builtins/string/implode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/implode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:192](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L192) (`lower_implode`) - **Function symbol**: `lower_implode()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function implode(string $separator, array $array): string +function implode(string $separator, array $array = null): string ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function implode(string $separator, array $array): string ## 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 f7dba19d13..06ac904870 100644 --- a/docs/internals/builtins/string/inet_ntop.md +++ b/docs/internals/builtins/string/inet_ntop.md @@ -2,15 +2,15 @@ title: "inet_ntop() — internals" description: "Compiler internals for inet_ntop(): lowering path, type checks, and runtime helpers." sidebar: - order: 346 + order: 361 --- ## `inet_ntop()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:497](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L497) (`lower_inet`) +- **Signature**: [`src/builtins/string/inet_ntop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/inet_ntop.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:497](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L497) (`lower_inet`) - **Function symbol**: `lower_inet()` @@ -36,4 +36,3 @@ function inet_ntop(string $ip): mixed ## 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 eaa52a5f8e..b234f68d13 100644 --- a/docs/internals/builtins/string/inet_pton.md +++ b/docs/internals/builtins/string/inet_pton.md @@ -2,15 +2,15 @@ title: "inet_pton() — internals" description: "Compiler internals for inet_pton(): lowering path, type checks, and runtime helpers." sidebar: - order: 347 + order: 362 --- ## `inet_pton()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:497](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L497) (`lower_inet`) +- **Signature**: [`src/builtins/string/inet_pton.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/inet_pton.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:497](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L497) (`lower_inet`) - **Function symbol**: `lower_inet()` @@ -36,4 +36,3 @@ function inet_pton(string $ip): mixed ## 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 20c8c624e6..90f6b900b9 100644 --- a/docs/internals/builtins/string/ip2long.md +++ b/docs/internals/builtins/string/ip2long.md @@ -2,15 +2,15 @@ title: "ip2long() — internals" description: "Compiler internals for ip2long(): lowering path, type checks, and runtime helpers." sidebar: - order: 348 + order: 363 --- ## `ip2long()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:488](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L488) (`lower_ip2long`) +- **Signature**: [`src/builtins/string/ip2long.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ip2long.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:488](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L488) (`lower_ip2long`) - **Function symbol**: `lower_ip2long()` @@ -37,4 +37,3 @@ function ip2long(string $ip): mixed ## 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 dda4a914e0..90a115bc7d 100644 --- a/docs/internals/builtins/string/lcfirst.md +++ b/docs/internals/builtins/string/lcfirst.md @@ -2,15 +2,15 @@ title: "lcfirst() — internals" description: "Compiler internals for lcfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 349 + order: 364 --- ## `lcfirst()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:104](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L104) (`lower_lcfirst`) +- **Signature**: [`src/builtins/string/lcfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/lcfirst.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:104](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L104) (`lower_lcfirst`) - **Function symbol**: `lower_lcfirst()` @@ -36,4 +36,3 @@ function lcfirst(string $string): string ## 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 69030b1718..0e104cd876 100644 --- a/docs/internals/builtins/string/long2ip.md +++ b/docs/internals/builtins/string/long2ip.md @@ -2,15 +2,15 @@ title: "long2ip() — internals" description: "Compiler internals for long2ip(): lowering path, type checks, and runtime helpers." sidebar: - order: 350 + order: 365 --- ## `long2ip()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:476](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L476) (`lower_long2ip`) +- **Signature**: [`src/builtins/string/long2ip.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/long2ip.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:476](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L476) (`lower_long2ip`) - **Function symbol**: `lower_long2ip()` @@ -37,4 +37,3 @@ function long2ip(int $ip): string ## 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 ef7d971ac8..36b3a638e8 100644 --- a/docs/internals/builtins/string/ltrim.md +++ b/docs/internals/builtins/string/ltrim.md @@ -2,15 +2,15 @@ title: "ltrim() — internals" description: "Compiler internals for ltrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 351 + order: 366 --- ## `ltrim()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:112](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L112) (`lower_trim_like`) +- **Signature**: [`src/builtins/string/ltrim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ltrim.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:112](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L112) (`lower_trim_like`) - **Function symbol**: `lower_trim_like()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function ltrim(string $string, string $characters): string +function ltrim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function ltrim(string $string, string $characters): string ## 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 03d42d867c..d5e038a304 100644 --- a/docs/internals/builtins/string/md5.md +++ b/docs/internals/builtins/string/md5.md @@ -2,15 +2,15 @@ title: "md5() — internals" description: "Compiler internals for md5(): lowering path, type checks, and runtime helpers." sidebar: - order: 352 + order: 367 --- ## `md5()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:355](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L355) (`lower_md5`) +- **Signature**: [`src/builtins/string/md5.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/md5.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:355](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L355) (`lower_md5`) - **Function symbol**: `lower_md5()` @@ -28,7 +28,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function md5(string $string, bool $binary): string +function md5(string $string, bool $binary = false): string ``` ## What the type checker enforces @@ -38,4 +38,3 @@ function md5(string $string, bool $binary): string ## 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 0ca2ac3b4f..297298b345 100644 --- a/docs/internals/builtins/string/nl2br.md +++ b/docs/internals/builtins/string/nl2br.md @@ -2,15 +2,15 @@ title: "nl2br() — internals" description: "Compiler internals for nl2br(): lowering path, type checks, and runtime helpers." sidebar: - order: 353 + order: 368 --- ## `nl2br()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/nl2br.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/nl2br.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function nl2br(string $string, bool $use_xhtml): string +function nl2br(string $string): string ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 c0edd06463..25bda575da 100644 --- a/docs/internals/builtins/string/number_format.md +++ b/docs/internals/builtins/string/number_format.md @@ -2,15 +2,15 @@ title: "number_format() — internals" description: "Compiler internals for number_format(): lowering path, type checks, and runtime helpers." sidebar: - order: 354 + order: 369 --- ## `number_format()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:875](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L875) (`lower_number_format`) +- **Signature**: [`src/builtins/string/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/number_format.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:875](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L875) (`lower_number_format`) - **Function symbol**: `lower_number_format()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function number_format(float $num, int $decimals, string $decimal_separator, string $thousands_separator): string +function number_format(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function number_format(float $num, int $decimals, string $decimal_separator, str ## 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 ed53e5e680..6f748d42d5 100644 --- a/docs/internals/builtins/string/ord.md +++ b/docs/internals/builtins/string/ord.md @@ -2,15 +2,15 @@ title: "ord() — internals" description: "Compiler internals for ord(): lowering path, type checks, and runtime helpers." sidebar: - order: 355 + order: 370 --- ## `ord()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:834](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L834) (`lower_ord`) +- **Signature**: [`src/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ord.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:834](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L834) (`lower_ord`) - **Function symbol**: `lower_ord()` @@ -35,4 +35,3 @@ function ord(string $character): int ## 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 c51beb26be..c132d37790 100644 --- a/docs/internals/builtins/string/printf.md +++ b/docs/internals/builtins/string/printf.md @@ -2,15 +2,15 @@ title: "printf() — internals" description: "Compiler internals for printf(): lowering path, type checks, and runtime helpers." sidebar: - order: 356 + order: 371 --- ## `printf()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:517](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L517) (`lower_printf`) +- **Signature**: [`src/builtins/string/printf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/printf.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:517](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L517) (`lower_printf`) - **Function symbol**: `lower_printf()` @@ -37,4 +37,3 @@ function printf(string $format, ...$values): int ## 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 e479c24987..8d1743e949 100644 --- a/docs/internals/builtins/string/rawurldecode.md +++ b/docs/internals/builtins/string/rawurldecode.md @@ -2,15 +2,15 @@ title: "rawurldecode() — internals" description: "Compiler internals for rawurldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 357 + order: 372 --- ## `rawurldecode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/rawurldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rawurldecode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function rawurldecode(string $string): string ## 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 62a8252a0f..63c5abfc9e 100644 --- a/docs/internals/builtins/string/rawurlencode.md +++ b/docs/internals/builtins/string/rawurlencode.md @@ -2,15 +2,15 @@ title: "rawurlencode() — internals" description: "Compiler internals for rawurlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 358 + order: 373 --- ## `rawurlencode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/rawurlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rawurlencode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function rawurlencode(string $string): string ## 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 eb9efd109b..bab7799890 100644 --- a/docs/internals/builtins/string/rtrim.md +++ b/docs/internals/builtins/string/rtrim.md @@ -2,18 +2,22 @@ title: "rtrim() — internals" description: "Compiler internals for rtrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 359 + order: 374 --- ## `rtrim()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/string/rtrim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rtrim.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:112](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L112) (`lower_trim_like`) +- **Function symbol**: `lower_trim_like()` +### Lowering notes + +- Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function rtrim(string $string, string $characters): string +function rtrim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string ``` ## What the type checker enforces @@ -31,4 +35,3 @@ function rtrim(string $string, string $characters): string ## 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 368ec729fc..7e809a1990 100644 --- a/docs/internals/builtins/string/sha1.md +++ b/docs/internals/builtins/string/sha1.md @@ -2,15 +2,15 @@ title: "sha1() — internals" description: "Compiler internals for sha1(): lowering path, type checks, and runtime helpers." sidebar: - order: 360 + order: 375 --- ## `sha1()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:360](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L360) (`lower_sha1`) +- **Signature**: [`src/builtins/string/sha1.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sha1.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:360](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L360) (`lower_sha1`) - **Function symbol**: `lower_sha1()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function sha1(string $string, bool $binary): string +function sha1(string $string, bool $binary = false): string ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function sha1(string $string, bool $binary): string ## 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 2a2dee4b54..b66c8e5ff0 100644 --- a/docs/internals/builtins/string/sprintf.md +++ b/docs/internals/builtins/string/sprintf.md @@ -2,15 +2,15 @@ title: "sprintf() — internals" description: "Compiler internals for sprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 361 + order: 376 --- ## `sprintf()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:511](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L511) (`lower_sprintf`) +- **Signature**: [`src/builtins/string/sprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sprintf.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:511](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L511) (`lower_sprintf`) - **Function symbol**: `lower_sprintf()` @@ -37,4 +37,3 @@ function sprintf(string $format, ...$values): string ## 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 a91af74076..7aec227847 100644 --- a/docs/internals/builtins/string/sscanf.md +++ b/docs/internals/builtins/string/sscanf.md @@ -2,15 +2,15 @@ title: "sscanf() — internals" description: "Compiler internals for sscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 362 + order: 377 --- ## `sscanf()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:163](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L163) (`lower_sscanf`) +- **Signature**: [`src/builtins/string/sscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sscanf.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:163](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L163) (`lower_sscanf`) - **Function symbol**: `lower_sscanf()` @@ -38,4 +38,3 @@ function sscanf(string $string, string $format, ...$vars): array ## 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 e2843efbfc..bfadc6a13f 100644 --- a/docs/internals/builtins/string/str_contains.md +++ b/docs/internals/builtins/string/str_contains.md @@ -2,15 +2,15 @@ title: "str_contains() — internals" description: "Compiler internals for str_contains(): lowering path, type checks, and runtime helpers." sidebar: - order: 363 + order: 378 --- ## `str_contains()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:682](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L682) (`lower_str_contains`) +- **Signature**: [`src/builtins/string/str_contains.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_contains.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:682](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L682) (`lower_str_contains`) - **Function symbol**: `lower_str_contains()` @@ -36,4 +36,3 @@ function str_contains(string $haystack, string $needle): bool ## 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 789deb4d81..fc8a7c8af4 100644 --- a/docs/internals/builtins/string/str_ends_with.md +++ b/docs/internals/builtins/string/str_ends_with.md @@ -2,15 +2,15 @@ title: "str_ends_with() — internals" description: "Compiler internals for str_ends_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 364 + order: 379 --- ## `str_ends_with()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:139](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L139) (`lower_binary_string_runtime`) +- **Signature**: [`src/builtins/string/str_ends_with.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_ends_with.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:139](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L139) (`lower_binary_string_runtime`) - **Function symbol**: `lower_binary_string_runtime()` @@ -36,4 +36,3 @@ function str_ends_with(string $haystack, string $needle): bool ## 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 f04ceecfe7..6986a05e28 100644 --- a/docs/internals/builtins/string/str_ireplace.md +++ b/docs/internals/builtins/string/str_ireplace.md @@ -2,18 +2,22 @@ title: "str_ireplace() — internals" description: "Compiler internals for str_ireplace(): lowering path, type checks, and runtime helpers." sidebar: - order: 365 + order: 380 --- ## `str_ireplace()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_ireplace.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:780](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L780) (`lower_string_replace`) +- **Function symbol**: `lower_string_replace()` +### Lowering notes + +- Lowers `str_replace()`/`str_ireplace()` with three string operands. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -21,15 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function str_ireplace(mixed $search, mixed $replace, mixed $subject, int $count): mixed +function str_ireplace(string $search, string $replace, string $subject, int $count = null): string ``` ## What the type checker enforces - **Arity**: takes 3–4 arguments (1 optional). -- **By-reference parameters**: `$count`. ## 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 1cfd662aaf..8cf5dd497d 100644 --- a/docs/internals/builtins/string/str_pad.md +++ b/docs/internals/builtins/string/str_pad.md @@ -2,15 +2,15 @@ title: "str_pad() — internals" description: "Compiler internals for str_pad(): lowering path, type checks, and runtime helpers." sidebar: - order: 366 + order: 381 --- ## `str_pad()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:818](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L818) (`lower_str_pad`) +- **Signature**: [`src/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_pad.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:818](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L818) (`lower_str_pad`) - **Function symbol**: `lower_str_pad()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function str_pad(string $string, int $length, string $pad_string, int $pad_type): string +function str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function str_pad(string $string, int $length, string $pad_string, int $pad_type) ## 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 71b2ea8ce1..89526a8a7c 100644 --- a/docs/internals/builtins/string/str_repeat.md +++ b/docs/internals/builtins/string/str_repeat.md @@ -2,15 +2,15 @@ title: "str_repeat() — internals" description: "Compiler internals for str_repeat(): lowering path, type checks, and runtime helpers." sidebar: - order: 367 + order: 382 --- ## `str_repeat()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:746](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L746) (`lower_str_repeat`) +- **Signature**: [`src/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_repeat.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:746](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L746) (`lower_str_repeat`) - **Function symbol**: `lower_str_repeat()` @@ -36,4 +36,3 @@ function str_repeat(string $string, int $times): string ## 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 1ed9805908..88ae2a2e33 100644 --- a/docs/internals/builtins/string/str_replace.md +++ b/docs/internals/builtins/string/str_replace.md @@ -2,15 +2,15 @@ title: "str_replace() — internals" description: "Compiler internals for str_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 368 + order: 383 --- ## `str_replace()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:780](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L780) (`lower_string_replace`) +- **Signature**: [`src/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_replace.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:780](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L780) (`lower_string_replace`) - **Function symbol**: `lower_string_replace()` @@ -25,15 +25,13 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function str_replace(string $search, string $replace, string $subject, int $count): mixed +function str_replace(string $search, string $replace, string $subject, int $count = null): string ``` ## What the type checker enforces - **Arity**: takes 3–4 arguments (1 optional). -- **By-reference parameters**: `$count`. ## 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 a57daf8485..2eeb07c3a2 100644 --- a/docs/internals/builtins/string/str_split.md +++ b/docs/internals/builtins/string/str_split.md @@ -2,15 +2,15 @@ title: "str_split() — internals" description: "Compiler internals for str_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 369 + order: 384 --- ## `str_split()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:176](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L176) (`lower_str_split`) +- **Signature**: [`src/builtins/string/str_split.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_split.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:176](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L176) (`lower_str_split`) - **Function symbol**: `lower_str_split()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function str_split(string $string, int $length): array +function str_split(string $string, int $length = 1): array ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function str_split(string $string, int $length): array ## 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 f20f94185f..1b0164c51d 100644 --- a/docs/internals/builtins/string/str_starts_with.md +++ b/docs/internals/builtins/string/str_starts_with.md @@ -2,15 +2,15 @@ title: "str_starts_with() — internals" description: "Compiler internals for str_starts_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 370 + order: 385 --- ## `str_starts_with()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:139](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L139) (`lower_binary_string_runtime`) +- **Signature**: [`src/builtins/string/str_starts_with.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_starts_with.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:139](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L139) (`lower_binary_string_runtime`) - **Function symbol**: `lower_binary_string_runtime()` @@ -36,4 +36,3 @@ function str_starts_with(string $haystack, string $needle): bool ## 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 f79ad7cbbe..b5fdbd33bf 100644 --- a/docs/internals/builtins/string/strcasecmp.md +++ b/docs/internals/builtins/string/strcasecmp.md @@ -2,21 +2,26 @@ title: "strcasecmp() — internals" description: "Compiler internals for strcasecmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 371 + order: 386 --- ## `strcasecmp()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **Signature**: [`src/builtins/string/strcasecmp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strcasecmp.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:139](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L139) (`lower_binary_string_runtime`) +- **Function symbol**: `lower_binary_string_runtime()` +### Lowering notes + +- Lowers a two-argument string builtin that directly delegates to a runtime helper. + ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_explode` ## Signature summary @@ -31,4 +36,3 @@ function strcasecmp(string $string1, string $string2): int ## 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 3cf3f19e66..280ae03df5 100644 --- a/docs/internals/builtins/string/strcmp.md +++ b/docs/internals/builtins/string/strcmp.md @@ -2,15 +2,15 @@ title: "strcmp() — internals" description: "Compiler internals for strcmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 372 + order: 387 --- ## `strcmp()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:139](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L139) (`lower_binary_string_runtime`) +- **Signature**: [`src/builtins/string/strcmp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strcmp.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:139](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L139) (`lower_binary_string_runtime`) - **Function symbol**: `lower_binary_string_runtime()` @@ -36,4 +36,3 @@ function strcmp(string $string1, string $string2): int ## 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 c261edb6e5..55a40a0cc3 100644 --- a/docs/internals/builtins/string/stripslashes.md +++ b/docs/internals/builtins/string/stripslashes.md @@ -2,15 +2,15 @@ title: "stripslashes() — internals" description: "Compiler internals for stripslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 373 + order: 388 --- ## `stripslashes()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/stripslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/stripslashes.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function stripslashes(string $string): string ## 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 688a27f92c..cd37f070bc 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -2,15 +2,15 @@ title: "strlen() — internals" description: "Compiler internals for strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 374 + order: 389 --- ## `strlen()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:971](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L971) (`lower_strlen`) +- **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`:493](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L493) (`lower_strlen`) - **Function symbol**: `lower_strlen()` @@ -36,4 +36,3 @@ function strlen(string $string): int ## 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 a4a902618a..b8f17df65f 100644 --- a/docs/internals/builtins/string/strpos.md +++ b/docs/internals/builtins/string/strpos.md @@ -2,15 +2,15 @@ title: "strpos() — internals" description: "Compiler internals for strpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 375 + order: 390 --- ## `strpos()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:700](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L700) (`lower_string_position`) +- **Signature**: [`src/builtins/string/strpos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strpos.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:700](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L700) (`lower_string_position`) - **Function symbol**: `lower_string_position()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function strpos(string $haystack, string $needle, int $offset): mixed +function strpos(string $haystack, string $needle, int $offset = 0): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function strpos(string $haystack, string $needle, int $offset): mixed ## 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 1af9a48b10..584b6b8adf 100644 --- a/docs/internals/builtins/string/strrev.md +++ b/docs/internals/builtins/string/strrev.md @@ -2,15 +2,15 @@ title: "strrev() — internals" description: "Compiler internals for strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 376 + order: 391 --- ## `strrev()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strrev.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function strrev(string $string): string ## 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 ae0372563e..dae2268cce 100644 --- a/docs/internals/builtins/string/strrpos.md +++ b/docs/internals/builtins/string/strrpos.md @@ -2,15 +2,15 @@ title: "strrpos() — internals" description: "Compiler internals for strrpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 377 + order: 392 --- ## `strrpos()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:700](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L700) (`lower_string_position`) +- **Signature**: [`src/builtins/string/strrpos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strrpos.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:700](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L700) (`lower_string_position`) - **Function symbol**: `lower_string_position()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function strrpos(string $haystack, string $needle, int $offset): mixed +function strrpos(string $haystack, string $needle, int $offset = 0): mixed ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function strrpos(string $haystack, string $needle, int $offset): mixed ## 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 2fe73e65f4..3b807047a0 100644 --- a/docs/internals/builtins/string/strstr.md +++ b/docs/internals/builtins/string/strstr.md @@ -2,15 +2,15 @@ title: "strstr() — internals" description: "Compiler internals for strstr(): lowering path, type checks, and runtime helpers." sidebar: - order: 378 + order: 393 --- ## `strstr()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:762](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L762) (`lower_strstr`) +- **Signature**: [`src/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strstr.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:762](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L762) (`lower_strstr`) - **Function symbol**: `lower_strstr()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function strstr(string $haystack, string $needle, bool $before_needle): string +function strstr(string $haystack, string $needle, bool $before_needle = false): string ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function strstr(string $haystack, string $needle, bool $before_needle): string ## 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 e6bc81c16f..4d0d7ecebd 100644 --- a/docs/internals/builtins/string/strtolower.md +++ b/docs/internals/builtins/string/strtolower.md @@ -2,15 +2,15 @@ title: "strtolower() — internals" description: "Compiler internals for strtolower(): lowering path, type checks, and runtime helpers." sidebar: - order: 379 + order: 394 --- ## `strtolower()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/strtolower.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strtolower.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function strtolower(string $string): string ## 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 ece1e909dd..89fae1815a 100644 --- a/docs/internals/builtins/string/strtoupper.md +++ b/docs/internals/builtins/string/strtoupper.md @@ -2,15 +2,15 @@ title: "strtoupper() — internals" description: "Compiler internals for strtoupper(): lowering path, type checks, and runtime helpers." sidebar: - order: 380 + order: 395 --- ## `strtoupper()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/strtoupper.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strtoupper.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function strtoupper(string $string): string ## 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 baa7b3204c..e1e389729e 100644 --- a/docs/internals/builtins/string/substr.md +++ b/docs/internals/builtins/string/substr.md @@ -2,15 +2,15 @@ title: "substr() — internals" description: "Compiler internals for substr(): lowering path, type checks, and runtime helpers." sidebar: - order: 381 + order: 396 --- ## `substr()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:713](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L713) (`lower_substr`) +- **Signature**: [`src/builtins/string/substr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:713](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L713) (`lower_substr`) - **Function symbol**: `lower_substr()` @@ -26,7 +26,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function substr(string $string, int $offset, int $length): string +function substr(string $string, int $offset, int $length = null): string ``` ## What the type checker enforces @@ -36,4 +36,3 @@ function substr(string $string, int $offset, int $length): string ## 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 876a69dd32..4a12ebc291 100644 --- a/docs/internals/builtins/string/substr_replace.md +++ b/docs/internals/builtins/string/substr_replace.md @@ -2,15 +2,15 @@ title: "substr_replace() — internals" description: "Compiler internals for substr_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 382 + order: 397 --- ## `substr_replace()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:730](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L730) (`lower_substr_replace`) +- **Signature**: [`src/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr_replace.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:730](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L730) (`lower_substr_replace`) - **Function symbol**: `lower_substr_replace()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function substr_replace(string $string, string $replace, int $offset, int $length): string +function substr_replace(string $string, string $replace, int $offset, int $length = null): string ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function substr_replace(string $string, string $replace, int $offset, int $lengt ## 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 826d2a50bc..3f0d39136f 100644 --- a/docs/internals/builtins/string/trim.md +++ b/docs/internals/builtins/string/trim.md @@ -2,15 +2,15 @@ title: "trim() — internals" description: "Compiler internals for trim(): lowering path, type checks, and runtime helpers." sidebar: - order: 383 + order: 398 --- ## `trim()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:112](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L112) (`lower_trim_like`) +- **Signature**: [`src/builtins/string/trim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/trim.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:112](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L112) (`lower_trim_like`) - **Function symbol**: `lower_trim_like()` @@ -25,7 +25,7 @@ _No direct `__rt_*` helpers captured — the lowering is inlined or routes throu ## Signature summary ```php -function trim(string $string, string $characters): string +function trim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string ``` ## What the type checker enforces @@ -35,4 +35,3 @@ function trim(string $string, string $characters): string ## 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 8a27dfb2d9..af5cea25a6 100644 --- a/docs/internals/builtins/string/ucfirst.md +++ b/docs/internals/builtins/string/ucfirst.md @@ -2,15 +2,15 @@ title: "ucfirst() — internals" description: "Compiler internals for ucfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 384 + order: 399 --- ## `ucfirst()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:96](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L96) (`lower_ucfirst`) +- **Signature**: [`src/builtins/string/ucfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ucfirst.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:96](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L96) (`lower_ucfirst`) - **Function symbol**: `lower_ucfirst()` @@ -36,4 +36,3 @@ function ucfirst(string $string): string ## 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 637986b82f..c88ddec735 100644 --- a/docs/internals/builtins/string/ucwords.md +++ b/docs/internals/builtins/string/ucwords.md @@ -2,15 +2,15 @@ title: "ucwords() — internals" description: "Compiler internals for ucwords(): lowering path, type checks, and runtime helpers." sidebar: - order: 385 + order: 400 --- ## `ucwords()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/ucwords.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ucwords.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function ucwords(string $string, string $separators): string +function ucwords(string $string, string $separators = ' \t\r\n\x0c\x0b'): string ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function ucwords(string $string, string $separators): string ## 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 d28fd70d37..83364251b5 100644 --- a/docs/internals/builtins/string/urldecode.md +++ b/docs/internals/builtins/string/urldecode.md @@ -2,15 +2,15 @@ title: "urldecode() — internals" description: "Compiler internals for urldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 386 + order: 401 --- ## `urldecode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/urldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/urldecode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function urldecode(string $string): string ## 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 11b6df1f6f..cda48a6b38 100644 --- a/docs/internals/builtins/string/urlencode.md +++ b/docs/internals/builtins/string/urlencode.md @@ -2,15 +2,15 @@ title: "urlencode() — internals" description: "Compiler internals for urlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 387 + order: 402 --- ## `urlencode()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) +- **Signature**: [`src/builtins/string/urlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/urlencode.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:76](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L76) (`lower_unary_string_runtime`) - **Function symbol**: `lower_unary_string_runtime()` @@ -37,4 +37,3 @@ function urlencode(string $string): string ## 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 349aeb3a52..30b3ad65ab 100644 --- a/docs/internals/builtins/string/vprintf.md +++ b/docs/internals/builtins/string/vprintf.md @@ -2,15 +2,15 @@ title: "vprintf() — internals" description: "Compiler internals for vprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 388 + order: 403 --- ## `vprintf()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:530](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L530) (`lower_vprintf`) +- **Signature**: [`src/builtins/string/vprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/vprintf.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:530](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L530) (`lower_vprintf`) - **Function symbol**: `lower_vprintf()` @@ -36,4 +36,3 @@ function vprintf(string $format, array $values): int ## 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 7d74f946b0..c63db8bedf 100644 --- a/docs/internals/builtins/string/vsprintf.md +++ b/docs/internals/builtins/string/vsprintf.md @@ -2,15 +2,15 @@ title: "vsprintf() — internals" description: "Compiler internals for vsprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 389 + order: 404 --- ## `vsprintf()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:524](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L524) (`lower_vsprintf`) +- **Signature**: [`src/builtins/string/vsprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/vsprintf.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:524](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L524) (`lower_vsprintf`) - **Function symbol**: `lower_vsprintf()` @@ -36,4 +36,3 @@ function vsprintf(string $format, array $values): string ## 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 627dab0014..127c959e39 100644 --- a/docs/internals/builtins/string/wordwrap.md +++ b/docs/internals/builtins/string/wordwrap.md @@ -2,15 +2,15 @@ title: "wordwrap() — internals" description: "Compiler internals for wordwrap(): lowering path, type checks, and runtime helpers." sidebar: - order: 390 + order: 405 --- ## `wordwrap()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/strings.rs`:802](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/strings.rs#L802) (`lower_wordwrap`) +- **Signature**: [`src/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/wordwrap.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/strings.rs`:802](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/strings.rs#L802) (`lower_wordwrap`) - **Function symbol**: `lower_wordwrap()` @@ -27,7 +27,7 @@ The following runtime helpers are referenced: ## Signature summary ```php -function wordwrap(string $string, int $width, string $break, bool $cut_long_words): string +function wordwrap(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string ``` ## What the type checker enforces @@ -37,4 +37,3 @@ function wordwrap(string $string, int $width, string $break, bool $cut_long_word ## 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 ad8e381159..34c4221dc1 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -2,15 +2,15 @@ title: "boolval() — internals" description: "Compiler internals for boolval(): lowering path, type checks, and runtime helpers." sidebar: - order: 391 + order: 406 --- ## `boolval()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:1064](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L1064) (`lower_boolval`) +- **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`:586](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L586) (`lower_boolval`) - **Function symbol**: `lower_boolval()` @@ -35,4 +35,3 @@ function boolval(mixed $value): bool ## 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 35882f881d..988bd80b55 100644 --- a/docs/internals/builtins/type/ctype_alnum.md +++ b/docs/internals/builtins/type/ctype_alnum.md @@ -2,15 +2,15 @@ title: "ctype_alnum() — internals" description: "Compiler internals for ctype_alnum(): lowering path, type checks, and runtime helpers." sidebar: - order: 392 + order: 407 --- ## `ctype_alnum()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/ctype.rs`:30](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/ctype.rs#L30) (`lower_ctype_alnum`) +- **Signature**: [`src/builtins/string/ctype_alnum.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_alnum.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/ctype.rs`:30](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/ctype.rs#L30) (`lower_ctype_alnum`) - **Function symbol**: `lower_ctype_alnum()` @@ -35,4 +35,3 @@ function ctype_alnum(string $text): bool ## 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 db33a05b3e..4586834d6d 100644 --- a/docs/internals/builtins/type/ctype_alpha.md +++ b/docs/internals/builtins/type/ctype_alpha.md @@ -2,15 +2,15 @@ title: "ctype_alpha() — internals" description: "Compiler internals for ctype_alpha(): lowering path, type checks, and runtime helpers." sidebar: - order: 393 + order: 408 --- ## `ctype_alpha()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/ctype.rs`:20](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/ctype.rs#L20) (`lower_ctype_alpha`) +- **Signature**: [`src/builtins/string/ctype_alpha.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_alpha.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/ctype.rs`:20](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/ctype.rs#L20) (`lower_ctype_alpha`) - **Function symbol**: `lower_ctype_alpha()` @@ -35,4 +35,3 @@ function ctype_alpha(string $text): bool ## 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 804af113a5..dd6e72eb47 100644 --- a/docs/internals/builtins/type/ctype_digit.md +++ b/docs/internals/builtins/type/ctype_digit.md @@ -2,15 +2,15 @@ title: "ctype_digit() — internals" description: "Compiler internals for ctype_digit(): lowering path, type checks, and runtime helpers." sidebar: - order: 394 + order: 409 --- ## `ctype_digit()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/ctype.rs`:25](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/ctype.rs#L25) (`lower_ctype_digit`) +- **Signature**: [`src/builtins/string/ctype_digit.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_digit.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/ctype.rs`:25](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/ctype.rs#L25) (`lower_ctype_digit`) - **Function symbol**: `lower_ctype_digit()` @@ -35,4 +35,3 @@ function ctype_digit(string $text): bool ## 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 6dc9fd9444..fede58f818 100644 --- a/docs/internals/builtins/type/ctype_space.md +++ b/docs/internals/builtins/type/ctype_space.md @@ -2,15 +2,15 @@ title: "ctype_space() — internals" description: "Compiler internals for ctype_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 395 + order: 410 --- ## `ctype_space()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/ctype.rs`:35](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/ctype.rs#L35) (`lower_ctype_space`) +- **Signature**: [`src/builtins/string/ctype_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_space.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/ctype.rs`:35](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/ctype.rs#L35) (`lower_ctype_space`) - **Function symbol**: `lower_ctype_space()` @@ -35,4 +35,3 @@ function ctype_space(string $text): bool ## 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 f29b36075e..e79832821b 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -2,15 +2,15 @@ title: "floatval() — internals" description: "Compiler internals for floatval(): lowering path, type checks, and runtime helpers." sidebar: - order: 396 + order: 411 --- ## `floatval()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:1034](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L1034) (`lower_floatval`) +- **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`:556](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L556) (`lower_floatval`) - **Function symbol**: `lower_floatval()` @@ -36,4 +36,3 @@ function floatval(mixed $value): float ## 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 5c93138589..5d8df3089e 100644 --- a/docs/internals/builtins/type/get_resource_id.md +++ b/docs/internals/builtins/type/get_resource_id.md @@ -2,15 +2,15 @@ title: "get_resource_id() — internals" description: "Compiler internals for get_resource_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 397 + order: 412 --- ## `get_resource_id()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:424](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L424) (`lower_get_resource_id`) +- **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`) - **Function symbol**: `lower_get_resource_id()` @@ -35,4 +35,3 @@ function get_resource_id(resource $resource): int ## 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 8b7519a022..2febdf1a4d 100644 --- a/docs/internals/builtins/type/get_resource_type.md +++ b/docs/internals/builtins/type/get_resource_type.md @@ -2,15 +2,15 @@ title: "get_resource_type() — internals" description: "Compiler internals for get_resource_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 398 + order: 413 --- ## `get_resource_type()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:412](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L412) (`lower_get_resource_type`) +- **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`) - **Function symbol**: `lower_get_resource_type()` @@ -35,4 +35,3 @@ function get_resource_type(resource $resource): string ## 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 3da66f0b62..a8c97e14b7 100644 --- a/docs/internals/builtins/type/gettype.md +++ b/docs/internals/builtins/type/gettype.md @@ -2,15 +2,15 @@ title: "gettype() — internals" description: "Compiler internals for gettype(): lowering path, type checks, and runtime helpers." sidebar: - order: 399 + order: 414 --- ## `gettype()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:612](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L612) (`lower_gettype`) +- **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`) - **Function symbol**: `lower_gettype()` @@ -35,4 +35,3 @@ function gettype(mixed $value): string ## 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 d755c6f9c5..3a348dfe89 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -2,15 +2,15 @@ title: "intval() — internals" description: "Compiler internals for intval(): lowering path, type checks, and runtime helpers." sidebar: - order: 400 + order: 415 --- ## `intval()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:1001](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L1001) (`lower_intval`) +- **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`:523](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L523) (`lower_intval`) - **Function symbol**: `lower_intval()` @@ -27,14 +27,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function intval(mixed $value, int $base): int +function intval(mixed $value): int ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes exactly 1 argument. ## 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 1ffaadf14f..2fb0727e6f 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -2,15 +2,15 @@ title: "is_array() — internals" description: "Compiler internals for is_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 401 + order: 416 --- ## `is_array()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:1480](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L1480) (`lower_is_array`) +- **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`:1002](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1002) (`lower_is_array`) - **Function symbol**: `lower_is_array()` @@ -37,4 +37,3 @@ function is_array(mixed $value): bool ## 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 83fef96bff..7553078da4 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -2,18 +2,22 @@ title: "is_bool() — internals" description: "Compiler internals for is_bool(): lowering path, type checks, and runtime helpers." sidebar: - order: 402 + order: 417 --- ## `is_bool()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`:736](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L736) (`lower_static_type_predicate`) +- **Function symbol**: `lower_static_type_predicate()` +### Lowering notes + +- Lowers a static `is_*` predicate for concrete non-Mixed values. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function is_bool(mixed $value): bool ## 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 77b58190d6..8f21568224 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -2,15 +2,15 @@ title: "is_callable() — internals" description: "Compiler internals for is_callable(): lowering path, type checks, and runtime helpers." sidebar: - order: 403 + order: 418 --- ## `is_callable()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:802](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L802) (`lower_is_callable`) +- **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`) - **Function symbol**: `lower_is_callable()` @@ -28,15 +28,13 @@ The following runtime helpers are referenced: ## Signature summary ```php -function is_callable(mixed $value, bool $syntax_only = false, string $callable_name = null): bool +function is_callable(mixed $value): bool ``` ## What the type checker enforces -- **Arity**: takes 1–3 arguments (2 optional). -- **By-reference parameters**: `$callable_name`. +- **Arity**: takes exactly 1 argument. ## 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 83af6295f6..304877e8d7 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -2,18 +2,22 @@ title: "is_float() — internals" description: "Compiler internals for is_float(): lowering path, type checks, and runtime helpers." sidebar: - order: 404 + order: 419 --- ## `is_float()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`:736](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L736) (`lower_static_type_predicate`) +- **Function symbol**: `lower_static_type_predicate()` +### Lowering notes + +- Lowers a static `is_*` predicate for concrete non-Mixed values. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function is_float(mixed $value): bool ## 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 704b6d2912..0655d3dca4 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -2,18 +2,22 @@ title: "is_int() — internals" description: "Compiler internals for is_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 405 + order: 420 --- ## `is_int()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`:736](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L736) (`lower_static_type_predicate`) +- **Function symbol**: `lower_static_type_predicate()` +### Lowering notes + +- Lowers a static `is_*` predicate for concrete non-Mixed values. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function is_int(mixed $value): bool ## 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 127d997372..adaa2e8c4e 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -2,15 +2,15 @@ title: "is_iterable() — internals" description: "Compiler internals for is_iterable(): lowering path, type checks, and runtime helpers." sidebar: - order: 406 + order: 421 --- ## `is_iterable()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:1272](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L1272) (`lower_is_iterable`) +- **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`:794](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L794) (`lower_is_iterable`) - **Function symbol**: `lower_is_iterable()` @@ -35,4 +35,3 @@ function is_iterable(mixed $value): bool ## 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 c8a30c86cc..99011917bb 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -2,18 +2,22 @@ title: "is_null() — internals" description: "Compiler internals for is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 407 + order: 422 --- ## `is_null()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`:992](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L992) (`lower_is_null_builtin`) +- **Function symbol**: `lower_is_null_builtin()` +### Lowering notes + +- Lowers `is_null()` for concrete scalar values and boxed Mixed payloads. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function is_null(mixed $value): bool ## 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 d0f726ca35..2b521d9b0b 100644 --- a/docs/internals/builtins/type/is_numeric.md +++ b/docs/internals/builtins/type/is_numeric.md @@ -2,15 +2,15 @@ title: "is_numeric() — internals" description: "Compiler internals for is_numeric(): lowering path, type checks, and runtime helpers." sidebar: - order: 408 + order: 423 --- ## `is_numeric()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/is_numeric.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/is_numeric.rs#L22) (`lower_is_numeric`) +- **Signature**: [`src/builtins/types/is_numeric.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_numeric.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/is_numeric.rs`:22](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/is_numeric.rs#L22) (`lower_is_numeric`) - **Function symbol**: `lower_is_numeric()` @@ -35,4 +35,3 @@ function is_numeric(mixed $value): bool ## 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 941b646244..e42568766c 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -2,15 +2,15 @@ title: "is_object() — internals" description: "Compiler internals for is_object(): lowering path, type checks, and runtime helpers." sidebar: - order: 409 + order: 424 --- ## `is_object()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:1495](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L1495) (`lower_is_object`) +- **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`:1017](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1017) (`lower_is_object`) - **Function symbol**: `lower_is_object()` @@ -36,4 +36,3 @@ function is_object(mixed $value): bool ## 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 817cd16c09..4a83fd77e7 100644 --- a/docs/internals/builtins/type/is_resource.md +++ b/docs/internals/builtins/type/is_resource.md @@ -2,15 +2,15 @@ title: "is_resource() — internals" description: "Compiler internals for is_resource(): lowering path, type checks, and runtime helpers." sidebar: - order: 410 + order: 425 --- ## `is_resource()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:400](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L400) (`lower_is_resource`) +- **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`) - **Function symbol**: `lower_is_resource()` @@ -35,4 +35,3 @@ function is_resource(mixed $value): bool ## 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 22e2b7b26a..4a73bcd86d 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -2,15 +2,15 @@ title: "is_scalar() — internals" description: "Compiler internals for is_scalar(): lowering path, type checks, and runtime helpers." sidebar: - order: 411 + order: 426 --- ## `is_scalar()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins.rs`:1511](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins.rs#L1511) (`lower_is_scalar`) +- **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`:1033](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1033) (`lower_is_scalar`) - **Function symbol**: `lower_is_scalar()` @@ -37,4 +37,3 @@ function is_scalar(mixed $value): bool ## 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 a41110d5eb..56a26f75f6 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -2,18 +2,22 @@ title: "is_string() — internals" description: "Compiler internals for is_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 412 + order: 427 --- ## `is_string()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`(not lowered)`:0]() -- **Function symbol**: `(none — type-checker only)()` +- **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`:736](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L736) (`lower_static_type_predicate`) +- **Function symbol**: `lower_static_type_predicate()` +### Lowering notes + +- Lowers a static `is_*` predicate for concrete non-Mixed values. + ## Runtime helpers _No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ @@ -31,4 +35,3 @@ function is_string(mixed $value): bool ## 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 2ae248b18c..bb36d472c8 100644 --- a/docs/internals/builtins/type/settype.md +++ b/docs/internals/builtins/type/settype.md @@ -2,15 +2,15 @@ title: "settype() — internals" description: "Compiler internals for settype(): lowering path, type checks, and runtime helpers." sidebar: - order: 413 + order: 428 --- ## `settype()` — internals ## Where it lives -- **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen_ir/lower_inst/builtins/types.rs`:25](https://github.com/illegalstudio/elephc/blob/main/src/codegen_ir/lower_inst/builtins/types.rs#L25) (`lower_settype`) +- **Signature**: [`src/builtins/types/settype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/settype.rs) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:25](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L25) (`lower_settype`) - **Function symbol**: `lower_settype()` @@ -36,4 +36,3 @@ function settype(mixed $var, string $type): bool ## Cross-references - [User reference for `settype()`](../../../php/builtins/type/settype.md) - diff --git a/docs/internals/how-elephc-works.md b/docs/internals/how-elephc-works.md index 1c065b1b20..efe80d9024 100644 --- a/docs/internals/how-elephc-works.md +++ b/docs/internals/how-elephc-works.md @@ -291,9 +291,16 @@ The exact textual IR contains value ids, types, ownership, spans, and terminator ## Phase 16: Code generation -**Files:** `src/codegen_ir/`, plus shared `src/codegen/abi/`, `src/codegen/runtime/`, and target helpers — See [The Code Generator](the-codegen.md) for details. - -The EIR backend emits assembly for the selected target. For ordinary control flow this is mostly straight-line branches and labels; for `try` / `catch` / `finally`, the compiler additionally emits handler records and resume labels around `_setjmp` / `_longjmp`-based exception unwinding. The legacy AST backend remains available only through `--ast-backend`; new PHP-visible behavior is expected to go through EIR. By this point our running example has already lost the `if` shell, so the AArch64 form is simpler than the original source (simplified, with comments): +**Files:** `src/codegen/`, plus shared `src/codegen_support/abi/`, +`src/codegen_support/runtime/`, and target helpers — See +[The Code Generator](the-codegen.md) for details. + +The EIR backend emits assembly for the selected target. For ordinary control +flow this is mostly straight-line branches and labels; for `try` / `catch` / +`finally`, the compiler additionally emits handler records and resume labels +around `_setjmp` / `_longjmp`-based exception unwinding. By this point our +running example has already lost the `if` shell, so the AArch64 form is simpler +than the original source (simplified, with comments): ```asm .global _main diff --git a/docs/internals/memory-model.md b/docs/internals/memory-model.md index 9dc0c6562a..782d87dac6 100644 --- a/docs/internals/memory-model.md +++ b/docs/internals/memory-model.md @@ -107,9 +107,9 @@ For heap-backed values, stack slots also carry compile-time ownership metadata i elephc has two representations for PHP `null` in scalar slots, selected per compilation by `--null-repr=sentinel|tagged` (or `ELEPHC_NULL_REPR`). The tagged representation is the -default; the sentinel is the legacy opt-out. +default; the sentinel is the compatibility opt-out. -#### The in-band sentinel (legacy opt-out) +#### The in-band sentinel (compatibility opt-out) `null` is represented as the integer `0x7FFFFFFFFFFFFFFE` (`PHP_INT_MAX - 1`). Because every 64-bit pattern is a valid PHP int, this sentinel collides with the real integer @@ -150,9 +150,9 @@ cmp x1, #8 ; runtime tag 8 = PHP null b.eq value_is_null ``` -A tagged null carries the legacy sentinel as its payload word, so boxing it into a Mixed -cell produces exactly the legacy `{tag 8, sentinel}` words and un-audited consumers degrade -to the legacy behavior. `?int` parameters, returns, and properties keep their boxed Mixed +A tagged null carries the sentinel as its payload word, so boxing it into a Mixed +cell produces `{tag 8, sentinel}` words and un-audited consumers degrade +to sentinel behavior. `?int` parameters, returns, and properties keep their boxed Mixed representation under both modes. ### Pointer values @@ -376,7 +376,7 @@ Associative arrays use a separate heap-allocated structure: an open-addressing h |---|---|---| | `count` | 8 bytes | Number of occupied entries | | `capacity` | 8 bytes | Total number of slots | -| `val_type` | 8 bytes | Coarse value-type summary (0=int, 1=str, 2=float, 3=bool, 4=array, 5=assoc, 6=object, 7=mixed) | +| `val_type` | 8 bytes | Coarse value-type summary (0=int, 1=str, 2=float, 3=bool, 4=array, 5=assoc, 6=object, 7=mixed, 8=null) | | `head` | 8 bytes | Slot index of the first inserted entry, or `-1` when empty | | `tail` | 8 bytes | Slot index of the most recently inserted entry, or `-1` when empty | @@ -585,6 +585,8 @@ The naming pattern comes from `static_property_symbol(...)`. Inherited static pr | Fiber scheduler state | `_fiber_current`, `_fiber_main_saved_sp`, `_fiber_main_saved_exc`, `_fiber_main_saved_call_frame` = 32 bytes total | Fixed-size current-fiber and main-frame resume bookkeeping | | Runtime diagnostics | `_rt_diag_suppression` = 8 bytes total | Fixed-size warning-suppression depth used by `@` and exception unwinding | | JSON state | `_json_last_error`, `_json_active_flags`, `_json_active_depth`, `_json_indent_depth`, `_json_depth_limit`, `_json_validate_idx`, `_json_validate_ptr`, `_json_validate_len`, `_json_decode_assoc`, `_json_error_source_ptr`, `_json_error_location_active`, `_json_error_line`, `_json_error_column` = 104 bytes total | Fixed-size bookkeeping for JSON calls and decode error locations | +| Serialize/unserialize state | `_ser_value_counter`, `_ser_obj_count`, `_unser_count` = 8 bytes each; `_ser_obj_ptrs`, `_ser_obj_idxs`, `_unser_values` = 512KB each | `serialize()` object-dedup counters/maps and `unserialize()` reference registry; overflow degrades gracefully (serialize stops deduping, unserialize fails the ref) | +| Date/time state | `_strtotime_clock`, `_php_default_tz_len` = 8 bytes each; `_php_tz_env`, `_php_tz_save` = 264 bytes each | `strtotime()` clock override plus default-timezone (`date_default_timezone_*`) env/save buffers and stored identifier length | | CLI globals | `_global_argc`, `_global_argv` = 16 bytes total | Fixed-size bookkeeping | | User globals | 16 bytes per `global $var` slot | Grows with number of referenced globals | | Static vars | 24 bytes per `static $var` (`16 + 8 init flag`) | Grows with number of declared static locals | @@ -595,7 +597,7 @@ The naming pattern comes from `static_property_symbol(...)`. Inherited static pr | Stream filter scratch | `_stream_filter_buf`, `_stream_grow_scratch` = 64KB each | Scratch space for stream filters, including length-growing filters such as base64 and quoted-printable encoders | | Stream context and callbacks | `_stream_context_options`, `_stream_notification_callback`, `_stream_connect_host`, `_stream_open_opened_path_scratch`, `_url_stat_matched` | Current stream-context options hash, notification callback, TLS peer host, wrapper opened-path scratch, and wrapper url_stat match flag | | TLS and crypto function slots | `_elephc_tls_*_fn`, `_zlib_*_fn`, `_bz2_*_fn`, `_phar_zlib_*_fn`, `_phar_bz2_*_fn`, `_iconv_*_fn`, `_elephc_crypto_*_fn` = 8 bytes per slot | Late-bound function pointers so programs only link optional TLS/compression/iconv/crypto support when a call site publishes the symbol | -| HTTP/HTTPS/FTP buffers | `_http_resp_buf`, `_https_resp_buf`, `_user_wrapper_drain_buf`, `_phar_write_out` = 1MB each; `_http_req_scratch` = 8KB; `_http_redirect_path_buf`, `_fgc_url_retr` = 2KB each; `_fgc_url_addr`, `_fsockopen_addr` = 512 bytes each; `_ftp_resp_buf` = 4KB; `_ftp_data_addr`, `_ftp_cmd_scratch` = 64 bytes each | Protocol-specific response, request, redirect, FTP, wrapper, and PHAR writer scratch buffers | +| HTTP/HTTPS/FTP buffers | `_http_resp_buf`, `_https_resp_buf`, `_user_wrapper_drain_buf`, `_phar_write_out` = 1MB each; `_http_req_scratch` = 8KB; `_http_redirect_path_buf`, `_fgc_url_retr` = 2KB each; `_fgc_url_addr`, `_fsockopen_addr` = 512 bytes each; `_ftp_resp_buf` = 4KB; `_ftp_data_addr`, `_ftp_cmd_scratch` = 64 bytes each; `_ftp_use_tls` = 8 bytes (FTPS handshake flag) | Protocol-specific response, request, redirect, FTP/FTPS, wrapper, and PHAR writer scratch buffers and flags | | HTTP active context | `_http_active_ignore_errors`, `_http_active_max_redirects`, `_http_active_timeout_seconds`, `_http_active_proxy_ptr`, `_http_active_proxy_len`, `_http_active_host_ptr`, `_http_active_host_len`, `_http_redirect_path_len` | Fixed-size state shared between HTTP request construction and redirect/open helpers | | Socket address scratch | `_recvfrom_addr_ptr`, `_recvfrom_addr_len`, `_accept_peer_ptr`, `_accept_peer_len` = 8 bytes each | Stores peer/address strings returned through by-reference socket parameters | | Protocol/service lookup buffers | `_protoent_buf` = 32KB, `_servent_buf` = 1MB | Scratch buffers for protocol and service database lookups | @@ -617,7 +619,8 @@ elephc uses a **free-list allocator with reference counting plus a targeted cycl 7. **String buffer reset** — the concat buffer resets at each statement, with strings that need to survive copied to heap via `__rt_str_persist` 8. **Stack memory** — automatically reclaimed when functions return 9. **Generator frame release** — Generator frames participate in object refcounting, with a custom deep-free branch for their frame slots and delegated iterator -10. **Process exit** — all memory reclaimed by the OS +10. **Resource scope-cleanup** — Mixed-boxed resources (tag 9) carry a resource-kind subtype in the high payload word, and `__rt_mixed_free_deep` runs the matching destructor when the box is released: kind 1 = native stream fd (`close()`), kind 2 = HashContext handle (`elephc_crypto_free` through `__rt_hash_ctx_free`), kind 3 = `popen` pipe (`__rt_pclose`, which closes the `FILE*` and reaps the child), kind 4 = `opendir` stream (`__rt_closedir`). Kind 0 resources (generic resources) are skipped, and every fd-backed kind also skips handles `>= 0x40000000` — synthetic wrapper handles and the `-1` sentinel that an explicit `fclose`/`pclose`/`closedir` stamps into the box so the descriptor is never released twice (even if its fd number was reused). Alias safety comes from the Mixed box refcount — `$b = $a` increfs the box, so only the last release triggers the destructor +11. **Process exit** — all memory reclaimed by the OS ### What is NOT freed @@ -628,6 +631,8 @@ elephc uses a **free-list allocator with reference counting plus a targeted cycl - **Container-copying builtins** no longer blindly duplicate borrowed heap handles for common nested payload paths: refcounted runtime variants now retain values before new arrays/hash tables take ownership (`array` literals with spreads, `array_merge`, `array_chunk`, `array_slice`, `array_reverse`, `array_pad`, `array_unique`, `array_splice`, `array_diff`, `array_intersect`, `array_filter`, `array_fill`, `array_combine`, `array_fill_keys`) - **Regression coverage now explicitly exercises** local aliases, borrowed nested-container returns, `Owned`/`Borrowed` control-flow merges, and scope-exit paths so future ownership work has focused tripwires instead of relying only on large end-to-end suites - **Raw/off-heap ownership cycles** are still outside the collector. `ptr` values, extern-managed buffers, and raw helper allocations (`kind=0`) are not traversed just because an address exists somewhere +- **Kind-0 resources** (generic/unknown resource kind, including synthetic user-wrapper handles `>= 0x40000000`) are not auto-freed by the Mixed deep-free path — their lifecycle remains managed by the wrapper layer or the user's explicit `close()` call. Kinds 1–4 (native stream fd, HashContext, `popen` pipe, `opendir` stream) are auto-released at scope exit +- **HashContext reuse after `hash_final()`** is memory-safe but not PHP-equivalent: `elephc_crypto_final` finalizes a *clone* and leaves the original handle live and owned by its Mixed box, so the box's kind-2 destructor frees it exactly once. A second `hash_final()` or a `hash_update()`/`hash_copy()` on the same handle therefore does not double-free or use-after-free (where PHP throws "Supplied resource is not a valid Hash Context resource"), it simply keeps hashing the still-live context (documented in `src/codegen/runtime/strings/hash_context.rs`) ### Targeted cycle collection diff --git a/docs/internals/the-codegen.md b/docs/internals/the-codegen.md index 17417f7424..4af8188e3a 100644 --- a/docs/internals/the-codegen.md +++ b/docs/internals/the-codegen.md @@ -1,1187 +1,88 @@ --- title: "The Code Generator" -description: "How typed AST nodes become native assembly for the selected target." +description: "How EIR becomes target assembly and links against the shared runtime." sidebar: order: 7 --- -**Source:** default backend `src/ir_lower/`, `src/ir/`, and `src/codegen_ir/`; shared target/runtime infrastructure under `src/codegen/abi/`, `src/codegen/runtime/`, `src/codegen/platform/`, `src/codegen/emit.rs`, and `src/codegen/data_section.rs`; frozen legacy AST backend under `src/codegen/expr.rs`, `src/codegen/expr/`, `src/codegen/stmt.rs`, `src/codegen/stmt/`, `src/codegen/functions/`, and `src/codegen/builtins/`; intrinsic method registry: `src/intrinsics.rs` - -The code generator (codegen) is the heart of the compiler. The default path lowers the checked and optimized AST into EIR first, then emits native assembly text for the selected target from that EIR. The temporary `--ast-backend` fallback still walks the checked AST directly and emits assembly while the legacy emitter remains in-tree. - -elephc currently supports more than one backend. AArch64 is still the clearest reference path in the codebase and in this document, while Linux `x86_64` is also a supported backend that goes through the same high-level lowering pipeline. - -Most snippets below use AArch64 because the instruction forms are compact and the surrounding docs already explain them in detail. When a section talks about target-specific ABI or runtime behavior, it calls out Linux `x86_64` explicitly. - -For an introduction to AArch64, see [Introduction to ARM64 Assembly](arm64-assembly.md). - -## Overview - -In the default path, `src/ir_lower/` walks the checked optimized AST, produces validated EIR, and `src/codegen_ir/` emits assembly for each EIR function, instruction, and terminator. The CLI's main output is the **user program assembly**; the shared runtime helpers are usually assembled separately and reused from the runtime object cache. The user-facing `.s` file still has this structure: - -```asm -.global _main -.align 2 - -; --- user-defined functions --- -_fn_factorial: - ... - ret - -; --- class methods --- -_method_Point_move: - ... - ret - -; --- main program --- -_main: - ; prologue (stack frame setup) - ; global argc/argv initialization - ; program statements - ; epilogue (exit syscall) - -; --- deferred closures emitted after _main --- -_closure_1: - ... - ret - -; --- data section --- -.data -_str_0: .ascii "hello" -_float_0: .quad 0x400921FB54442D18 - -; --- source markers used by --source-map --- -; @src line=12 col=5 -``` - -Trait composition does not add a separate runtime dispatch layer. Traits are flattened into each concrete class during type checking, then inheritance metadata is layered on top. Codegen still emits `_method_Class_method` / `_static_Class_method` labels, but instance calls now use vtable slots keyed by `class_id` so child overrides work through inherited methods. - -The exact directives and symbol decoration vary by target. The example above is intentionally AArch64-flavored, but the same structural phases apply on Linux `x86_64`. - -When you call the legacy library-style `codegen::generate(...)` entry point, elephc still exposes both pieces explicitly as `(user_asm, runtime_asm)`. The normal CLI path lowers to EIR, calls `codegen_ir::generate_user_asm_from_ir_with_options(...)`, and links against the runtime-object cache so repeated compiles do not have to reassemble the same shared runtime text every time. - -## The Emitter - -**File:** `src/codegen/emit.rs` - -The `Emitter` is a simple string buffer with helper methods: - -| Method | Output | +**Source:** assembly emitter `src/codegen/`; EIR lowering `src/ir_lower/`; +IR model and validation `src/ir/`; shared runtime/ABI support +`src/codegen_support/`. + +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 +user assembly for the selected target. + +## Pipeline Position + +```text +PHP source + -> Lexer + -> Parser + -> Magic constants + -> Conditional compilation + -> Resolver / autoload + -> NameResolver + -> AST constant folding + -> Type checker / warnings + -> AST optimizer passes + -> AST -> EIR lowering + -> EIR validation + -> EIR optimization passes + -> EIR -> target assembly + -> runtime cache + -> assembler / linker + -> binary or cdylib +``` + +`--emit-ir` stops after lowering and IR optimization, printing the textual EIR. +Normal builds continue through `codegen::generate_user_asm_from_ir_with_options` +and link the resulting user object against the cached runtime object. + +## Module Layout + +| Path | Responsibility | |---|---| -| `instruction("mov x0, #42")` | ` mov x0, #42\n` (indented) | -| `label("_main")` | `_main:\n` | -| `comment("load variable")` | ` ; load variable\n` | -| `raw(".global _main")` | `.global _main\n` (no indent) | -| `blank()` | `\n` | - -All assembly is built as text, then written to the `.s` file. - -Statement emission also injects source markers of the form `@src line= col=`. They are ignored by the assembler as comments, but the CLI can later scan them to build a simple source-map sidecar file when `--source-map` is enabled. - -## Runtime split, cache, and source maps - -The compiler's codegen/runtime handoff now has three distinct artifacts: - -1. **User assembly** — emitted by the selected backend into the per-build `.s` file -2. **Runtime object** — assembled from the shared runtime once and cached under `~/.cache/elephc/` (or `XDG_CACHE_HOME`) using the compiler version, target, heap size, and generated runtime assembly hash in the filename -3. **Optional source map** — a JSON sidecar generated from `@src` markers embedded in the user assembly comments - -This means normal CLI builds no longer concatenate the runtime text into every output assembly file before assembling. Instead, they: - -- prepare or reuse the cached runtime object -- assemble only the user `.s` file into `file.o` -- link `file.o` against the cached runtime object - -The source-map file is intentionally simple. Today it stores a list of `(asm_line, php_line, php_col)` entries so tools and humans can correlate generated assembly back to the original PHP statements without needing full DWARF debug info. - -The AST optimizer intentionally still runs before backend selection. By the time the default EIR backend runs, constant expressions and some dead control-flow have already been removed, and EIR adds the function-wide value and control-flow shape that the linear-scan register allocator (`src/ir_passes/`, see [The IR](the-ir.md)) and future IR optimizations rely on. The legacy AST backend sees the same optimized AST when `--ast-backend` is selected. - -## Emit modes: executable vs cdylib - -Codegen runs in one of two emit modes selected by the `--emit` flag. `executable` (the default) produces the standalone-binary shape described throughout this page: a `main` entry point, top-level statements, and a process-exit epilogue. `cdylib` produces a shared library instead: no `main` body is emitted, and after the user functions a set of C-ABI trampolines is appended for every `#[Export]`-marked function plus the four `elephc_*` lifecycle entry points (see [Shared Libraries](../beyond-php/cdylib.md)). - -Cdylib emission also switches the emitter into position-independent mode (`pic_data_refs`): global data references emitted through the `abi::symbols` helpers resolve through the GOT (`@GOTPCREL` on x86_64, `:got:`/`:got_lo12:` on AArch64) instead of direct PC-relative addressing, and the runtime object is generated and cached separately in a PIC variant. On ELF targets a final pass (`src/codegen/visibility.rs`) appends `.hidden` directives for every internal global so the `.so` exports only its public ABI and internal runtime state cannot be preempted across loaded modules. - -## The Context - -**File:** `src/codegen/context.rs` - -The `Context` tracks state during code generation: - -```rust -pub struct Context { - pub variables: HashMap, // variable → type + stack offset - pub stack_offset: usize, // next available stack slot - pub loop_stack: Vec, // for break/continue - pub return_label: Option, // for early returns - pub functions: HashMap, - pub function_variant_groups: HashSet, // include-loaded function dispatchers - pub deferred_closures: Vec, // closures emitted after current function - pub deferred_fiber_wrappers: Vec, - pub deferred_callback_wrappers: Vec, - pub constants: HashMap, // compile-time constants - pub global_vars: HashSet, // globals active in current scope - pub static_vars: HashSet, // statics active in current scope - pub ref_params: HashSet, // pass-by-reference params - pub local_ref_cell_flags: HashMap, // compiler-created ref cells - pub in_main: bool, // whether we're compiling top-level code - pub all_global_var_names: HashSet, - pub all_static_vars: HashMap<(String, String), PhpType>, - pub closure_sigs: HashMap, - pub callable_param_sigs: HashMap<(String, String), FunctionSig>, - pub closure_captures: HashMap>, - pub runtime_callable_builtin_wrappers: HashMap, - pub runtime_callable_extern_wrappers: HashMap, - pub runtime_callable_static_method_wrappers: HashMap, - pub first_class_callable_targets: HashMap, - pub variable_fcc_label: HashMap, - pub classes: HashMap, - pub interfaces: HashMap, - pub traits: HashSet, - pub enums: HashMap, - pub packed_classes: HashMap, - pub current_class: Option, - pub extern_functions: HashMap, - pub extern_classes: HashMap, - pub extern_globals: HashMap, - pub return_type: PhpType, - pub activation_prev_offset: Option, - pub activation_cleanup_offset: Option, - pub activation_frame_base_offset: Option, - pub pending_action_offset: Option, - pub pending_target_offset: Option, - pub nested_concat_offset_offset: Option, - pub pending_return_value_offset: Option, - pub try_slot_offsets: Vec, - pub next_try_slot_idx: usize, - pub finally_stack: Vec, -} -``` - -Each variable has a `VarInfo`: - -```rust -pub struct VarInfo { - pub ty: PhpType, // current runtime storage type - pub static_ty: PhpType, // declared/static type retained for checks and calls - pub stack_offset: usize, // offset from frame pointer (x29) - pub ownership: HeapOwnership, // NonHeap / Owned / Borrowed / MaybeOwned - pub epilogue_cleanup_safe: bool, // false for locals populated through still-ambiguous control-flow/alias paths -} -``` - -`HeapOwnership` is a codegen-only ownership lattice used for heap-backed values flowing through stack slots: - -- `NonHeap` — integers, floats, bools, null, resources, raw pointers -- `Owned` — this slot definitely owns the current heap-backed value -- `Borrowed` — this slot currently aliases heap storage owned elsewhere -- `MaybeOwned` — control flow merged heap-backed paths with different ownership states - -The lattice is now threaded through the main local-variable paths. Function epilogues re-enable cleanup only for slots classified as `Owned` and still marked `epilogue_cleanup_safe`; locals coming from still-ambiguous control-flow or aliasing paths are intentionally skipped. Special aliases such as `$this`, by-reference params, globals, and statics are explicitly kept out of epilogue cleanup because the current frame does not own their storage. Builtins that duplicate containers now also dispatch to dedicated `_refcounted` runtime helpers when their element/value types are heap-backed, so nested array/hash/object/string payloads are retained before the new container becomes an owner. - -The exception-related fields let codegen thread `try` / `catch` / `finally` through non-local control flow. Function and `_main` frames publish activation records into the runtime cleanup stack, pre-allocate handler slots for `setjmp` buffers, and use `finally_stack` plus the `pending_*` slots to defer `return`, `break`, and `continue` until the innermost `finally` body has run. - -### Label generation - -`ctx.next_label("while")` produces `_while_1`, `_while_2`, etc. A global atomic counter ensures labels never collide across functions or compilation units. - -## The Data Section - -**File:** `src/codegen/data_section.rs` - -String literals and float constants are stored in the `.data` section: - -```rust -pub struct DataSection { - entries: Vec<(String, Vec)>, // string label → bytes - float_entries: Vec<(String, u64)>, // float label → bit pattern - counter: usize, // next unique label suffix - dedup: HashMap, String>, // avoid duplicate strings - float_dedup: HashMap, // avoid duplicate floats -} -``` - -When the codegen encounters `"hello"`, it calls `data.add_string(b"hello")` which returns a label (`_str_0`) and length (`5`). Identical strings are deduplicated — two `"hello"` literals share the same label. - -Floats are stored as their raw 64-bit IEEE 754 bit patterns (`.quad` directive). - -## Legacy AST expression codegen - -**Files:** `src/codegen/expr.rs`, `src/codegen/expr/` - -The frozen `--ast-backend` path still uses `emit_expr()` to take an expression node and emit code that leaves the result in the standard registers. The default backend reaches the same ABI/runtime helpers through EIR lowering in `src/ir_lower/` and instruction lowering in `src/codegen_ir/`. The top-level legacy `expr.rs` file mainly dispatches into focused helpers under `expr/` such as `scalars.rs`, `variables.rs`, `binops/`, `arrays.rs`, `compare/`, `calls/`, and `objects/`. - -| Type | Result location | -|---|---| -| `Int` / `Bool` / `Void` / `Resource` | `x0` | -| `Float` | `d0` | -| `Str` | `x1` (pointer), `x2` (length) | -| `Array` / `AssocArray` / `Iterable` | `x0` (heap pointer) | -| `Mixed` | `x0` (pointer to boxed mixed cell) | -| `Object` | `x0` (heap pointer) | -| `Callable` / `Pointer` | `x0` | -| `Buffer` / `Packed` | `x0` (heap pointer) | -| `Union` | `x0` (same as Mixed — boxed runtime-tagged payload) | - -### Expression AST dispatch coverage - -The legacy expression dispatcher is intentionally thin. It routes each `ExprKind` -variant into one of the focused lowering paths below, while the EIR path mirrors the same PHP-visible coverage through `src/ir_lower/expr/`: - -| Variants | Lowering path | -|---|---| -| `StringLiteral`, `IntLiteral`, `FloatLiteral`, `BoolLiteral`, `Null`, `Negate`, `Not`, `BitNot`, `Cast`, `Print`, `ErrorSuppress` | Scalar, coercion, stdout, and diagnostics helpers | -| `Variable`, `This`, `PreIncrement`, `PostIncrement`, `PreDecrement`, `PostDecrement`, `Assignment` | Variable load/store and assignment-expression helpers | -| `BinaryOp`, `InstanceOf`, `NullCoalesce`, `Pipe`, `Ternary`, `ShortTernary`, `Throw` | Operator, comparison, call-pipe, branch, and exception-aware expression helpers | -| `ArrayLiteral`, `ArrayLiteralAssoc`, `ArrayAccess`, `Spread`, `Match` | Indexed-array, associative-array, unpacking, string-indexing, and match-expression helpers | -| `FunctionCall`, `NamedArg`, `ClosureCall`, `ExprCall`, `Closure`, `FirstClassCallable` | Shared call-argument planner, closure wrappers, and callable dispatch helpers | -| `ConstRef`, `ClassConstant`, `ScopedConstantAccess`, `MagicConstant` | Compile-time constant and class-constant loading. `MagicConstant` should already be lowered by the frontend before codegen. | -| `NewObject`, `NewDynamic`, `NewScopedObject`, `NewDynamicObject`, `PropertyAccess`, `DynamicPropertyAccess`, `NullsafePropertyAccess`, `NullsafeDynamicPropertyAccess`, `StaticPropertyAccess`, `MethodCall`, `NullsafeMethodCall`, `StaticMethodCall` | Object allocation (including `new $var()` via `NewDynamic` and the internal runtime-class-string factory `NewDynamicObject`), property/member access, nullsafe chain lowering, vtable dispatch, and late-static-binding helpers | -| `PtrCast`, `BufferNew`, `Yield`, `YieldFrom` | Pointer/buffer extensions and generator state-machine lowering | - -### Intrinsic Calls - -Most method calls use the normal class metadata path: receiver evaluation, argument materialization, vtable or direct-method target selection, then a call to the emitted PHP method body. A small set of runtime-managed core objects cannot use the synthetic PHP stubs as their real implementation. For those, `src/intrinsics.rs` records an `IntrinsicCall` entry keyed by PHP class and method name, and `src/codegen/expr/objects/dispatch/intrinsic.rs` emits the direct runtime-helper call after the normal receiver and argument setup has already run. - -Current intrinsic call sites cover `Fiber` instance/static APIs and the runtime-backed `Generator` method surface. User classes with the same method names are not affected because the lookup includes the resolved class name. - -### Literals - -```php -42 → mov x0, #42 -3.14 → adrp x9, _float_0@PAGE / add x9, ... / ldr d0, [x9] -"hello" → adrp x1, _str_0@PAGE / add x1, ... / mov x2, #5 -true → mov x0, #1 -null → movz x0, #0xFFFE / movk x0, ... (load null sentinel) -``` - -Large integers (> 65535 or negative) use `movz` + `movk` sequences. See [ARM64 Instruction Reference](arm64-instructions.md#loading-large-constants). - -### The push/pop pattern for binary operations - -Binary operations like `$a + $b` need both operands in registers simultaneously, but `emit_expr` uses the same registers for every expression. The solution: **push the left result onto the stack, evaluate the right, then pop the left back**. - -```php -$a + $b -``` - -```asm -; Step 1: evaluate left ($a) -ldur x0, [x29, #-8] ; x0 = $a - -; Step 2: push left onto stack -str x0, [sp, #-16]! ; save x0 to stack, decrement sp - -; Step 3: evaluate right ($b) -ldur x0, [x29, #-16] ; x0 = $b (overwrites left!) - -; Step 4: pop left back into a different register -ldr x1, [sp], #16 ; restore left into x1, increment sp - -; Step 5: operate -add x0, x1, x0 ; x0 = left + right -``` - -For strings (which use two registers), the push saves both `x1` and `x2`, and the pop restores them to `x3` and `x4`. - -For floats, the push/pop uses `d0`/`d1`: - -```asm -str d0, [sp, #-16]! ; push left float -; ... evaluate right → d0 ... -ldr d1, [sp], #16 ; pop left float into d1 -fadd d0, d1, d0 ; d0 = left + right -``` - -### Comparison operators - -Comparisons use `cmp` (integer) or `fcmp` (float) followed by `cset`: - -```php -$x > 5 -``` - -```asm -; ... push $x, evaluate 5 ... -cmp x1, x0 ; compare left with right -cset x0, gt ; x0 = 1 if greater, 0 otherwise -``` - -The result is always `x0` with value 0 or 1 (`PhpType::Bool`). - -### Short-circuit logical operators - -`&&`, `||`, `and`, and `or` use **short-circuit evaluation** — the right side isn't evaluated if the left determines the result. `xor` is also a logical operator, but it evaluates both operands because exclusive OR needs both truthiness values. - -```php -$a && $b -``` - -```asm -; evaluate $a -cmp x0, #0 -b.eq _sc_end_1 ; if $a is falsy, skip $b entirely (result = 0) -; evaluate $b -cmp x0, #0 -cset x0, ne ; result = whether $b is truthy -_sc_end_1: -``` - -### String concatenation - -The `.` operator calls the runtime's `__rt_concat`: - -```php -"hello" . " world" -``` - -```asm -; push left string (x1, x2) -; evaluate right string → x1, x2 -; pop left → x3, x4 -; call concat -mov x3, ... ; left ptr -mov x4, ... ; left len -bl __rt_concat ; result → x1 (ptr), x2 (len) -``` - -See [The Runtime](the-runtime.md) for how `__rt_concat` works. - -### Bitwise operations - -The bitwise operators (`&`, `|`, `^`, `~`, `<<`, `>>`) operate on integers and emit single ARM64 instructions: - -```php -$a & $b → and x0, x1, x0 // bitwise AND -$a | $b → orr x0, x1, x0 // bitwise OR -$a ^ $b → eor x0, x1, x0 // bitwise XOR -$a << $b → lsl x0, x1, x0 // logical shift left -$a >> $b → asr x0, x1, x0 // arithmetic shift right (preserves sign) -~$a → mvn x0, x0 // bitwise complement (one's complement) -``` - -Like other binary operations, bitwise ops use the push/pop pattern — evaluate left, push, evaluate right, pop left, apply operation. - -### Spaceship operator - -The spaceship operator (`<=>`) returns -1, 0, or 1 depending on the comparison result. It uses conditional select instructions: - -```php -$a <=> $b -``` - -```asm -; ... push $a, evaluate $b ... -cmp x1, x0 ; compare left with right -cset x0, gt ; x0 = 1 if left > right, else 0 -csinv x0, x0, xzr, ge ; if left < right: x0 = ~0 = -1 (all ones) -``` - -`csinv` (conditional select invert) inverts `xzr` (the zero register) to produce -1 when the condition is not met. - -For floats, `fcmp` replaces `cmp`, but the same `cset`/`csinv` pattern applies. - -### Array union - -When both operands of `+` are arrays, codegen routes the expression to PHP array-union lowering instead of numeric addition. Indexed arrays call `__rt_array_union`, which clones the left operand and appends only the right-side numeric suffix whose keys are missing from the left. Associative arrays call `__rt_hash_union`, which clones the left hash, walks the right hash in insertion order, and inserts only keys that are absent from the clone. Mixed indexed/associative operands return a hash result: `__rt_array_hash_union` maps left indexed positions into integer hash keys before merging the right hash, while `__rt_hash_array_union` clones the left hash and probes right indexed positions as integer keys. - -### Null coalescing operator - -The `??` operator returns the left operand if it is non-null, otherwise the right: - -```php -$x ?? "default" -``` - -```asm -; evaluate $x -; compare with null sentinel (0x7FFFFFFFFFFFFFFE) -b.ne _nc_done_1 ; if not null, keep left value -; evaluate "default" ; otherwise, use right side -_nc_done_1: -``` - -The null check compares the value against the [null sentinel](memory-model.md). The operator is right-associative (`$a ?? $b ?? $c` = `$a ?? ($b ?? $c)`). - -Null coalescing assignment is parsed as `$x = $x ?? expr`, but assignment lowering recognizes that exact shape and emits a conditional store: - -```php -$x ??= "default"; -``` - -The generated code loads `$x`, branches past the assignment when it is non-null, and evaluates/stores the right-hand side only on the null path. This preserves PHP's `??=` short-circuit behavior and avoids rewriting an already-owned heap value back into the same local slot. - -### Pipe operator - -PHP 8.5 `value |> callable` lowers through `src/codegen/expr/calls/pipe.rs`. -`emit_expr()` first stores the left-hand value into a hidden local slot so the -left side is observably evaluated before the callable target. It then builds a -synthetic one-argument call using that hidden local as the single positional -argument. - -The pipe lowering delegates to the existing call paths whenever possible: -first-class function targets become `FunctionCall`, first-class static method -targets become `StaticMethodCall`, first-class instance method targets become -`MethodCall`, local callable variables become `ClosureCall`, and other callable -expressions become `ExprCall`. Argument planning, ABI materialization, -ownership, and diagnostics therefore stay aligned with ordinary calls. - -### Error-control operator - -The `@` operator lowers to a scoped runtime diagnostic-suppression pair: - -1. Call `__rt_diag_push_suppression` -2. Evaluate the operand normally -3. Preserve the operand result in the appropriate ABI result shape -4. Call `__rt_diag_pop_suppression` -5. Restore the operand result - -The exception handler frame also snapshots the current suppression depth before `setjmp()` and restores it after a `longjmp()` into catch dispatch. That prevents a thrown expression inside `@` from leaking warning suppression into later code. - -### Nullsafe operator - -The `?->` operator lowers nullable receivers through the boxed mixed path used by nullable and union storage. Codegen flattens postfix chains that contain a nullsafe segment, evaluates the base once, and branches to a shared boxed-`null` result when a nullsafe receiver is null. That branch skips the rest of the chain, including later ordinary `->` segments, array indexes, method arguments, and callable arguments. If an ordinary segment later receives a real null value from the non-short-circuited path, it still follows PHP's warning or fatal behavior. - -### Type coercions - -When types need to match (e.g., int + float), the codegen inserts conversion instructions: - -```asm -scvtf d0, x0 ; convert signed integer (x0) → double (d0) -fcvtzs x0, d0 ; convert double (d0) → signed integer (x0) -``` - -The `.` (concat) operator also coerces non-strings: -- `Int` → calls `__rt_itoa` to get a string -- `Float` → calls `__rt_ftoa` -- `Bool true` → string "1" -- `Bool false` / `Null` → empty string (length 0) - -### Constant references - -```php -const MAX = 100; -echo MAX; -``` - -Constants declared with `const` or `define()` are resolved at compile time. When the codegen encounters a `ConstRef`, it looks up the constant's value and emits it as a literal — `mov x0, #100` for an integer, or loads a string label from the data section. `define()` call sites still emit a per-constant runtime seen flag so the call returns `true` only for the first runtime definition and returns `false` with a suppressible warning on duplicate attempts. - -Enum cases reuse the same idea, but through enum metadata instead of scalar constants: parser output uses `ExprKind::ScopedConstantAccess` for `Color::Red`, and codegen detects enum receivers to load the canonical enum-case symbol emitted in runtime data. Helper builtins such as `Enum::from()` / `Enum::tryFrom()` lower through the checker/codegen enum tables carried in `Context`; a missing `Enum::from()` value constructs a catchable `ValueError` with the PHP-compatible backing-value message. - -### Pointer values and casts - -Pointer expressions are carried in `x0` as plain 64-bit addresses: - -- `ptr($var)` computes the address of a stack or global slot and returns it in `x0` -- `ptr_null()` loads the zero address -- `ptr_cast($p)` only changes the static type tag seen by the checker, so codegen emits the inner expression and leaves the address unchanged -- Pointer printing routes through `__rt_ptoa`, which formats the address as a `0x...` string before writing - -### Buffer allocation and packed hot-path access - -`buffer_new(len)` lowers directly from `ExprKind::BufferNew`: codegen evaluates the element count, loads the checked element stride from the type metadata, and calls `__rt_buffer_new`. The resulting pointer in `x0` references a contiguous `[length][stride][payload...]` block rather than a PHP array/hash structure. - -When `T` is a scalar POD type, reads and writes use direct address arithmetic from the buffer base plus `index * stride`. When `T` is a `packed class`, codegen combines the buffer element stride with the field offset from `packed_classes` metadata and emits direct typed loads/stores into the packed payload. - -### Function calls - -```php -my_func($a, $b, $c) -``` - -1. Evaluate each argument and push results onto the stack -2. Pop arguments into the correct ABI registers (`x0`-`x7` for ints, `d0`-`d7` for floats, two registers per string) -3. If a heap-backed argument is being borrowed from an existing owner (for example a local variable or container read), retain it before passing it to the callee -4. `bl _fn_my_func` — branch with link (saves return address) -5. Result is in `x0`/`d0`/`x1`+`x2` depending on return type - -Named-argument calls split evaluation order from ABI order. `src/codegen/expr/calls/args.rs` evaluates source arguments left-to-right, stores any out-of-order values in temporary slots, validates spread prefixes after later named expressions have run, then materializes the final parameter list in ABI order. Spread prefixes before named arguments are evaluated once; multiple prefix spreads are combined before runtime length/overwrite checks, and too-short positional spreads for required parameters fail instead of reading beyond the array payload. Runtime associative-array spreads are dynamic named providers: they look up string keys by parameter name, fall back to numeric keys for positional slots, and let the per-parameter missing/default branch decide whether a required value is present. Built-in and extern named calls use the same source-order pre-evaluation step before their normalized positional emitters run; mutating built-ins mark their target parameter as ref-like so pre-evaluation does not redirect writes into a temporary. Extern calls preserve PHP source evaluation order first and only then load C ABI registers. - -## Closure codegen - -### Anonymous functions and arrow functions - -Closures (`function($x) { ... }`) and arrow functions (`fn($x) => ...`) are compiled as separate labeled functions, similar to user-defined functions. The key difference is **deferred emission** — the closure body is not emitted inline. Instead: - -1. **At the closure expression site**: the codegen generates a unique entry label (e.g., `_closure_1`), creates a static callable descriptor in `.data`, and loads the descriptor address into `x0`. The descriptor includes side records for signature/default/by-reference/variadic metadata, capture and hidden-parameter bindings, and invocation shape. The descriptor pointer is then stored in the variable's stack slot as a `Callable` (8 bytes). - -2. **The body is deferred**: the closure's parameter list, body statements, captured variables, and label are pushed onto `ctx.deferred_closures`. This avoids emitting function code in the middle of the current function's instruction stream. - -3. **After `_main`**: all deferred closures are emitted as standalone labeled functions (prologue, body, epilogue), just like user-defined functions. - -### `use` captures - -Closures can capture variables from the enclosing scope via `use ($var1, $var2)`: - -```php -$greeting = "Hello"; -$fn = function($name) use ($greeting) { - echo $greeting . " " . $name; -}; -``` - -Only explicit `use (...)` captures are stored in the AST and forwarded as hidden closure arguments. Arrow functions are still parsed as closures, but they use `is_arrow = true` with an empty `captures` list. - -The AST stores captured variable names in the `captures` field of the `Closure` expression. At the call site, captured variables are passed as **extra arguments** after the explicit arguments: - -1. **At the closure expression site**: the captured variable names and types are recorded in `ctx.closure_captures` alongside the deferred closure. -2. **At the call site** (`$fn("World")`): the codegen looks up the captured variables, evaluates them from the caller's scope, and passes them as additional arguments after the explicit ones. -3. **In the closure body**: the captured values arrive as extra parameters and are stored in local stack slots, making them accessible like regular local variables. - -This means captures are passed **by value** — modifying a captured variable inside the closure does not affect the outer scope (matching PHP semantics). - -### Closure calls - -When a closure variable is called (`$fn(1, 2)`), the codegen: - -1. Evaluates each argument and pushes results onto the stack -2. Loads the closure descriptor from the variable's stack slot into `x9` -3. Loads the native entry address from the descriptor's entry slot -4. Pushes `x9` temporarily while popping arguments into ABI registers -5. Pops `x9` back and calls `blr x9` — an indirect branch through a register - -`blr` (Branch with Link to Register) is like `bl` but the target address comes from a register rather than a label. This is what makes closures work — the compiler doesn't know at compile time which function will be called, so it uses an indirect jump. - -### Closures as callback arguments - -Built-in functions like `array_map`, `array_filter`, `array_reduce`, `array_walk`, `usort`, `uksort`, `uasort`, and `preg_replace_callback` accept callback values. PHP callable storage carries a descriptor pointer; callback runtimes receive the native entry loaded from that descriptor plus an optional environment pointer, then call the entry via `blr`. - -For captured closures passed through callback runtimes such as `array_map`, `array_filter`, `array_reduce`, `array_walk`, `usort`, `uksort`, `uasort`, and `preg_replace_callback`, codegen builds a temporary callback environment containing the original entry address plus its hidden `use (...)` values. The runtime passes that environment to a generated callback wrapper, and the wrapper re-materializes the original visible arguments plus hidden captures before calling the closure. `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `usort()`, `uksort()`, `uasort()`, and `preg_replace_callback()` route descriptor-valued callable variables and `callable` parameters through descriptor-backed callback wrappers, so stored closure captures and first-class-callable receiver environments come from the descriptor rather than from source locals that may have changed. Those callback runtimes also select descriptor cases for runtime callable-array variables such as `[$object, $method]` or `[$class, $method]`, then build the same descriptor callback environment after the runtime receiver/class and method strings match a public method. These runtimes can also route branch-shaped captured callable expressions through descriptor-backed callback wrappers: the environment stores the selected descriptor, the wrapper boxes visible arguments into a temporary Mixed argument array, invokes the descriptor's uniform invoker, casts or discards the boxed result for the callback runtime, and preserves runtime-loop callee-saved registers around the generated invoker. `CallbackFilterIterator` and `RecursiveCallbackFilterIterator` use a heap-backed variant of that descriptor environment so branch-selected captured descriptors and runtime-selected callable-array variable or literal descriptors stay attached to the iterator object and recursive child filters. `iterator_apply()` uses the same uniform descriptor invoker for branch-shaped captured callbacks and runtime-selected callable-array variables, with its callback-argument array evaluated once before the iterator loop and reused for each invocation. String-producing descriptor-backed `array_map()` and `preg_replace_callback()` calls detach string results from boxed Mixed values before releasing the invoker result. `call_user_func()` and `call_user_func_array()` can dispatch through callable descriptors directly: closure and first-class-callable values with hidden context allocate runtime descriptor copies whose static header is followed by 16-byte capture slots. Dynamic callback dispatch uses `codegen::callable_dispatch` cases carrying descriptor labels with PHP-visible names, signature metadata, defaults, by-reference flags, variadic metadata, and hidden captures. Descriptor-selected callbacks compare entry slots with user functions and emitted closure/FCC wrappers; when the matched case exposes an invoker, the callsite switches back to the actual descriptor and calls the uniform wrapper. Runtime string-name callbacks compare case-insensitively against user-function, extern-wrapper, builtin-wrapper, and static-method names, materialize the matched descriptor, then call the descriptor's uniform invoker wrapper. Compile-time static-method callable arrays also use static-method descriptors for direct variable and literal calls, `call_user_func()` calls, and `call_user_func_array()` calls, including associative variadic tails and positional prefixes before indexed spreads. Direct instance-method callable-array variable calls read the receiver from the stored callable array slot, while direct literal calls evaluate the receiver before visible call arguments; both then build the receiver-prefixed descriptor argument container. Direct invokable-object variable calls build the same receiver-prefixed descriptor argument container from the local object and use the object-invoke invocation shape. Receiver-bound `call_user_func()` calls with one spread source pass that source container through the receiver-prefix normalizer and descriptor invoker instead of falling back to direct method emission. Receiver-bound calls with positional prefixes followed by indexed spreads build a raw Mixed-slot indexed argument array, prepend the receiver as descriptor slot zero, append the prefix values, merge cloned indexed spread tails, and then let the descriptor normalizer clone and box that container for invocation. Direct branch-selected expression calls that combine spread prefixes with named arguments build a temporary Mixed hash from the source-order positional prefix plus named suffix entries, while direct calls whose only argument segment is one spread source pass that source container to the same descriptor invoker. Direct descriptor calls with positional prefixes followed by indexed spreads use the same raw Mixed-slot indexed container builder, without adding a receiver slot. Indirect calls on callable variables or array elements fall back to the descriptor invoker whenever the local callsite no longer has a trustworthy static signature or capture list; variable arguments in indexed containers, positional prefixes before indexed spreads, and associative named-argument hashes are encoded as ref-cell markers so the generated invoker can apply runtime by-reference flags from the descriptor or dereference the same marker for by-value parameters. The invoker receives `(descriptor, boxed argument container)`, loads the entry slot itself, reloads hidden captures from the descriptor when present, branches on the boxed container tag for indexed-array or associative-hash materialization, unboxes boxed array/hash payloads for declared array parameters, applies the matched signature, and returns a boxed `mixed`. `call_user_func_array()` descriptor invokers receive a cloned temporary argument container widened to boxed `Mixed`, so wrappers are shared by callable signature instead of by the caller's static element type or argument-array shape. When `call_user_func_array()` targets a by-reference callback and receives a literal argument array, codegen passes frame-slot addresses for variable elements in by-reference positions instead of loading array payload values. - -Extern `callable` parameters that receive descriptor values use a separate C-ABI trampoline path. Codegen emits one mutable descriptor slot and one trampoline symbol per callsite; the extern call stores the retained descriptor in that slot, passes the trampoline address to C, and the trampoline boxes incoming scalar/pointer callback arguments before calling the descriptor invoker. This lets C APIs with only a raw function-pointer slot retain closure captures, first-class method receivers, and branch-selected descriptor state without changing the C signature. - -First-class callable wrappers reuse this hidden argument path when the callable target carries context. `$obj->method(...)` records the receiver as a hidden capture; non-local receiver expressions are evaluated once into a hidden temporary before wrapper creation. `static::method(...)` records the forwarded called-class id, or `$this` in an instance method, so late static binding is preserved for direct callable calls and for callback paths that forward an environment. - -## Generator codegen - -**Files:** `src/codegen/functions/generator/`, `src/codegen/runtime/generators/`, `src/codegen/expr/objects/dispatch/vtable.rs` - -A function or closure body that contains `yield` does not emit as an ordinary function body. Codegen emits two symbols: - -1. `_fn_` — a wrapper that allocates a heap `GeneratorFrame`, stamps it as the built-in `Generator` object, copies supported scalar parameters/captures into frame slots, zeroes local slots, and returns the frame pointer. -2. `_fn___resume` — a state-machine entry point. State `0` enters the body; each yield gets a numbered resume label. At a yield, the resume function boxes the key/value into Mixed cells, replaces the frame's last key/value slots, stores the next state index, and returns to the caller. - -Generator closures reuse the same path as ordinary deferred closures, but their hidden `use (...)` captures are copied into the generator frame alongside visible parameters. `yield from` stores the active inner generator in the frame's `delegated_iter` slot and resumes it through the same `__rt_gen_*` runtime helpers used by user-visible `Generator` methods. - -The generated `Generator` object has a custom payload layout rather than ordinary PHP properties. Method dispatch for `current`, `key`, `valid`, `next`, `rewind`, `send`, `throw`, and `getReturn` is intercepted before vtable lookup and routed directly to `__rt_gen_*`. Both AArch64 and Linux `x86_64` follow the same high-level state-machine model; the wrapper, resume dispatcher, and runtime helper emitters select target-specific instruction sequences internally. - -## Fiber codegen - -**Files:** `src/codegen/expr/objects/allocation.rs`, `src/codegen/expr/objects/dispatch/`, `src/codegen/expr/objects/fiber_callable.rs`, `src/codegen/expr/objects/fiber_wrapper.rs`, `src/codegen/functions/fiber_wrapper.rs`, `src/codegen/runtime/fibers/` - -`Fiber` is a built-in class, but codegen does not lower it through the ordinary object constructor and method-dispatch path. `new Fiber($callable)` is intercepted, materializes raw string callbacks, callable-array, and invokable-object shapes into callable descriptors, then delegates to `__rt_fiber_construct`, which allocates the larger runtime-managed Fiber object, creates its guarded native stack, stores the callable descriptor pointer, and records the generated wrapper label that adapts Fiber start values to the callback ABI. Stored instance-method callable arrays bind the receiver from `$callback[0]` into a descriptor capture so later writes to the source callable variable cannot change the Fiber body. Inline receiver expressions, invokable-object expressions, and runtime-selected callable-array variables or literals are evaluated once at construction time before the receiver is stored in the descriptor. - -Each accepted Fiber callback gets a deferred entry wrapper emitted next to deferred closure bodies. The wrapper runs on the Fiber stack, reloads boxed `start()` values from `start_args[0..6]`, unboxes them to the callback's declared parameter types, reloads hidden captures or method receivers from the stored callable descriptor's runtime capture slots, loads the original entry from that descriptor, calls it with normal ABI materialization, and boxes the terminal return value back to `mixed`. - -Instance and static Fiber methods are also intercepted: - -- `$fiber->start(...)` spills up to seven boxed `mixed` start arguments into the Fiber object before calling `__rt_fiber_start`; callable captures and receivers are already stored in the descriptor and are not overwritten by start arguments. -- `$fiber->resume($value)`, `$fiber->throw($exception)`, `$fiber->getReturn()`, and the state predicates branch directly to their `__rt_fiber_*` runtime helpers. -- `Fiber::suspend($value)` and `Fiber::getCurrent()` lower to runtime helper calls instead of ordinary static method dispatch. - -Both AArch64 and Linux `x86_64` use the same high-level lowering. The final register moves, temporary-stack layout, direct/indirect calls, and frame setup go through the ABI module so the Fiber wrapper follows each target's calling convention rather than hardcoding ARM64 register names in shared code. - -## Associative array codegen - -Associative arrays use a hash table stored on the heap. The codegen differs from indexed arrays at every level: - -### Literal creation - -```php -$m = ["name" => "Alice", "age" => "30"]; -``` - -1. Call `__rt_hash_new` with initial capacity and value type tag → `x0` = hash table pointer -2. For each key-value pair: evaluate key (string → `x1`/`x2`), evaluate value, call `__rt_hash_set` - -### Access - -```php -$m["name"] -``` - -1. Save hash table pointer on stack -2. Evaluate key expression → `x1`/`x2` (string) -3. Call `__rt_hash_get` → `x0` = found (0/1), `x1` = value_lo, `x2` = value_hi, `x3` = per-entry value tag -4. Move result to standard registers based on value type; if the static result is `Mixed`, box the payload into a heap cell first - -### Functions on associative arrays - -Builtin functions like `array_key_exists`, `in_array`, `array_keys`, `array_values` dispatch on the array type at compile time: -- `PhpType::Array` → use indexed runtime routines (e.g., bounds check, linear scan) -- `PhpType::AssocArray` → use hash table routines (e.g., `__rt_hash_get`, `__rt_hash_iter_next`) - -### `foreach` over associative arrays - -When `foreach` iterates a `PhpType::AssocArray`, the lowering differs from indexed arrays: - -1. Save the hash pointer and an iteration cursor on the stack (`0` means "start from header.head") -2. Call `__rt_hash_iter_next` -3. If `x0 == -1`, exit the loop -4. Otherwise save the returned cursor, store `x1`/`x2` into the optional key variable, and store `x3`/`x4`/`x5` into the value variable according to the inferred element type; `Mixed` loop variables reuse or allocate boxed mixed cells as needed -5. Emit the loop body, then branch back to the iterator call - -This preserves PHP-style insertion order because `__rt_hash_iter_next` walks the hash table's linked insertion-order chain rather than scanning physical buckets. - -See [The Runtime](the-runtime.md) for details on hash table routines and [Memory Model](memory-model.md) for the hash table memory layout. - -## String indexing codegen - -The same `ArrayAccess` AST node also covers string indexing such as `$str[1]` or `$str[-1]`. In `src/codegen/expr/arrays.rs`, `emit_array_access()` checks for `PhpType::Str` and lowers the operation inline: - -1. Save the string pointer/length while evaluating the index expression -2. Adjust negative indices relative to the end of the string -3. Clamp offsets below `-len` to the start and offsets past the end to the end -4. Advance the string pointer to the selected byte -5. Return either a one-character string (`x1` + `x2 = 1`) or an empty string when the offset is out of bounds - -So the behavior is slice-like, but it does not call `substr()` or a dedicated runtime helper. - -## Statement codegen - -**Files:** `src/codegen/stmt.rs`, `src/codegen/stmt/` - -`emit_stmt()` is similarly split across focused helpers under `stmt/`: assignment/storage logic, array statements, include-once guards, and control-flow lowering (`branching`, `foreach`, `loops`) now live outside the thin top-level dispatcher. `stmt/includes.rs` emits the `.comm` flag and branch sequence used by resolver-generated `IncludeOnceMark` and `IncludeOnceGuard` nodes, plus the active-variant store used when an include point loads a hidden function implementation. Small shared statement-side policies such as borrowed-result retention, local-slot ownership updates, static-init guards, and indexed-array metadata stamping now sit in `stmt/helpers.rs` instead of bloating `stmt.rs` itself. Storage lowering is now split too: `stmt/storage.rs` is just a boundary, with `storage/locals.rs` handling ordinary global/static symbol access and `storage/extern_globals.rs` owning extern-global load/store conventions. Assignment lowering is also split one level deeper: `stmt/assignments/locals.rs` handles plain local/global/ref writes, while `stmt/assignments/properties.rs` now orchestrates property writes across `properties/target.rs`, `magic_set.rs`, and `storage.rs`. Array-index writes follow the same pattern now: `stmt/arrays/assign.rs` is just a dispatcher, while `stmt/arrays/assign/buffer.rs` and `assoc.rs` isolate the non-indexed-container paths, and `stmt/arrays/assign/indexed.rs` now orchestrates the indexed-array write across `indexed/prepare.rs`, `normalize.rs`, `store.rs`, and `extend.rs`. Branching lowering now follows that same shape too: `stmt/control_flow/branching.rs` is just a boundary, while `branching/if_stmt.rs` and `branching/switch_stmt.rs` own the distinct lowering paths. Exception lowering follows the same structure: `stmt/control_flow/exceptions.rs` orchestrates the high-level try/catch/finally flow, while `exceptions/handlers.rs`, `catches.rs`, and `finally.rs` own the lower-level handler stack, catch matching, and pending-action/finally dispatch mechanics. Loop lowering is split too: `stmt/control_flow/loops.rs` is now just a boundary, with `loops/iterative.rs` handling `for`/`while`/`do...while` and `loops/exits.rs` owning `break`/`continue`/`return`. `foreach` lowering now follows the same pattern: `stmt/control_flow/foreach.rs` dispatches between `foreach/indexed.rs`, `foreach/assoc.rs`, and `foreach/iterator.rs` for arrays, hashes, `Iterator`, `IteratorAggregate`, and object-backed `iterable` values. - -### Statement AST dispatch coverage - -The statement dispatcher maps `StmtKind` variants to storage, control-flow, -declaration, include, or extension paths: - -| Variants | Lowering path | -|---|---| -| `Echo`, `ExprStmt`, `Throw`, `Synthetic` | Direct statement helpers, expression dispatch, exception throw, or already-lowered statement sequences | -| `Assign`, `RefAssign`, `TypedAssign`, `ArrayAssign`, `NestedArrayAssign`, `ArrayPush`, `ListUnpack`, `PropertyAssign`, `StaticPropertyAssign`, `PropertyArrayPush`, `PropertyArrayAssign`, `StaticPropertyArrayPush`, `StaticPropertyArrayAssign` | Local/global/static storage, reference aliasing, array storage, destructuring, and property storage helpers | -| `If`, `IfDef`, `While`, `DoWhile`, `For`, `Foreach`, `Switch`, `Try`, `Break`, `Continue`, `Return` | Branching, compile-time conditional lowering, loops, foreach dispatch, switch lowering, exception/finally control flow, loop exits, and return epilogues | -| `Include`, `IncludeOnceMark`, `IncludeOnceGuard`, `FunctionVariantGroup`, `FunctionVariantMark` | Resolver-produced include guards and include-loaded function variant activation | -| `NamespaceDecl`, `NamespaceBlock`, `UseDecl`, `ConstDecl` | Mostly frontend/name-resolution artifacts; constants remain available through the codegen context | -| `FunctionDecl`, `ClassDecl`, `EnumDecl`, `InterfaceDecl`, `TraitDecl`, `PackedClassDecl` | Deferred function/method emission and metadata-driven class, enum, interface, trait, and packed-record setup | -| `Global`, `StaticVar` | Symbol-backed local aliases and per-function static storage | -| `ExternFunctionDecl`, `ExternClassDecl`, `ExternGlobalDecl` | Registration-only at statement emission; expression/call lowering uses the collected FFI metadata | - -### Echo and print - -```php -echo $x; -echo "a", "b"; -$status = print $x; -``` - -1. Evaluate each `echo` expression in source order → result in registers -2. Check for null/false (skip printing if so — matches PHP behavior where `echo false` prints nothing) -3. Call `emit_write_stdout()` from the [ABI module](#the-abi-module) - -`print` expressions reuse the same stdout helper, then write integer `1` into -the expression result register so the value can be assigned, concatenated, or -passed into another expression. - -### Assignment - -```php -$x = expr; -``` - -1. Evaluate expression -2. If the result is a borrowed heap value, retain it before the local slot becomes a new owner -3. Release the previous owned heap value from `$x` when overwriting a heap-backed slot -4. `emit_store()` — write result to `$x`'s stack slot and classify the local slot as `Owned` for heap-backed types - -Typed local declarations such as `int $x = 42;` or `buffer $xs = buffer_new(8);` share the same storage path after the checker has resolved `StmtKind::TypedAssign` into a concrete `PhpType`. - -### Constant declaration - -```php -const MAX = 100; -``` - -`ConstDecl` registers a compile-time constant. The value is stored in the codegen context and substituted directly wherever the constant is referenced via `ConstRef`. No runtime storage or stack allocation is needed. - -### Global variables - -```php -$x = 10; -function inc() { - global $x; - $x++; -} -``` - -The `global` statement inside a function declares that a variable refers to global storage rather than a local stack slot. The codegen uses BSS-allocated storage (`_gvar_NAME`, 16 bytes each) for global variables: - -1. At `global $x;`: the variable is marked as global in the context. The current value is loaded from `_gvar_x` into the local stack slot. - The local view is tracked as a borrowed alias of the BSS-backed owner. -2. On assignment to a global variable: the codegen writes to the BSS storage (`_gvar_x`) via `adrp`/`add`/`str` instead of (or in addition to) the local stack slot. -3. In `_main`: when the main scope assigns to a variable that any function declares as `global`, the value is also written to `_gvar_NAME` so that functions can read it. - -### Extern declarations - -`ExternFunctionDecl`, `ExternClassDecl`, and `ExternGlobalDecl` are registration-only statements during codegen. Their metadata has already been collected by the type checker and copied into `Context`, so `emit_stmt()` treats the declarations themselves as no-ops while later expression codegen uses the recorded FFI data. - -Extern globals are loaded through GOT-relative addressing (`adrp ...@GOTPAGE` / `ldr ...@GOTPAGEOFF`) instead of ordinary stack or BSS slots. - -### Static variables - -```php -function counter() { - static $count = 0; - $count++; - echo $count; -} -``` - -Static variables persist their value across function calls. Each static variable gets two BSS slots: - -- `_static_FUNC_VAR` (16 bytes) — stores the persisted value -- `_static_FUNC_VAR_init` (8 bytes) — initialization flag (0 = not yet initialized) - -The codegen for `static $count = 0;`: - -1. Check the init flag — if already initialized, skip to loading the persisted value -2. If not initialized: evaluate the init expression, store to the BSS slot, set the init flag to 1 -3. Load the persisted value into the local stack slot - -That per-call local slot is tracked as `Borrowed`; the persisted static storage remains the long-lived owner. - -At function epilogue, variables marked as static are written back to their BSS storage. - -### Static properties - -Static properties use one global 16-byte storage slot per effective declaring class property: - -- `_static_prop_CLASS_PROP` stores the current value payload -- inherited static properties point back to the declaring class slot until a subclass redeclares the property -- redeclared static properties get a separate subclass slot - -At program startup, `_main` evaluates static property defaults and stores them into these slots before user statements run. Reads such as `ClassName::$count` load directly from the resolved symbol, and assignments store the new result back to the same symbol after type coercion and previous-value release for heap-backed values. `static::$count` uses the forwarded called-class id (or `$this` in instance methods) to select a redeclared descendant slot at runtime; if that late-bound slot is private and inaccessible from the current method scope, generated code emits a fatal private-static-property diagnostic. - -### List unpacking - -```php -[$a, $b, $c] = [10, 20, 30]; -``` - -Simple local positional destructuring remains a `ListUnpack` statement. The codegen: - -1. Evaluates the right-hand side expression (an array) -2. Saves the array pointer on the stack -3. For each variable in the list: loads the element at the corresponding index from the array, stores it into the variable's stack slot, and marks heap-backed elements as borrowed aliases of the source container - -Richer PHP destructuring patterns are lowered by the parser into ordinary synthetic assignments before checking and codegen. Skipped entries simply emit no assignment, keyed entries become array reads with the given key, nested patterns bind a hidden temporary for the nested source array, and non-local targets reuse the same assignment emitters as `$arr[$i] = ...`, `$arr[] = ...`, `$obj->prop = ...`, and static-property writes. - -### If / Elseif / Else - -```php -if ($cond1) { body1 } elseif ($cond2) { body2 } else { body3 } -``` - -```asm -; evaluate $cond1 -cmp x0, #0 -b.eq _elseif_1 ; skip to next branch if falsy - -; body1 -b _end_if_1 ; done — skip all remaining branches - -_elseif_1: -; evaluate $cond2 -cmp x0, #0 -b.eq _else_1 - -; body2 -b _end_if_1 - -_else_1: -; body3 - -_end_if_1: -``` - -### While loop - -```php -while ($cond) { body } -``` - -```asm -_while_1: ; ← continue jumps here -; evaluate $cond -cmp x0, #0 -b.eq _end_while_1 ; exit if falsy ← break jumps here - -; body -b _while_1 ; loop back - -_end_while_1: -``` - -### For loop - -```php -for ($i = 0; $i < 10; $i++) { body } -``` - -```asm -; emit init ($i = 0) - -_for_1: -; evaluate condition ($i < 10) -cmp x0, #0 -b.eq _end_for_1 - -; body - -_for_cont_1: ; ← continue jumps here -; emit update ($i++) -b _for_1 - -_end_for_1: ; ← break jumps here -``` - -### Foreach - -```php -foreach ($arr as $v) { body } -``` - -For indexed arrays: - -1. Save array pointer, length, and index counter on the stack (3 × 16-byte slots) -2. Loop: load element at current index, unbox through the runtime `value_type` tag when the static element type is `Mixed`, store to `$v`, and classify heap-backed loop variables as borrowed aliases of the iterated container -3. Branch back to condition check -4. Cleanup: deallocate the 48 bytes - -For associative arrays, see [Associative array codegen](#associative-array-codegen): the loop stores a hash pointer plus cursor, then advances with `__rt_hash_iter_next`. - -For `Iterator` objects, codegen parks the receiver in a 16-byte stack slot, dispatches `rewind()`, then drives the loop through `valid()`, `key()`, `current()`, and `next()`. Keys and values are boxed into `Mixed` because the concrete runtime payload can vary per iterator implementation. `IteratorAggregate` values dispatch `getIterator()` first, then reuse the same iterator loop path. Values typed as `iterable` branch through runtime heap-kind and interface metadata so arrays, direct `Iterator` objects, and aggregate-backed objects select the correct lowering. - -Before the first `valid()` call, foreach target slots are normalized to boxed `Mixed`. That keeps empty iterators compatible with PHP: existing target variables keep a valid mixed cell, fresh loop variables remain null-like, and receiver aliases stay live until loop cleanup. - -### Break / Continue - -`break` emits a `b` (unconditional jump) to the selected loop/switch end label. -`continue` emits a `b` to the selected continue label (the condition check for -`while`, the update for `for`, or the switch end label for PHP-style -`continue` inside `switch`). - -The `loop_stack` in the Context tracks labels for nested loops and switches. -Multi-level forms such as `break 2;` and `continue 2;` index back through that -stack. Each `LoopLabels` entry also carries an `sp_adjust` field so multi-level -exits and returns can undo any skipped switch-subject temporary stack slots -before jumping to the selected target or shared function epilogue. If the exit -crosses a `finally`, codegen records the selected target and runs the active -`finally` chain before resuming the branch. - -The type checker rejects `break` / `continue` that would jump out of a -`finally` body, so codegen only has to route legal exits from protected `try` or -`catch` bodies through `finally_stack`. - -### Exceptions and `finally` - -Exception lowering lives in `src/codegen/stmt/control_flow/exceptions.rs`. The basic strategy is: - -1. Evaluate the thrown object and publish it to `_exc_value` -2. Call `__rt_throw_current`, which unwinds activation records and `longjmp`s into the nearest handler -3. For `try`, emit a `_setjmp` resume point plus a linked handler record in `_exc_handler_top` -4. Test each catch target by class id or interface id through `__rt_exception_matches` -5. Route `return`, `break`, `continue`, and rethrow through `finally_stack` so every enclosing `finally` runs before control leaves the protected region. The checker rejects `break` / `continue` that would originate inside a `finally` and target an outer loop/switch. - -This means `finally` is part of ordinary control-flow lowering, not a separate runtime pass. The runtime only unwinds frames and chooses the landing pad; the compiler-generated labels still decide whether execution resumes in a matching `catch`, in a `finally`, or in an outer handler. - -### Switch - -```php -switch ($x) { - case 1: echo "one"; break; - case 2: echo "two"; break; - default: echo "other"; break; -} -``` - -1. Evaluate the subject expression once and push the result onto the stack -2. For each case: pop subject, evaluate case value, compare (`cmp` + `b.ne` for integers, `bl __rt_str_eq` for strings) -3. If match: emit case body, which may contain `break` (jump to end label) or fall through to next case -4. Default case: emit body unconditionally -5. End label after all cases - -The switch uses the loop stack so that `break` inside a case body jumps to the switch end label rather than an enclosing loop. - -### Match expression - -Match is an expression (returns a value), not a statement. It uses strict comparison (`===`) and has no fall-through: - -```php -$result = match($x) { - 1 => "one", - 2 => "two", - default => "other", -}; -``` - -1. Evaluate subject, push onto stack -2. For each arm: compare subject with each pattern in the arm's pattern list -3. If any pattern matches: evaluate the arm's result expression, jump to end -4. Default arm: evaluate result unconditionally -5. Result is left in standard registers (`x0`, `d0`, or `x1`/`x2`) - -## Class codegen - -### Object allocation (`new ClassName(...)`) - -When the codegen encounters a `NewObject` expression: - -1. **Calculate object size**: `8 + (num_properties × 16) + dyn_props_slot` — 8 bytes for the class ID, 16 bytes per property across the full inherited layout, plus one optional 8-byte slot for the dynamic-property hash pointer when the class carries `#[\AllowDynamicProperties]` -2. **Allocate heap memory**: call `__rt_heap_alloc` with the calculated size -3. **Zero-initialize**: clear all property slots to zero -4. **Store class ID**: write the class identifier at offset 0 -5. **Apply defaults**: for properties with default values, evaluate and store them at their fixed offsets -6. **Call constructor**: if the class exposes `__construct`, pass the new object pointer as `x0` (`$this`) followed by the constructor arguments, then branch to the implementation label recorded in class metadata (which may come from an inherited constructor) - -Classes declared with the PHP 8.2 `#[\AllowDynamicProperties]` attribute reserve a trailing per-object hash slot so undeclared property writes/reads can be routed through a runtime side table instead of failing at compile time. - -The result is the object pointer in `x0`. - -### Attribute reflection objects - -`new ReflectionClass(...)`, `new ReflectionMethod(...)`, and `new ReflectionProperty(...)` are intercepted by `src/codegen/expr/objects/reflection.rs` instead of relying on ordinary user-defined constructor bodies. The type checker has already forced their class/member arguments to compile-time strings after normal call-argument planning, so codegen can look up the target `ClassInfo` directly and populate the private `__attrs` slot with a freshly built `array`. - -`src/codegen/reflection.rs` owns the shared materialization path. It allocates each synthetic `ReflectionAttribute`, writes the resolved `__name`, builds the `array` `__args` payload from supported literal attribute arguments, and stores a deterministic `__factory` id. `ReflectionAttribute::newInstance()` is then generated in `src/codegen/class_methods.rs` as a branch table over those factory ids; each branch constructs the real attribute class with the captured literal args, and the fallback returns `null` when no defined attribute class can be materialized. - -The `_class_attribute_*` runtime data tables still emit class-level attribute metadata from the same `ClassInfo` fields, but the supported Reflection owner constructors are compile-time materialized and do not perform runtime name lookups for classes, methods, or properties. - -### Type checks (`$obj instanceof ClassName`) - -`ExprKind::InstanceOf` evaluates the left-hand side exactly once, materializes the target class or interface id from emitted metadata, and returns a boolean in `x0`. Direct object values call `__rt_exception_matches`, the same metadata matcher used by exception catch lowering, so inherited classes and implemented interfaces are handled through the same parent-id and class-interface tables. - -For named targets, when the left-hand side is lowered as `Mixed` or `Union`, codegen calls `__rt_mixed_instanceof` instead. That helper unwraps nested mixed boxes, returns `false` for scalar, array, null, and unknown payload tags, and only forwards object payloads into `__rt_exception_matches`. This keeps nullable and union object checks PHP-compatible without treating the boxed mixed cell itself as an object pointer. - -Named targets are resolved before codegen. Named classes/interfaces become concrete metadata ids, `self` and `parent` resolve in the current lexical class context, and `static` uses the forwarded called-class id for late static binding. Dynamic targets are evaluated and validated after the left-hand side is evaluated; string targets are resolved through emitted case-insensitive class/interface name metadata, object targets load the target object's runtime class id, invalid target payloads branch to a fatal runtime diagnostic, and non-object left-hand payloads become `false` after that validation step. - -### Property access (`$obj->prop`) - -Property access usually uses fixed offsets computed at compile time from `ClassInfo.property_offsets`: - -```asm -; $obj->prop where prop resolved to offset 24 -ldur x0, [x29, #-offset] ; load object pointer -ldur x0, [x0, #24] ; load property at resolved inherited offset -``` - -If the property does not exist but the class exposes `__get($name)`, codegen materializes the property name as a string literal, pushes it as an argument, and dispatches the instance method through the normal object-call path. The returned value then flows back through the ordinary result registers based on the inferred return type. - -For property assignment (`$obj->prop = value`), the value is evaluated first, then stored at the resolved inherited offset. If the property is missing but the class exposes `__set($name, $value)`, codegen boxes the value as `Mixed`, materializes the property name, and dispatches `__set` instead of emitting a direct store. - -Property-array writes use the same fixed-offset property resolution first, then delegate to the ordinary array storage paths for the nested container. `$obj->items[] = $value` lowers through `PropertyArrayPush`, and `$obj->items[$key] = $value` lowers through `PropertyArrayAssign`; both require a concrete array/assoc-array property rather than a magic `__set` fallback. - -### Method call (`$obj->method(args)`) - -1. Evaluate the object expression to get the pointer in `x0` -2. Push the object pointer onto the stack -3. Evaluate and push all arguments -4. Pop arguments into ABI registers, with the object pointer as the first argument (`x0`) -5. Load the object's `class_id`, fetch the class vtable pointer from `_class_vtable_ptrs`, load the method slot, and `blr` to the resolved implementation -6. Result is in the standard registers based on return type - -Inside the method body, `$this` is the first parameter and lives in the function's first stack slot. - -Private instance methods are the exception: they do not get vtable slots, so calls resolved to a private method of the current lexical class use a direct `_method_Class_method` branch instead of virtual dispatch. - -### Static method call (`ClassName::method(args)`) - -Static methods are called like regular functions, but with the label `_static_ClassName_methodName`. No object pointer is passed: - -```asm -bl _static_Point_origin ; call static method -; result in x0 (object pointer) -``` - -`self::method()` is handled as a direct call against the current lexical class. If it resolves to an instance method, codegen loads the implicit `$this` receiver and branches directly to the resolved `_method_Class_method` label. `parent::method()` works the same way against the immediate parent class. For static targets, codegen now also threads a hidden "called class id" argument through static method bodies: named `ClassName::method()` calls pin that id to the named class, while `self::` and `parent::` forward the current called class. `static::method()` then uses that forwarded class id to load the target from a per-class static-method table at runtime. - -## The ABI module - -**Files:** `src/codegen/abi/mod.rs`, `src/codegen/abi/` - -Centralizes register conventions so they're consistent everywhere: - -### Large offset addressing - -ARM64's `stur`/`ldur` instructions only support 9-bit signed immediates (offsets up to 255). Functions with many local variables can exceed this limit. The ABI module handles this transparently via `store_at_offset()` and `load_at_offset()`: - -- **Offsets <= 255**: single `stur`/`ldur` instruction (fast path) -- **Offsets 256-4095**: two-instruction sequence — `sub x9, x29, #offset` to compute the address in a scratch register, then `str`/`ldr` through that register - -This means all codegen that accesses stack variables goes through the ABI helpers rather than emitting `stur`/`ldur` directly, so large stack frames work automatically. The same boundary now also owns indirect `[*ptr]` loads/stores used by by-reference params and mutation-heavy expression paths, so x86_64-specific memory syntax does not leak back into `expr.rs`. - -`emit_frame_slot_address()` complements those helpers when codegen needs the address of a local slot itself rather than the value stored there. By-reference calls, `ptr($var)`, and exception-frame bookkeeping now all reuse that helper instead of open-coding frame-slot address math. - -### Frame and return-value helpers - -The `abi/` module now centralizes the frame-management primitives used by both `_main` and ordinary functions: - -- `emit_frame_prologue()` / `emit_frame_restore()` — shared stack-frame setup and teardown -- `emit_cleanup_callback_prologue()` / `emit_cleanup_callback_epilogue()` — tiny helper frames used by exception cleanup callbacks -- `emit_preserve_return_value()` / `emit_restore_return_value()` — spill/reload of scalar, float, and string returns across epilogue side effects or `finally` dispatch - -That moves prologue/epilogue mechanics out of the higher-level walkers and makes the ABI layer responsible for more than just local-slot addressing. - -### Incoming argument lowering - -Incoming parameter decoding now goes through `IncomingArgCursor` plus `emit_store_incoming_param()`. - -The cursor tracks: - -- current integer argument register index -- current floating-point argument register index -- when argument passing has overflowed to the caller stack -- the caller-stack byte offset for subsequent spilled parameters - -Those helpers now understand both the AArch64 calling convention and the Linux `x86_64` SysV AMD64 target. Function codegen delegates incoming-parameter lowering to the ABI layer instead of open-coding register names or caller-stack offsets inline. - -### Outgoing call argument lowering - -Outgoing calls now use ABI-owned helpers as well: - -- `build_outgoing_arg_assignments_for_target()` decides whether each argument lands in an integer register, a floating-point register, or overflows onto the caller-visible stack area for the selected target -- `materialize_outgoing_args()` rewrites the temporary pushed-argument stack into the final ABI layout expected at the call site - -That logic is shared by ordinary function calls, indirect/callable dispatch, object/method calls, constructor/static dispatch, and helpers such as `call_user_func_array()`. The assignment/materialization rules now cover both AArch64 and Linux `x86_64` SysV layout, so the call ABI policy lives in one place instead of being duplicated across several dispatch paths. - -The same module now also owns a thin layer of call-site and temporary-stack primitives used by higher-level walkers: - -- `emit_call_label()` / `emit_call_reg()` emit direct and indirect calls for the current target -- `emit_push_reg()`, `emit_pop_reg()`, `emit_push_float_reg()`, `emit_pop_float_reg()`, `emit_push_reg_pair()`, `emit_pop_reg_pair()`, and `emit_push_result_value()` manage the temporary argument stack without hardcoding ARM64 push/pop forms in each call path -- `emit_reserve_temporary_stack()`, `emit_temporary_stack_address()`, and `emit_load_temporary_stack_slot()` now also back the FFI extern-call path, where borrowed C-string temporaries are tracked and released after the foreign call returns -- `emit_release_temporary_stack()` and `emit_store_zero_to_local_slot()` centralize target-specific stack cleanup and zero-initialization details -- `emit_store_process_args_to_globals()`, `emit_enable_heap_debug_flag()`, `emit_copy_frame_pointer()`, and `emit_exit()` cover the `_main` bootstrap/teardown path without hardcoding process-entry registers or exit sequences in the higher-level driver - -That keeps target-specific ABI work focused inside `abi/` instead of scattering `call`, `blr`, `add sp`, `rsp`, or zero-register assumptions across function, closure, callable, and method dispatch code. - -The same `abi/` layer now also owns symbol-slot plumbing for compiler-managed globals such as `_gvar_*`, `_static_*`, `_exc_*`, `_global_*`, and the high-frequency runtime symbols used by string builders, heap bookkeeping, and GC state such as `_concat_off`, `_heap_*`, and `_gc_*`: computing symbol addresses, moving result registers into symbol storage, loading symbol storage back into result registers, and copying local frame slots into symbol-backed storage during epilogues. Extern globals now use the same boundary too, so GOT/GOTPCREL address materialization lives in `abi/` instead of being open-coded separately in expression and statement lowering. - -### `emit_store(emitter, type, offset)` - -Stores the current result to a stack variable. Uses `store_at_offset()` internally to handle large offsets: - -| Type | What it stores | -|---|---| -| `Int` / `Bool` / `Resource` | `stur x0, [x29, #-offset]` (or 2-insn sequence for large offsets) | -| `Float` | `stur d0, [x29, #-offset]` | -| `Str` | `bl __rt_str_persist`, then `stur x1, [x29, #-offset]` + `stur x2, [x29, #-(offset-8)]` | -| `Array` / `AssocArray` / `Iterable` | `stur x0, [x29, #-offset]` | -| `Mixed` | `stur x0, [x29, #-offset]` | -| `Object` | `stur x0, [x29, #-offset]` | -| `Callable` / `Pointer` | `stur x0, [x29, #-offset]` | -| `Buffer` / `Packed` / `Union` | `stur x0, [x29, #-offset]` | - -### `emit_load(emitter, type, offset)` - -Loads a stack variable into result registers (inverse of store). Uses `load_at_offset()` internally. - -### `emit_write_stdout(emitter, type)` - -Emits code to print a value to stdout: - -| Type | How it prints | -|---|---| -| `Str` | move the string pointer/length into `__rt_stdout_write`'s convention, then `bl __rt_stdout_write` | -| `Int` | `bl __rt_itoa` → then write | -| `Float` | `bl __rt_ftoa` → then write | -| `Bool` | `true` prints "1", `false` prints nothing | -| `Pointer` | `bl __rt_ptoa` → then write | -| `Mixed` | `bl __rt_mixed_write_stdout` → inspect boxed runtime tag, then write | -| `Void`/`Array`/`AssocArray`/`Callable`/`Object` | Prints nothing | - -The terminal write itself goes through one shared runtime indirection, `__rt_stdout_write(ptr, len)` (byte pointer in `x0`/`rdi`, length in `x1`/`rsi`). It performs the platform `write(1, ptr, len)` syscall directly. In `--web` builds it first checks the `_elephc_web_capture` flag and, when capture is enabled, hands the bytes to `elephc_web_write` instead so per-request response bodies can be captured; non-web binaries never reference the web symbols. (The `Mixed` / `Resource` / `Iterable` writers still issue their own syscalls and bypass this indirection.) - -For Linux `x86_64`, the same write path now follows the SysV ABI and a broad native runtime slice rather than AArch64-specific helper sequences. String results use the Linux syscall register layout, integer and float echo go through x86_64 `__rt_itoa` / `__rt_ftoa`, `_main` initializes `$argc` / `$argv` only when needed, and the bootstrap runtime now covers a wide set of array, string, math, filesystem, FFI, enum, exception, GC, and mixed-value helpers without leaking AArch64-only assumptions back into the higher-level walkers. - -That same bootstrap system slice now also includes x86_64-native `time()` / `microtime(true)` through libc `gettimeofday()`, target-aware `php_uname()` through libc `uname()`, plus package-version lowering for `phpversion()` and constant-string lowering for `sys_get_temp_dir()` via the shared symbol-address ABI helpers instead of ARM64-only `adrp` / `add_lo12` sequences. - -The x86_64 math surface is broader now too: the libc-backed float builtin family (`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `exp`, `log`, `log2`, `log10`, `atan2`, `hypot`, `pow`) and the pure float helpers (`sqrt`, `pi`, `deg2rad`, `rad2deg`, `min`, `max`) all use SysV floating-point registers plus the shared temporary-stack ABI helpers instead of raw AArch64 `d0` / `scvtf` / `str d0` lowering. The same applies to the `**` operator in expression codegen, which now routes through the x86_64 `pow()` libc call path with the right floating argument order. The scalar random helpers (`rand()`, `mt_rand()`, `random_int()`) also live on that target-aware ABI path now, so their `[min, max]` range materialization no longer emits raw AArch64 stack spills on Linux x86_64. Comparator-driven indexed-array sorting is on that same path too: `usort()`, `uasort()`, and `uksort()` now resolve callback addresses through the shared symbol/stack ABI helpers and dispatch through an x86_64 `__rt_usort` bubble-sort runtime instead of hard-coded ARM64 `adrp` / `blr` sequences. - -## Function codegen - -**Files:** `src/codegen/functions/mod.rs`, `src/codegen/functions/` - -### `emit_function()` - -Compiles a user-defined function: - -1. **Collect local variables** — scan the function body to find all variables and their types -2. **Calculate stack frame size** — 16-byte aligned, includes space for all locals -3. **Emit prologue** — call the shared ABI frame helper -4. **Store parameters** — lower incoming arguments through the ABI helpers into stack slots, marking by-value heap params as `Owned` and by-reference params as borrowed aliases of the caller's storage -5. **Emit body** — all statements -6. **Emit epilogue** — preserve return registers, save static locals back to BSS through the shared ABI storage helpers, clean up only `Owned` + `epilogue_cleanup_safe` heap locals, then call the shared ABI frame-restore helper and `ret` - -### Pass by reference - -```php -function increment(&$val) { - $val++; -} -``` - -When a parameter is declared with `&`, the codegen passes the **stack address** of the argument instead of its value: - -1. At the call site: the address of the argument's stack slot is computed (`sub x_n, x29, #offset`) and passed in the argument register. -2. In the function prologue: the address is stored in the parameter's stack slot (it holds a pointer, not a value). -3. On reads: the codegen dereferences the pointer (`ldr x0, [x0]`) to get the actual value. -4. On writes: the codegen stores through the pointer (`str x0, [addr]`), modifying the caller's variable directly. - -The context tracks which parameters are pass-by-reference via `ctx.ref_params`. - -### Variadic parameters and spread operator - -```php -function sum(...$nums) { /* $nums is an array */ } -sum(1, 2, 3); -sum(...$arr); // spread -``` - -**Variadic functions**: The last parameter can be prefixed with `...` to collect all remaining arguments into an array. At the call site, the codegen: - -1. Passes regular (non-variadic) arguments normally via registers -2. Uses the shared helpers in `src/codegen/expr/calls/args.rs` to prepare normalized/defaulted argument lists, lower pass-by-reference slots, handle spread-into-named parameters, and build the trailing variadic array when needed -3. Passes the array pointer as the last argument register - -**Spread operator** (`...$arr`): When calling a function with `...$arr`, the array is unpacked into positional parameters. For `function f($a, ...$rest)`, `f(...[1, 2, 3])` passes `1` to `$a` and collects `[2, 3]` into `$rest`. Associative-array spreads map string keys to named arguments, keep numeric keys positional, and collapse duplicate static string keys to the last value before planning. Variable `AssocArray` spreads before named arguments can satisfy later parameters by string key at runtime, so codegen skips fixed prefix length checks for that dynamic provider and emits per-parameter lookup/default handling instead. In array literals, the spread operator uses `__rt_array_merge_into` to append all elements from the spread array into the target array. - -### Default parameter values - -Functions and closures support default parameter values: - -```php -function greet($name, $greeting = "Hello") { ... } -``` - -When a call site omits an argument that has a default value, the codegen fills in the default. At the call site, the compiler checks how many arguments were actually passed and, for each missing parameter with a default, evaluates the default expression and places it in the appropriate argument register. This is handled at compile time — no runtime checks are needed. - -### `collect_local_vars()` - -Pre-scans the function body AST to find every variable that will be used. This is necessary because stack space must be allocated in the prologue, before any code runs. - -It walks the statement tree before code emission and handles the major local-binding forms recursively (`Assign`, control-flow blocks, `For`/`Foreach`, `ListUnpack`, `Global`, `StaticVar`, and related cases). The exact match is implementation-driven in the `functions/` module, so this list is illustrative rather than exhaustive. - -## Main program codegen - -**File:** `src/codegen/mod.rs` - -The `generate()` function orchestrates everything: - -1. **Emit user functions** — scan AST for `FunctionDecl`, emit each one -2. **Emit class methods** — constructor, instance methods, and static methods use their own labels -3. **Emit `_main`**: - - Prologue (stack frame for global variables) - - Save `argc` and `argv` from OS (they arrive in `x0` and `x1`) - - Build `$argv` array via `__rt_build_argv` runtime call - - Register the main activation record so exceptions can unwind through top-level code too - - Emit all non-function statements - - Epilogue: clean up owned locals, unregister the activation record, then `exit(0)` -4. **Emit deferred closures** — closure bodies recorded during earlier expression codegen -5. **Emit runtime routines** — all `__rt_*` helper functions -6. **Emit data section** — string and float literals -7. **Emit runtime data / BSS** — global buffers, globals, statics, and lookup tables - -Linux x86_64 uses the same shared runtime emission surface as the AArch64 targets. Array transforms, sorting helpers, copy-on-write paths, GC accounting, heap debug helpers, string search/formatting helpers, inline array/string accessors, and list unpacking all route through target-aware emitters and the ABI module rather than a separate reduced runtime slice. - -When an operation needs architecture-specific assembly, the leaf runtime module selects the native sequence internally. For example, x86_64 helpers use SysV registers, RIP-relative addressing, and x86_64 heap markers where needed, while AArch64 helpers use their own register and relocation conventions. Higher-level lowering should continue to call the shared runtime labels and ABI helpers instead of branching on raw ARM64 or x86_64 details. +| `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_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 | +| `src/codegen/value_placement.rs` | Stack/register placement for EIR values | +| `src/codegen_support/abi/` | Target ABI helpers for registers, calls, stack slots, symbols, and frame mechanics | +| `src/codegen_support/arrays.rs` | Shared array metadata helpers used by EIR and runtime support | +| `src/codegen_support/callable_invoker_args.rs` | Shared descriptor-invoker argument cloning and boxing helpers | +| `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/platform/` | Target descriptions and assembler/linker naming conventions | + +The active backend must remain target-aware. New lowering paths should use the +ABI helpers instead of hardcoding AArch64 or x86_64 register and stack details. + +## Runtime Split + +Codegen produces two linked 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. + +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. + +## Emit Modes + +`--emit executable` is the default and emits a process entry point. `--emit +cdylib` emits a PIC user object with `#[Export]` trampolines and lifecycle +symbols for embedding hosts. On Linux cdylib output also hides internal runtime +symbols so separate loaded elephc modules do not preempt each other's state. + +## Backend Contract + +- PHP-visible behavior belongs in `src/ir_lower/` and `src/codegen/`. +- Shared runtime, ABI, platform, and metadata helpers belong in + `src/codegen_support/`. diff --git a/docs/internals/the-ir.md b/docs/internals/the-ir.md index 5412720106..28a40ed833 100644 --- a/docs/internals/the-ir.md +++ b/docs/internals/the-ir.md @@ -5,10 +5,10 @@ sidebar: order: 13 --- -**Status:** EIR is the default user-facing backend. The diagnostic `--emit-ir` -path lowers the checked and optimized AST into validated textual EIR, and the -normal executable/cdylib path lowers that same EIR into assembly. The legacy -AST backend remains available only as the temporary `--ast-backend` fallback. +**Status:** EIR is the canonical compiler IR and backend contract for v1.0. +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` @@ -23,13 +23,9 @@ AST backend remains available only as the temporary `--ast-backend` fallback. - `src/parser/ast/ffi.rs` - `src/ir/` - `src/ir_lower/` -- `src/codegen_ir/` -- `src/codegen/expr.rs` and `src/codegen/expr/` -- `src/codegen/stmt.rs` and `src/codegen/stmt/` -- `src/codegen/functions/locals.rs` -- `src/codegen/context.rs` -- `src/codegen/builtins/` -- `src/codegen/runtime/emitters.rs` +- `src/codegen/` +- `src/codegen_support/abi/` +- `src/codegen_support/runtime/emitters.rs` - `src/optimize/effects.rs` and `src/optimize/effects/` EIR is elephc's intermediate representation. It sits between the AST-level @@ -42,27 +38,6 @@ descriptors, copy-on-write checks, fatal paths, exception paths, runtime calls, and exact source evaluation order are first-class compiler concepts. EIR is not a generic LLVM- or Cranelift-style IR. -## Historical Design Boundary - -The first roadmap item originally covered this document only: - -```text -EIR design specification (`docs/internals/the-ir.md`) - types, instructions, -terminators, effects, ownership, textual format -``` - -That first item did **not** include: - -- EIR -> assembly backend -- `--ir-backend` -- register allocation -- IR optimization passes - -That design-only milestone is complete. The current implementation has since -added `src/ir/`, `src/ir_lower/`, `--emit-ir`, and the default EIR assembly -backend under `src/codegen_ir/`. Register allocation and IR optimization passes -remain follow-up work. - ## Pipeline Position Current default production path: @@ -88,45 +63,20 @@ PHP source -> binary ``` -Temporary legacy fallback path (`--ast-backend`): - -```text -PHP source - -> Lexer - -> Parser - -> Magic constants - -> Conditional compilation - -> Resolver - -> NameResolver - -> Autoload insertion - -> AST constant folding - -> Type checker / warnings - -> AST optimizer passes - -> AST -> assembly codegen - -> runtime cache - -> assembler / linker - -> binary -``` - The AST optimizer remains in front of EIR. It handles PHP-preserving rewrites that are naturally expressed over syntax: constant folding, local scalar propagation, control-flow pruning, control-flow normalization, and DCE. EIR adds what the AST cannot express well: value identity, basic blocks, block parameters, liveness, dominance, register placement, CSE, LICM, and inlining. -## Backend Selection - -The compiler currently supports two assembly backends: +## Backend Contract -- EIR backend, selected by default and also by explicit `--ir-backend` -- legacy AST backend, selected only by `--ast-backend` +The compiler lowers EIR through the target-aware assembly emitter in +`src/codegen/`. -`--ast-backend` is a deprecated escape hatch while the legacy emitter remains -in-tree. It emits a warning and will be removed after the default EIR path has -completed its validation window. EIR preserves the existing hand-written assembly -style and adds linear-scan register allocation plus a fixed-point IR optimization -pass driver (see [Optimization Passes](#optimization-passes)); further IR passes -are incremental follow-up work. +EIR preserves the hand-written assembly style while adding linear-scan register +allocation plus a fixed-point IR optimization pass driver (see +[Optimization Passes](#optimization-passes)). ## Design Invariants @@ -138,7 +88,7 @@ are incremental follow-up work. generic "call". - EIR is target-aware through metadata and the selected `Target`, but it does not hardcode ARM64/x86_64 register names or stack layouts. -- `src/codegen/abi/` remains authoritative for physical ABI lowering. +- `src/codegen_support/abi/` remains authoritative for physical ABI lowering. - Runtime helpers remain outside the EIR `Module`; EIR references them through `RuntimeCall` or specialized opcodes. - Source spans must survive lowering for diagnostics, source maps, and `--emit-ir` @@ -1456,7 +1406,7 @@ The pass has four stages: active set; a free register from the matching pool is assigned, otherwise the use-weighted spill heuristic evicts the cheapest interval. Integer and float values draw from separate pools. -4. **Frame integration** (`codegen_ir/frame.rs`): the allocation is stored in +4. **Frame integration** (`codegen/frame.rs`): the allocation is stored in the frame layout, each used callee-saved register gets a save slot, and the value-access chokepoints (`load_value_to_result`, `load_value_to_reg`, `store_result_value`) read and write registers instead of slots. diff --git a/docs/internals/the-optimizer.md b/docs/internals/the-optimizer.md index 623cf55e20..9059b85ab5 100644 --- a/docs/internals/the-optimizer.md +++ b/docs/internals/the-optimizer.md @@ -416,7 +416,6 @@ The current optimizer is still intentionally local. It does not yet implement: - broader guard reasoning for range facts and multi-variable relationships beyond the current boolean, scalar, loose-comparison, and safe relational-complement facts - broader control-flow normalization beyond the current local AST shell rewrites - backend-specific peephole cleanup -- runtime dead stripping - elimination of the `adrp/add/stur` instruction triple at the FCC assignment site when the wrapper is stubbed (the stub address still gets loaded and stored even though both are dead) Those remain roadmap items for later optimization work. diff --git a/docs/internals/the-parser.md b/docs/internals/the-parser.md index 412eefed49..eb5da2f4c0 100644 --- a/docs/internals/the-parser.md +++ b/docs/internals/the-parser.md @@ -73,7 +73,7 @@ Things that have a value: | `ShortTernary { value, default }` | `$a ?: $fallback` | PHP short ternary / Elvis form. Codegen evaluates `value` once, returns it if truthy, otherwise returns `default`. | | `ErrorSuppress(Expr)` | `@file_get_contents("missing.txt")` | PHP error-control prefix expression. Codegen wraps the operand in a runtime warning-suppression scope. | | `Cast { target, expr }` | `(int)$x` | | -| `Closure { params, variadic, variadic_type, return_type, body, is_arrow, is_static, captures, capture_refs }` | `function(int $x = 1) use ($y, &$z): string { ... }`, `fn(int $x): int => $x * 2`, or `static function(): int { ... }` | Anonymous function / arrow function. Params is `Vec<(String, Option, Option, bool)>` - name, declared type, default, is_ref. `variadic` is an optional parameter name and `variadic_type` its optional declared element type (`int ...$xs`). `return_type` stores the optional declared closure / arrow return `TypeExpr`. `captures` stores by-value captures and `capture_refs` stores `use (&$var)` captures. Arrow functions are still represented as `Closure`, parse with `is_arrow = true`, and do not carry explicit `use (...)` captures in the AST. `is_static` is set when the closure is prefixed with the `static` keyword (PHP `static function () {}` / `static fn () => ...`); the type checker rejects any reference to `$this` inside a static closure. | +| `Closure { params, variadic, variadic_type, return_type, body, is_arrow, is_static, by_ref_return, captures, capture_refs }` | `function(int $x = 1) use ($y, &$z): string { ... }`, `fn(int $x): int => $x * 2`, or `static function(): int { ... }` | Anonymous function / arrow function. Params is `Vec<(String, Option, Option, bool)>` - name, declared type, default, is_ref. `variadic` is an optional parameter name and `variadic_type` its optional declared element type (`int ...$xs`). `return_type` stores the optional declared closure / arrow return `TypeExpr`. `captures` stores by-value captures and `capture_refs` stores `use (&$var)` captures. Arrow functions are still represented as `Closure`, parse with `is_arrow = true`, and do not carry explicit `use (...)` captures in the AST. `is_static` is set when the closure is prefixed with the `static` keyword (PHP `static function () {}` / `static fn () => ...`); the type checker rejects any reference to `$this` inside a static closure. `by_ref_return` is set when the closure is declared `fn &()` / `function &()`, so it returns a reference (alias) to the returned lvalue rather than a copy. | | `NamedArg { name, value }` | `foo(name: "Alice")` | Named call argument. The parser preserves source order; later phases validate names against the declared parameter list and normalize known-signature calls for ABI lowering. | | `ClosureCall { var, args }` | `$fn(1, 2)` | Calling a closure stored in a variable | | `ExprCall { callee, args }` | `$arr[0](1, 2)` | Calling the result of an expression (e.g., array access returning a callable) | @@ -110,7 +110,7 @@ Each `Stmt` also carries a source `span` and an `attributes` list. The list is p |---|---| | `Echo(Expr)` | `echo $x;`; multi-argument `echo $a, $b;` lowers to a `Synthetic` sequence of `Echo` statements | | `Assign { name, value }` | `$x = 42;` | -| `RefAssign { target, source }` | `$y =& $x;` — reference aliasing where both target and source are plain variables | +| `RefAssign { target, source }` | `$y =& $x;`, `$y =& $obj->prop;`, `$y =& $arr[$k];` — reference aliasing where `target` is a plain variable name and `source` is the aliased lvalue: a plain variable, a property access, an array element, or a call to a by-reference-returning callee | | `If { condition, then_body, elseif_clauses, else_body }` | `if (...) { } elseif (...) { } else { }` | | `While { condition, body }` | `while (...) { }` | | `DoWhile { body, condition }` | `do { } while (...);` | @@ -121,7 +121,7 @@ Each `Stmt` also carries a source `span` and an `attributes` list. The list is p | `NestedArrayAssign { target, value }` | `$arr[0][1] = 5;`, `$obj->items[0] = 5;` | | `ArrayPush { array, value }` | `$arr[] = 5;` | | `TypedAssign { type_expr, name, value }` | `int $x = 42;`, `buffer $xs = buffer_new(8);` | -| `FunctionDecl { name, params, variadic, return_type, body }` | `function foo(int $a, &$b, string $c = "x"): string { }` — params is `Vec<(String, Option, Option, bool)>` where the tuple stores name, declared type, default value, and `is_ref` (pass by reference). `variadic` is `Option` for variadic parameters (`...$args`) and `return_type` is an optional declared `TypeExpr` | +| `FunctionDecl { name, params, variadic, variadic_type, return_type, by_ref_return, body }` | `function foo(int $a, &$b, string $c = "x"): string { }`, `function &ref(): int { ... }` — params is `Vec<(String, Option, Option, bool)>` where the tuple stores name, declared type, default value, and `is_ref` (pass by reference). `variadic` is `Option` for variadic parameters (`...$args`), `variadic_type` is the optional declared element type on that variadic (`int ...$xs`), `return_type` is an optional declared `TypeExpr`, and `by_ref_return` is `true` when declared `function &f()` so calls return a reference (alias) to the returned lvalue rather than a copy | | `FunctionVariantGroup { name, variants }` | Internal resolver metadata for include-loaded hidden function implementations behind one public name | | `FunctionVariantMark { name, variant }` | Internal include-body marker that activates the hidden function variant loaded at that runtime include point | | `Return(Option)` | `return $x;` or `return;` | diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d19c441b08..51e3143d00 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -137,7 +137,8 @@ Each routine follows the same pattern — inputs in registers, output in standar |---|---|---|---| | `__rt_strcopy` | Copy string into concat buffer | `x1`/`x2` | `x1`/`x2` | | `__rt_str_to_number` | Parse a PHP numeric string for loose comparison and numeric-string casts | `x1`/`x2` | numeric payload + success flag | -| `__rt_str_to_int` | Parse a PHP numeric-string prefix (via `__rt_str_to_number`) and truncate toward zero like PHP `(int)` casts | `x1`/`x2` | `x0` (integer) | +| `__rt_str_looks_like_int_for_coercion` | Validate PHP coercive int-parameter numeric strings while rejecting libc-only `strtod` forms such as `0x`, `INF`, and `NAN` | `x1`/`x2` | `x0` (0 or 1) | +| `__rt_str_to_int` | Parse a PHP numeric-string prefix with integer/float forms and truncate toward zero like PHP `(int)` casts | `x1`/`x2` | `x0` (integer) | | `__rt_str_loose_eq` | Compare two strings using PHP loose-comparison numeric-string rules before falling back to bytes | two strings | `x0` (0 or 1) | | `__rt_strtolower` | Lowercase conversion | `x1`/`x2` | `x1`/`x2` | | `__rt_strtoupper` | Uppercase conversion | `x1`/`x2` | `x1`/`x2` | @@ -184,6 +185,7 @@ Each routine follows the same pattern — inputs in registers, output in standar | `__rt_hash` | Hash with algorithm | algo + data | `x1`/`x2` | | `__rt_hash_init` / `__rt_hash_update` / `__rt_hash_final` | Incremental hash-context API backing `hash_init()` and friends | context + data | context / `x1`/`x2` | | `__rt_hash_copy` | Clone an incremental hash context | context | context | +| `__rt_hash_ctx_free` | Free a HashContext via `elephc_crypto_free`; the sole destructor, called by `__rt_mixed_free_deep` when a Mixed(tag=9, kind=2) cell is released at scope exit (`hash_final` no longer frees) | context | — | | `__rt_hash_hmac` | Keyed HMAC over a message | algo + key + data | `x1`/`x2` | | `__rt_hash_equals` | Constant-time string comparison | two strings | `x0` (0 or 1) | | `__rt_hash_algos_list` | Build the `hash_algos()` array of supported algorithm names | — | `x0` (array ptr) | @@ -196,9 +198,9 @@ Each routine follows the same pattern — inputs in registers, output in standar ## Callable routines -**Source:** `src/codegen/runtime/callables/` (3 files including `mod.rs`) +**Source:** `src/codegen/runtime/callables/` (4 files including `mod.rs`) -These routines implement the runtime fallback path for `is_callable()` when the argument is not a compile-time literal or statically known callable value. They consult generated metadata for builtins, user functions, public methods, public static methods, and `__invoke` objects. +These routines implement the runtime fallback path for `is_callable()` when the argument is not a compile-time literal or statically known callable value, plus the `Closure::bind` family helper. They consult generated metadata for builtins, user functions, public methods, public static methods, and `__invoke` objects. Dynamic invocation builtins use generated callable descriptors rather than these boolean helpers. A descriptor is an eight-word record: callable kind, native entry pointer, PHP-visible name pointer, name length, signature-record pointer, environment-record pointer, invocation-record pointer, and optional uniform invoker pointer. Indirect calls keep the one-word callable ABI by loading the native entry from the descriptor, while descriptor-invoker paths call the generated `(descriptor, boxed argument container) -> mixed` adapter. @@ -219,6 +221,7 @@ Extern callback trampolines use the same descriptor invoker from a C-facing entr | `__rt_is_callable_mixed` | Unbox a Mixed value and dispatch string, array, hash, or object callable checks | mixed pointer | `x0` = bool | | `__rt_is_callable_heap` | Dispatch callable checks from a raw heap pointer by inspecting its heap-kind tag | heap pointer | `x0` = bool | | `__rt_callable_descriptor_release` | Free a heap-backed callable descriptor copy plus the by-value capture slots appended after its static header; static `.data` descriptors are ignored | `x0` = descriptor pointer | — | +| `__rt_closure_bind` | Bind a `$this`-only closure to a new receiver for `Closure::bind` / `Closure::bindTo` / `Closure::call`: copy the runtime descriptor, overwrite the captured object, and incref it. Closures with any other capture shape abort with a fatal diagnostic | `x0` = source closure descriptor, `x1` = new `$this` object | `x0` = bound descriptor copy | ## Array routines @@ -348,7 +351,7 @@ See [Memory Model](memory-model.md) for the hash table memory layout. | `__rt_gc_note_child_ref` | Add one transient incoming edge to a heap child during cycle counting | `x0` = child pointer | — | | `__rt_gc_mark_reachable` | Recursively mark array/hash/object blocks reachable from external roots | `x0` = pointer | — | | `__rt_gc_collect_cycles` | Run the targeted cycle collector over heap-backed arrays/hashes/objects | — | — | -| `__rt_mixed_free_deep` | Free a mixed cell and release any nested heap-backed payload | `x0` = mixed pointer | — | +| `__rt_mixed_free_deep` | Free a mixed cell and release any nested heap-backed payload; for tag-9 resources, dispatch the kind-specific destructor (kind 1 `close`, kind 2 `__rt_hash_ctx_free`, kind 3 `__rt_pclose`, kind 4 `__rt_closedir`) | `x0` = mixed pointer | — | | `__rt_object_free_deep` | Free an object and release heap-backed properties using runtime/class metadata | `x0` = object pointer | — | Refcounts are stored as a 32-bit value in the uniform 16-byte heap header, at `[user_ptr - 12]`. Each heap allocation starts with refcount 1. When a reference is shared (e.g., assigned to another variable or passed to a function), `__rt_incref` bumps it. When the reference goes away, `__rt_decref_any` can dispatch through the uniform heap-kind tag to the concrete string/array/hash/object/mixed release path. Arrays, hashes, objects, and boxed mixed cells still use ordinary reference counting first, but when a decref sees a container/object graph that can contain nested heap-backed values, the runtime can invoke `__rt_gc_collect_cycles` to clear transient metadata, count heap-only incoming edges, mark externally reachable blocks, and deep-free the remaining unreachable array/hash/object/mixed island. @@ -443,6 +446,20 @@ The `json_encode` implementation uses **type-aware dispatch** — the codegen ca | `__rt_json_last_error_msg` | Return the message string corresponding to `_json_last_error` through the `_json_err_msg_table` data table, including decode location suffixes when active | global JSON state | `x1`/`x2` = message | | `__rt_json_pretty_push` / `__rt_json_pretty_pop` / `__rt_json_pretty_line` / `__rt_json_pretty_colon_space` | Maintain `_json_indent_depth` and append PHP-style pretty-print whitespace while each container encoder emits bytes. These helpers are no-ops unless `JSON_PRETTY_PRINT` is active, avoiding a second buffer walk. | current JSON state, `x11` write pointer for line/space helpers | updated formatting state / `x11` write pointer | +### Serialization routines + +**Files:** `system/serialize.rs`, `system/unserialize.rs` + +These helpers back PHP's `serialize()` / `unserialize()`. The serializer writes PHP's exact wire format (`N;`, `b:0;`/`b:1;`, `i:;`, `d:;`, `s::"";`, `a::{...}`, and `O::""::{...}`) directly into the [concat buffer](memory-model.md#the-string-buffer-scratch-pad), reusing `__rt_json_ftoa` for shortest-round-trip float digits. Object serialization honors `__sleep()` / `Serializable` and reuses an object back-reference table so repeated instances emit `r:`/`R:` references. + +| Routine | What it does | Input | Output | +|---|---|---|---| +| `__rt_serialize_value` | Tag-dispatching serializer for a raw runtime value, appending its wire form to the concat buffer | value tag + payload | `x1`/`x2` = string slice | +| `__rt_serialize_mixed` | Unbox a boxed Mixed cell (null pointer → `N;`), then serialize it | `x0` = mixed cell | `x1`/`x2` = string slice | +| `__rt_serialize_indexed_array` / `__rt_serialize_hash` | Serialize indexed arrays and hashes as `a::{...}` | array/hash pointer | `x1`/`x2` = string slice | +| `__rt_serialize_object` / `__rt_serialize_named_prop` / `__rt_serialize_obj_ref` | Serialize objects (`O:`/`C:`), emit one named property entry, and resolve back-references | object pointer | `x1`/`x2` = string slice | +| `__rt_unserialize_begin` / `__rt_unserialize_mixed` / `__rt_unserialize_object` | Parse a serialized string back into boxed Mixed cells, including nested arrays/hashes and objects | `x1`/`x2` = serialized string | `x0` = Mixed* (0 on malformed input) | + ### Regex routines **Files:** `system/preg_strip.rs`, `system/pcre_to_posix.rs`, `system/preg_match.rs`, `system/preg_match_all.rs`, `system/preg_replace.rs`, `system/preg_replace_callback.rs`, `system/preg_split.rs` @@ -661,26 +678,26 @@ These helpers back SPL container classes whose PHP surface needs custom runtime ## Generator routines -**Source:** `src/codegen/runtime/generators/` (2 files) +**Source:** `src/codegen/runtime/generators/` (3 files: `mod.rs`, `coro.rs`, `frame.rs`) + +These helpers back the built-in `Generator` class. Generators are **stackful coroutines** that reuse the [Fiber runtime](#fiber-routines): a `Generator` object reuses the Fiber 232-byte layout (so it can drive itself through `__rt_fiber_switch` / `suspend` / `resume` / `throw`) plus a small block of generator-specific fields (`last_key`, `last_value`, `return_value`, `auto_key`, `delegated_iter`) at offsets 184..224 inside the otherwise-unused Fiber reserved region. The generated generator body runs on its own coroutine stack and calls `__rt_gen_suspend` at each `yield`; the accessor helpers below drive the coroutine for the public Iterator surface, `send()`/`throw()`, and `getReturn()`. -These helpers back the built-in `Generator` class. Generator functions emit a heap-allocated frame and a generated resume function; the runtime helpers read/write that frame for the public Iterator surface and coroutine operations. +Because the fiber suspend boundary re-raises a scheduled exception *inside* the coroutine's own stack, `Generator::throw()` lands in an in-generator `try/catch` (issue #329) rather than unwinding the caller. | Routine | What it does | Input | Output | |---|---|---|---| -| `__rt_gen_current` | Return an owned ref to the boxed Mixed value from the most recent yield | `GeneratorFrame*` | boxed `mixed` payload | -| `__rt_gen_key` | Return an owned ref to the boxed Mixed key from the most recent yield | `GeneratorFrame*` | boxed `mixed` key | -| `__rt_gen_valid` | Report whether the generator is not terminated | `GeneratorFrame*` | bool | -| `__rt_gen_next` | Resume the state machine past the current yield unless terminated | `GeneratorFrame*` | — | -| `__rt_gen_next_done` | Shared global return label used after `next()` skips or completes a resume | `GeneratorFrame*` | — | -| `__rt_gen_send` | Store a boxed Mixed sent value, then resume the state machine | `GeneratorFrame*`, boxed `mixed` value | boxed `mixed` payload | -| `__rt_gen_send_done` | Shared global return label used after `send()` skips or completes a resume | `GeneratorFrame*` | boxed `mixed` payload | -| `__rt_gen_send_epilogue` | Shared epilogue that boxes and returns the yield produced by a resumed `send()` | `GeneratorFrame*` | boxed `mixed` payload | -| `__rt_gen_rewind` | Run the generator to its first yield once | `GeneratorFrame*` | — | -| `__rt_gen_rewind_done` | Shared global return label used when `rewind()` has already run or just finished | `GeneratorFrame*` | — | -| `__rt_gen_throw` | Mark the generator terminated and throw through the normal exception runtime | `GeneratorFrame*`, throwable object | does not return | -| `__rt_gen_get_return` | Return an owned ref to the boxed terminal return value | `GeneratorFrame*` | boxed `mixed` payload | - -Generator frames are stamped as object heap blocks because `Generator` is a built-in class implementing `Iterator`. `__rt_object_free_deep` detects the built-in Generator class id and releases the frame's custom Mixed slots plus any active `yield from` delegate instead of treating the payload as ordinary class properties. +| `__rt_gen_suspend` | `yield` suspension primitive: record the yielded key/value into the generator's persistent slots (NULL key → auto-increment integer key), then suspend via `__rt_fiber_suspend` | boxed key cell, boxed value cell | boxed `mixed` delivered by the next `send()`/`next()` | +| `__rt_gen_current` | Return an owned ref to the boxed Mixed value from the most recent yield | `Generator*` | boxed `mixed` payload | +| `__rt_gen_key` | Return an owned ref to the boxed Mixed key from the most recent yield | `Generator*` | boxed `mixed` key | +| `__rt_gen_valid` | Report whether the generator is not terminated | `Generator*` | bool | +| `__rt_gen_next` | Resume the coroutine past the current yield unless terminated | `Generator*` | — | +| `__rt_gen_send` | Store a boxed Mixed sent value, then resume the coroutine | `Generator*`, boxed `mixed` value | boxed `mixed` payload | +| `__rt_gen_throw` | Schedule a pending throw and resume so the exception is re-raised inside the coroutine | `Generator*`, throwable object | boxed `mixed` payload or rethrown exception | +| `__rt_gen_rewind` | Run the generator to its first yield once | `Generator*` | — | +| `__rt_gen_get_return` | Return an owned ref to the boxed terminal return value | `Generator*` | boxed `mixed` payload | +| `__rt_gen_delegate` | Drive a `yield from` delegate stored in `delegated_iter`, forwarding inner yields to the outer caller until the inner iterator is exhausted | `Generator*` | — | + +Generators are stamped as object heap blocks (heap kind `4`) because `Generator` is a built-in class implementing `Iterator`. `__rt_object_free_deep` detects the built-in Generator class id and releases the coroutine's custom Mixed slots plus any active `yield from` delegate instead of treating the payload as ordinary class properties. ## Fiber routines @@ -715,10 +732,10 @@ pub fn emit_runtime(emitter: &mut Emitter) { // diagnostics: runtime warning emission and @ suppression state // strings: itoa, resource display/stdout, ftoa, concat, atoi, equality, formatting, trim/mask, // search/replace, explode/implode, hashing, encoding, sscanf, ... - // callables: dynamic is_callable() fallback plus callable-descriptor release - // system: argv, time, getenv, shell, date/mktime/strtotime, JSON, regex + // callables: dynamic is_callable() fallback, callable-descriptor release, Closure::bind + // system: argv, time, getenv, shell, date/mktime/strtotime, JSON, serialize/unserialize, regex // exceptions: cleanup walk, catch matching, class-implements, throw/rethrow helpers - // generators: Generator current/key/valid/next/send/rewind/throw/getReturn helpers + // generators: fiber-backed Generator suspend/current/key/valid/next/send/rewind/throw/getReturn/yield-from // arrays: heap alloc/free, array/hash helpers, sort, callbacks, refcount // spl: SplDoublyLinkedList/SplStack/SplQueue and SplFixedArray storage helpers // objects: stdClass dynamic properties and boxed Mixed property/index dispatch @@ -729,9 +746,9 @@ pub fn emit_runtime(emitter: &mut Emitter) { } ``` -Notable runtime-only helpers emitted here include `__rt_diag_push_suppression`, `__rt_diag_pop_suppression`, `__rt_diag_warning`, `__rt_exception_cleanup_frames`, `__rt_exception_matches`, `__rt_instanceof_lookup`, `__rt_instanceof_invalid_target`, `__rt_throw_current`, `__rt_heap_debug_fail`, `__rt_heap_kind`, `__rt_hash_insert_owned`, `__rt_hash_free_deep`, `__rt_array_column_ref`, `__rt_mixed_instanceof`, `__rt_iterable_write_stdout`, `__rt_iterable_unsupported_kind`, `__rt_class_implements_interface`, `__rt_callable_descriptor_release`, `__rt_spl_dll_new`, `__rt_spl_fixed_new`, `__rt_gen_current`, `__rt_gen_send`, `__rt_preg_strip`, `__rt_pcre_to_posix`, `__rt_str_to_cstr`, `__rt_cstr_to_str`, `__rt_fiber_switch`, and `__rt_fiber_entry` in addition to the more user-visible helpers. +Notable runtime-only helpers emitted here include `__rt_diag_push_suppression`, `__rt_diag_pop_suppression`, `__rt_diag_warning`, `__rt_exception_cleanup_frames`, `__rt_exception_matches`, `__rt_instanceof_lookup`, `__rt_instanceof_invalid_target`, `__rt_throw_current`, `__rt_heap_debug_fail`, `__rt_heap_kind`, `__rt_hash_insert_owned`, `__rt_hash_free_deep`, `__rt_array_column_ref`, `__rt_mixed_instanceof`, `__rt_iterable_write_stdout`, `__rt_iterable_unsupported_kind`, `__rt_class_implements_interface`, `__rt_callable_descriptor_release`, `__rt_closure_bind`, `__rt_serialize_value`, `__rt_unserialize_begin`, `__rt_spl_dll_new`, `__rt_spl_fixed_new`, `__rt_gen_suspend`, `__rt_gen_current`, `__rt_gen_send`, `__rt_preg_strip`, `__rt_pcre_to_posix`, `__rt_str_to_cstr`, `__rt_cstr_to_str`, `__rt_fiber_switch`, and `__rt_fiber_entry` in addition to the more user-visible helpers. -Every routine in the selected target runtime slice is linked into the binary, even if unused by the current program. elephc already does AST-side control-flow pruning and dead-code elimination before codegen, but runtime-specific dead stripping is still future work. +Compiled **executables** dead-strip unreachable runtime helpers at link time. On Linux each `__rt_*` helper is emitted in its own `.text.` section and collected with `--gc-sections`; on macOS the runtime object carries a `.subsections_via_symbols` footer so each helper is a separately collectable atom dropped by `-dead_strip` (internal cross-helper labels stay assembler-local `L`-locals, with the few helpers reached by a `b`/`bl` from another atom marked `.alt_entry` so they remain live symbols). Combined with the AST-side control-flow pruning and dead-code elimination elephc already does before codegen, only the helpers a program actually reaches are linked. Shared libraries (`--emit cdylib`) keep the full runtime so every exported entry stays callable. The runtime can also be emitted in **position-independent mode** for `--emit cdylib` builds: the emitter's `pic_data_refs` flag makes the `abi::symbols` helpers route every global data reference through the GOT (`@GOTPCREL` on x86_64, `:got:`/`:got_lo12:` on AArch64) instead of direct PC-relative addressing, and on ELF targets every internal global gets a `.hidden` visibility directive. The PIC and non-PIC variants produce different assembly text, so they cache as separate runtime objects. See [The Codegen](the-codegen.md) and [Shared Libraries](../beyond-php/cdylib.md). diff --git a/docs/php/arrays.md b/docs/php/arrays.md index c23a3135fb..2585651ee2 100644 --- a/docs/php/arrays.md +++ b/docs/php/arrays.md @@ -160,6 +160,26 @@ $matrix = [[1, 2], [3, 4]]; echo $matrix[0][1]; // 2 ``` +## Spread in array literals + +The `...` spread operator flattens an array's elements into a new array literal, matching PHP semantics. + +```php + '0.0.0.0', 'timeout' => 30]; +$config = ['host' => 'localhost', 'port' => 8080]; +$merged = [...$defaults, ...$config]; +// ['host' => 'localhost', 'timeout' => 30, 'port' => 8080] +``` + ## Array destructuring Array destructuring assigns array elements to writable targets. Both short syntax and `list(...)` are supported. @@ -203,6 +223,9 @@ PHP does not allow keyed and unkeyed entries in the same destructuring pattern, | `array_splice()` | `array_splice($arr, $offset [, $length]): array` | Remove a slice in place and return the removed elements | | `array_chunk()` | `array_chunk($arr, $size): array` | Split into chunks | | `array_merge()` | `array_merge($arr1, $arr2): array` | Merge two arrays | +| `array_merge_recursive()` | `array_merge_recursive($arr1, $arr2): array` | Recursively merge two arrays: integer keys append (renumbered), string keys that collide recurse when both values are arrays and otherwise combine into a list. Accepts associative arrays or **indexed arrays of scalars** (int/float/bool); nested indexed-array values are treated as opaque. | +| `array_replace()` | `array_replace($arr, $replacements): array` | Overwrite matching keys in `$arr` (in place, keeping position) and append new keys from `$replacements`; later values win. Accepts associative arrays or **indexed arrays of scalars** (int/float/bool). | +| `array_replace_recursive()` | `array_replace_recursive($arr, $replacements): array` | Like `array_replace()`, but when both values at a key are associative arrays they are merged recursively instead of overwritten. Accepts associative arrays or **indexed arrays of scalars** (int/float/bool); nested indexed arrays are overwritten, not merged. | | `array_combine()` | `array_combine($keys, $values): array` | Create array from keys/values | | `array_fill()` | `array_fill($start, $num, $value): array` | Fill with values | | `array_fill_keys()` | `array_fill_keys($keys, $value): array` | Fill with values using keys | @@ -212,6 +235,10 @@ PHP does not allow keyed and unkeyed entries in the same destructuring pattern, | `array_intersect()` | `array_intersect($arr1, $arr2): array` | Values in both | | `array_diff_key()` | `array_diff_key($arr1, $arr2): array` | Keys in $arr1 not in $arr2 | | `array_intersect_key()` | `array_intersect_key($arr1, $arr2): array` | Keys in both | +| `array_diff_assoc()` | `array_diff_assoc($arr1, $arr2): array` | Entries of $arr1 whose `(key, value)` pair is absent from $arr2 (values compared as `(string)$a === (string)$b`). Accepts associative arrays or **indexed arrays of scalars** (int/float/bool). | +| `array_intersect_assoc()` | `array_intersect_assoc($arr1, $arr2): array` | Entries of $arr1 whose `(key, value)` pair is present in $arr2 (values compared as strings). Accepts associative arrays or **indexed arrays of scalars** (int/float/bool). | +| `array_udiff()` | `array_udiff($arr1, $arr2, $cmp): array` | Values in $arr1 not in $arr2, equality decided by the two-argument comparator (`$cmp($a, $b) === 0`). Supports string / function / non-capturing closure comparators. | +| `array_uintersect()` | `array_uintersect($arr1, $arr2, $cmp): array` | Values in both arrays, equality decided by the comparator (`$cmp($a, $b) === 0`). | | `array_unique()` | `array_unique($arr): array` | Remove duplicates | | `array_reverse()` | `array_reverse($arr): array` | Reverse order | | `array_flip()` | `array_flip($arr): array` | Exchange keys and values, normalizing integer and numeric-string result keys | @@ -220,6 +247,9 @@ PHP does not allow keyed and unkeyed entries in the same destructuring pattern, | `array_sum()` | `array_sum($arr): int\|float` | Sum of values | | `array_product()` | `array_product($arr): int\|float` | Product of values | | `array_column()` | `array_column($arr, $column_key): array` | Extract column from array of assoc rows | +| `array_is_list()` | `array_is_list($arr): bool` | `true` if the keys are exactly `0..count-1` in order (the empty array is a list) | +| `array_key_first()` | `array_key_first($arr): int\|string\|null` | First key in insertion order, or `null` if the array is empty | +| `array_key_last()` | `array_key_last($arr): int\|string\|null` | Last key in insertion order, or `null` if the array is empty | | `sort()` | `sort($arr): void` | Sort ascending (in-place) | | `rsort()` | `rsort($arr): void` | Sort descending | | `asort()` | `asort($arr): void` | Sort by value, maintain keys | @@ -229,11 +259,16 @@ PHP does not allow keyed and unkeyed entries in the same destructuring pattern, | `natsort()` | `natsort($arr): void` | Natural order sort | | `natcasesort()` | `natcasesort($arr): void` | Case-insensitive natural sort | | `shuffle()` | `shuffle($arr): void` | Randomly shuffle (in-place) | +| `array_multisort()` | `array_multisort($arr1, $arr2): bool` | Sort `$arr1` ascending (stable) and reorder `$arr2` in tandem; both are sorted in place (by reference). **Two indexed arrays of scalar elements**; sort flags, descending order, and >2 arrays are follow-ups. | | `array_rand()` | `array_rand($arr): int` | Pick one random key | | `array_map()` | `array_map($callback, $arr): array` | Apply callback to each element | | `array_filter()` | `array_filter($arr, $callback, $mode = ARRAY_FILTER_USE_VALUE): array` | Filter where callback is truthy; mode selects value, key, or both callback args | | `array_reduce()` | `array_reduce($arr, $callback, $init): int` | Reduce to single value | | `array_walk()` | `array_walk($arr, $callback): void` | Call callback on each element | +| `array_walk_recursive()` | `array_walk_recursive($arr, $callback): void` | Apply `$callback` to each non-array leaf value, recursing into nested indexed/associative arrays. Leaf values must share a scalar type (consistent with `array_walk`: leaf passed by value, no key argument). | +| `array_find()` | `array_find($arr, $callback): mixed` | (PHP 8.4) Returns the first element for which `$callback($value)` is truthy, or `null` if none match. | +| `array_any()` | `array_any($arr, $callback): bool` | (PHP 8.4) `true` if `$callback($value)` is truthy for at least one element. | +| `array_all()` | `array_all($arr, $callback): bool` | (PHP 8.4) `true` if `$callback($value)` is truthy for every element. | | `usort()` | `usort($arr, $callback): void` | Sort with user comparison | | `uksort()` | `uksort($arr, $callback): void` | Sort by key with user comparison | | `uasort()` | `uasort($arr, $callback): void` | Sort with user comparison, maintain keys | diff --git a/docs/php/builtins.md b/docs/php/builtins.md index b73b60e7c9..3b6a671303 100644 --- a/docs/php/builtins.md +++ b/docs/php/builtins.md @@ -9,94 +9,111 @@ sidebar: | Function | Signature | Returns | |---|---|---| -| [`array_chunk()`](./builtins/array/array_chunk.md) | `(array $array, int $length, bool $preserve_keys): array` | `array` | -| [`array_column()`](./builtins/array/array_column.md) | `(array $array, string $column_key, string $index_key): array` | `array` | +| [`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, int $mode): array` | `array` | -| [`array_flip()`](./builtins/array/array_flip.md) | `(array $array): float` | `float` | +| [`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_keys()`](./builtins/array/array_keys.md) | `(array $array, string $filter_value, bool $strict): array` | `array` | +| [`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): float` | `float` | +| [`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 $num): int` | `int` | -| [`array_reduce()`](./builtins/array/array_reduce.md) | `(array $array, callable $callback, mixed $initial): int` | `int` | -| [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array, bool $preserve_keys): array` | `array` | -| [`array_search()`](./builtins/array/array_search.md) | `(mixed $needle, array $haystack, bool $strict): mixed` | `mixed` | +| [`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, bool $preserve_keys): array` | `array` | -| [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length, array $replacement): array` | `array` | -| [`array_sum()`](./builtins/array/array_sum.md) | `(array $array): float` | `float` | -| [`array_unique()`](./builtins/array/array_unique.md) | `(array $array, int $flags): array` | `array` | +| [`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, mixed $arg): void` | `void` | -| [`arsort()`](./builtins/array/arsort.md) | `(array $array, int $flags): bool` | `bool` | -| [`asort()`](./builtins/array/asort.md) | `(array $array, int $flags): bool` | `bool` | -| [`count()`](./builtins/array/count.md) | `(array $value, int $mode): int` | `int` | -| [`in_array()`](./builtins/array/in_array.md) | `(mixed $needle, array $haystack, bool $strict): mixed` | `mixed` | -| [`krsort()`](./builtins/array/krsort.md) | `(array $array, int $flags): bool` | `bool` | -| [`ksort()`](./builtins/array/ksort.md) | `(array $array, int $flags): bool` | `bool` | +| [`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, int $step): array` | `array` | -| [`rsort()`](./builtins/array/rsort.md) | `(array $array, int $flags): 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, int $flags): 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): bool` | `bool` | +| [`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): bool` | `bool` | -| [`class_get_attributes()`](./builtins/class/class_get_attributes.md) | `(string $class_name): mixed` | `mixed` | -| [`class_implements()`](./builtins/class/class_implements.md) | `(mixed $object_or_class, bool $autoload): mixed` | `mixed` | -| [`class_parents()`](./builtins/class/class_parents.md) | `(mixed $object_or_class, bool $autoload): mixed` | `mixed` | -| [`class_uses()`](./builtins/class/class_uses.md) | `(mixed $object_or_class, bool $autoload): mixed` | `mixed` | -| [`enum_exists()`](./builtins/class/enum_exists.md) | `(string $enum, bool $autoload): bool` | `bool` | +| [`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): string` | `string` | +| [`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): string` | `string` | -| [`interface_exists()`](./builtins/class/interface_exists.md) | `(string $interface, bool $autoload): bool` | `bool` | -| [`is_a()`](./builtins/class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string): bool` | `bool` | -| [`is_subclass_of()`](./builtins/class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string): bool` | `bool` | -| [`trait_exists()`](./builtins/class/trait_exists.md) | `(string $trait, bool $autoload): bool` | `bool` | +| [`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): string` | `string` | +| [`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): array` | `array` | -| [`gmdate()`](./builtins/date/gmdate.md) | `(string $format, int $timestamp): string` | `string` | +| [`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): mixed` | `mixed` | -| [`localtime()`](./builtins/date/localtime.md) | `(int $timestamp, bool $associative): array` | `array` | -| [`microtime()`](./builtins/date/microtime.md) | `(bool $as_float): 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): mixed` | `mixed` | +| [`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` | +| [`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, int $group): 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, int $user): bool` | `bool` | -| [`clearstatcache()`](./builtins/filesystem/clearstatcache.md) | `(bool $clear_realpath_cache, string $filename): void` | `void` | -| [`copy()`](./builtins/filesystem/copy.md) | `(string $from, string $to, mixed $context): bool` | `bool` | -| [`dirname()`](./builtins/filesystem/dirname.md) | `(string $path, int $levels): string` | `string` | +| [`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` | @@ -109,10 +126,10 @@ sidebar: | [`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): bool` | `bool` | +| [`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, bool $local_only): mixed` | `mixed` | -| [`glob()`](./builtins/filesystem/glob.md) | `(string $pattern, int $flags): array` | `array` | +| [`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` | @@ -120,29 +137,29 @@ sidebar: | [`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, int $group): bool` | `bool` | -| [`lchown()`](./builtins/filesystem/lchown.md) | `(string $filename, int $user): 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, int $permissions, bool $recursive, bool $context): bool` | `bool` | -| [`pathinfo()`](./builtins/filesystem/pathinfo.md) | `(string $path, int $flags): 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, bool $use_include_path, mixed $context): mixed` | `mixed` | +| [`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, mixed $context): bool` | `bool` | -| [`rmdir()`](./builtins/filesystem/rmdir.md) | `(string $directory, mixed $context = null): bool` | `bool` | -| [`scandir()`](./builtins/filesystem/scandir.md) | `(string $directory, int $sorting_order, mixed $context): array` | `array` | +| [`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, int $atime): bool` | `bool` | -| [`umask()`](./builtins/filesystem/umask.md) | `(int $mask): int` | `int` | +| [`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` | @@ -150,24 +167,24 @@ sidebar: | [`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, string $separator, string $enclosure, string $escape): array` | `array` | -| [`fgets()`](./builtins/io/fgets.md) | `(resource $stream, int $length): mixed` | `mixed` | -| [`file()`](./builtins/io/file.md) | `(string $filename, int $flags, mixed $context): array` | `array` | -| [`file_get_contents()`](./builtins/io/file_get_contents.md) | `(string $filename, bool $use_include_path, mixed $context, int $offset, int $length): mixed` | `mixed` | -| [`file_put_contents()`](./builtins/io/file_put_contents.md) | `(string $filename, mixed $data, int $flags = 0, mixed $context = null): int` | `int` | -| [`flock()`](./builtins/io/flock.md) | `(resource $stream, int $operation, bool $would_block): bool` | `bool` | -| [`fopen()`](./builtins/io/fopen.md) | `(string $filename, string $mode, bool $use_include_path, mixed $context): 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 = '"', string $escape = '\\', string $eol = '\n'): 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): int` | `int` | +| [`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 $length): int` | `int` | +| [`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` | @@ -175,64 +192,64 @@ sidebar: | [`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, array $options = []): 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, array $params): mixed` | `mixed` | -| [`stream_context_get_default()`](./builtins/io/stream_context_get_default.md) | `(array $options): mixed` | `mixed` | -| [`stream_context_get_options()`](./builtins/io/stream_context_get_options.md) | `(resource $stream_or_context): array` | `array` | +| [`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, mixed $value): bool` | `bool` | +| [`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, int $offset): mixed` | `mixed` | +| [`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, int $offset): mixed` | `mixed` | +| [`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_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): int` | `int` | +| [`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): bool` | `bool` | +| [`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, string $peer_name): mixed` | `mixed` | -| [`stream_socket_client()`](./builtins/io/stream_socket_client.md) | `(string $address, int $error_code, int $error_message, string $timeout, float $flags): mixed` | `mixed` | -| [`stream_socket_enable_crypto()`](./builtins/io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method, resource $session_stream): bool` | `bool` | +| [`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, string $address): mixed` | `mixed` | -| [`stream_socket_sendto()`](./builtins/io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags, string $address): mixed` | `mixed` | -| [`stream_socket_server()`](./builtins/io/stream_socket_server.md) | `(string $address, int $error_code, int $error_message): 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): 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, int $depth, int $flags): mixed` | `mixed` | -| [`json_encode()`](./builtins/json/json_encode.md) | `(mixed $value, int $flags, int $depth): string` | `string` | +| [`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, int $flags): bool` | `bool` | +| [`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): string` | `string` | +| [`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` | @@ -245,37 +262,38 @@ sidebar: | [`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): float` | `float` | +| [`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): float` | `float` | -| [`min()`](./builtins/math/min.md) | `(mixed $value, ...$values): 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_bytes()`](./builtins/math/random_bytes.md) | `(int $length): string` | `string` | | [`random_int()`](./builtins/math/random_int.md) | `(int $min, int $max): int` | `int` | -| [`round()`](./builtins/math/round.md) | `(float $num, int $precision): float` | `float` | +| [`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` | -| [`call_user_func()`](./builtins/misc/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | -| [`call_user_func_array()`](./builtins/misc/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | -| [`define()`](./builtins/misc/define.md) | `(string $constant_name, mixed $value, bool $case_insensitive): bool` | `bool` | +| [`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) | `(mixed $header, mixed $replace, mixed $response_code): void` | `void` | -| [`http_response_code()`](./builtins/misc/http_response_code.md) | `(mixed $response_code): int` | `int` | +| [`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): string` | `string` | -| [`phpversion()`](./builtins/misc/phpversion.md) | `(string $extension = null): string` | `string` | -| [`print_r()`](./builtins/misc/print_r.md) | `(...$values): void` | `void` | +| [`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): void` | `void` | +| [`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) | `(...$values): void` | `void` | +| [`var_dump()`](./builtins/misc/var_dump.md) | `(mixed $value): 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` | @@ -286,116 +304,116 @@ sidebar: | [`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): mixed` | `mixed` | +| [`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` | | [`die()`](./builtins/process/die.md) | `(int $status): void` | `void` | -| [`exec()`](./builtins/process/exec.md) | `(string $command, array $output, int $result_code): string` | `string` | +| [`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, int $result_code): 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): 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, int $result_code): string` | `string` | +| [`system()`](./builtins/process/system.md) | `(string $command): string` | `string` | | [`usleep()`](./builtins/process/usleep.md) | `(int $microseconds): void` | `void` | -| [`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, array $matches): int` | `int` | -| [`preg_replace()`](./builtins/regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject, int $limit = -1, int $count = null): string` | `string` | -| [`preg_replace_callback()`](./builtins/regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject, int $limit = -1, int $count = null, int $flags = 0): array` | `array` | -| [`preg_split()`](./builtins/regex/preg_split.md) | `(string $pattern, string $subject, int $limit, int $flags): array` | `array` | -| [`iterator_apply()`](./builtins/spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args): int` | `int` | +| [`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): array` | `array` | -| [`spl_autoload()`](./builtins/spl/spl_autoload.md) | `(string $class, string $file_extensions): void` | `void` | +| [`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): string` | `string` | +| [`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, bool $throw, bool $prepend): bool` | `bool` | +| [`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, string $error_message, float $timeout): mixed` | `mixed` | -| [`pfsockopen()`](./builtins/streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code, string $error_message, float $timeout): mixed` | `mixed` | +| [`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 $filter_name, int $mode, mixed $params): mixed` | `mixed` | -| [`stream_filter_prepend()`](./builtins/streams/stream_filter_prepend.md) | `(resource $stream, string $filter_name, int $mode, mixed $params): mixed` | `mixed` | +| [`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, bool $strict): 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): 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): array` | `array` | +| [`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, int $encoding): string` | `string` | -| [`gzdeflate()`](./builtins/string/gzdeflate.md) | `(string $data, int $level, int $encoding): string` | `string` | -| [`gzinflate()`](./builtins/string/gzinflate.md) | `(string $data, int $max_length): string` | `string` | -| [`gzuncompress()`](./builtins/string/gzuncompress.md) | `(string $data, int $max_length): string` | `string` | -| [`hash()`](./builtins/string/hash.md) | `(string $algo, string $data, bool $binary = false, array $options = []): string` | `string` | +| [`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): string` | `string` | -| [`hash_hmac()`](./builtins/string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary): string` | `string` | -| [`hash_init()`](./builtins/string/hash_init.md) | `(string $algo, int $flags = 0, string $key = '', array $options = []): mixed` | `mixed` | +| [`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, int $flags, string $encoding): string` | `string` | -| [`htmlentities()`](./builtins/string/htmlentities.md) | `(string $string, int $flags, string $encoding, bool $double_encode): string` | `string` | -| [`htmlspecialchars()`](./builtins/string/htmlspecialchars.md) | `(string $string, int $flags, string $encoding, bool $double_encode): string` | `string` | -| [`implode()`](./builtins/string/implode.md) | `(string $separator, array $array): string` | `string` | +| [`html_entity_decode()`](./builtins/string/html_entity_decode.md) | `(string $string): string` | `string` | +| [`htmlentities()`](./builtins/string/htmlentities.md) | `(string $string): string` | `string` | +| [`htmlspecialchars()`](./builtins/string/htmlspecialchars.md) | `(string $string): 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): string` | `string` | -| [`md5()`](./builtins/string/md5.md) | `(string $string, bool $binary): string` | `string` | -| [`nl2br()`](./builtins/string/nl2br.md) | `(string $string, bool $use_xhtml): string` | `string` | -| [`number_format()`](./builtins/string/number_format.md) | `(float $num, int $decimals, string $decimal_separator, string $thousands_separator): 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): string` | `string` | -| [`sha1()`](./builtins/string/sha1.md) | `(string $string, bool $binary): 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) | `(mixed $search, mixed $replace, mixed $subject, int $count): mixed` | `mixed` | -| [`str_pad()`](./builtins/string/str_pad.md) | `(string $string, int $length, string $pad_string, int $pad_type): string` | `string` | +| [`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): mixed` | `mixed` | -| [`str_split()`](./builtins/string/str_split.md) | `(string $string, int $length): array` | `array` | +| [`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): mixed` | `mixed` | +| [`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): mixed` | `mixed` | -| [`strstr()`](./builtins/string/strstr.md) | `(string $haystack, string $needle, bool $before_needle): 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): string` | `string` | -| [`substr_replace()`](./builtins/string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length): string` | `string` | -| [`trim()`](./builtins/string/trim.md) | `(string $string, string $characters): 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): 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, string $break, bool $cut_long_words): 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` | @@ -405,10 +423,10 @@ sidebar: | [`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 $base): int` | `int` | +| [`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 $syntax_only = false, string $callable_name = null): 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` | diff --git a/docs/php/builtins/array.md b/docs/php/builtins/array.md index d157f2c790..f2f4d44092 100644 --- a/docs/php/builtins/array.md +++ b/docs/php/builtins/array.md @@ -9,49 +9,66 @@ sidebar: | Function | Signature | Returns | |---|---|---| -| [`array_chunk()`](./array/array_chunk.md) | `(array $array, int $length, bool $preserve_keys): array` | `array` | -| [`array_column()`](./array/array_column.md) | `(array $array, string $column_key, string $index_key): array` | `array` | +| [`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, int $mode): array` | `array` | -| [`array_flip()`](./array/array_flip.md) | `(array $array): float` | `float` | +| [`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_keys()`](./array/array_keys.md) | `(array $array, string $filter_value, bool $strict): array` | `array` | +| [`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): float` | `float` | +| [`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 $num): int` | `int` | -| [`array_reduce()`](./array/array_reduce.md) | `(array $array, callable $callback, mixed $initial): int` | `int` | -| [`array_reverse()`](./array/array_reverse.md) | `(array $array, bool $preserve_keys): array` | `array` | -| [`array_search()`](./array/array_search.md) | `(mixed $needle, array $haystack, bool $strict): mixed` | `mixed` | +| [`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, bool $preserve_keys): array` | `array` | -| [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length, array $replacement): array` | `array` | -| [`array_sum()`](./array/array_sum.md) | `(array $array): float` | `float` | -| [`array_unique()`](./array/array_unique.md) | `(array $array, int $flags): array` | `array` | +| [`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, mixed $arg): void` | `void` | -| [`arsort()`](./array/arsort.md) | `(array $array, int $flags): bool` | `bool` | -| [`asort()`](./array/asort.md) | `(array $array, int $flags): bool` | `bool` | -| [`count()`](./array/count.md) | `(array $value, int $mode): int` | `int` | -| [`in_array()`](./array/in_array.md) | `(mixed $needle, array $haystack, bool $strict): mixed` | `mixed` | -| [`krsort()`](./array/krsort.md) | `(array $array, int $flags): bool` | `bool` | -| [`ksort()`](./array/ksort.md) | `(array $array, int $flags): bool` | `bool` | +| [`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, int $step): array` | `array` | -| [`rsort()`](./array/rsort.md) | `(array $array, int $flags): 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, int $flags): 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 new file mode 100644 index 0000000000..20fa87b4bd --- /dev/null +++ b/docs/php/builtins/array/array_all.md @@ -0,0 +1,33 @@ +--- +title: "array_all()" +description: "Returns true when every array element satisfies the predicate callback." +sidebar: + order: 1 +--- + +## array_all() + +```php +function array_all(mixed $array, mixed $callback): bool +``` + +Returns true when every array element satisfies the predicate callback. + +**Parameters**: +- `$array` (`mixed`) +- `$callback` (`mixed`) + +**Returns**: `bool` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_all` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_all.md). + diff --git a/docs/php/builtins/array/array_any.md b/docs/php/builtins/array/array_any.md new file mode 100644 index 0000000000..c8c8658fad --- /dev/null +++ b/docs/php/builtins/array/array_any.md @@ -0,0 +1,33 @@ +--- +title: "array_any()" +description: "Returns true when at least one array element satisfies the predicate callback." +sidebar: + order: 2 +--- + +## array_any() + +```php +function array_any(mixed $array, mixed $callback): bool +``` + +Returns true when at least one array element satisfies the predicate callback. + +**Parameters**: +- `$array` (`mixed`) +- `$callback` (`mixed`) + +**Returns**: `bool` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_any` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_any.md). + diff --git a/docs/php/builtins/array/array_chunk.md b/docs/php/builtins/array/array_chunk.md index a1a2366bbe..3bf1f31b8c 100644 --- a/docs/php/builtins/array/array_chunk.md +++ b/docs/php/builtins/array/array_chunk.md @@ -1,22 +1,21 @@ --- title: "array_chunk()" -description: "Lowers `array_chunk()` by splitting an indexed array into nested indexed arrays." +description: "Splits an array into chunks of the given size." sidebar: - order: 1 + order: 3 --- ## array_chunk() ```php -function array_chunk(array $array, int $length, bool $preserve_keys): array +function array_chunk(array $array, int $length): array ``` -Lowers `array_chunk()` by splitting an indexed array into nested indexed arrays. +Splits an array into chunks of the given size. **Parameters**: - `$array` (`array`) - `$length` (`int`) -- `$preserve_keys` (`bool`) **Returns**: `array` diff --git a/docs/php/builtins/array/array_column.md b/docs/php/builtins/array/array_column.md index 371306c602..c4f21c1c22 100644 --- a/docs/php/builtins/array/array_column.md +++ b/docs/php/builtins/array/array_column.md @@ -1,22 +1,21 @@ --- title: "array_column()" -description: "Lowers `array_column()` by dispatching to the helper matching row value ownership." +description: "Returns the values from a single column of an array of arrays." sidebar: - order: 2 + order: 4 --- ## array_column() ```php -function array_column(array $array, string $column_key, string $index_key): array +function array_column(array $array, string $column_key): array ``` -Lowers `array_column()` by dispatching to the helper matching row value ownership. +Returns the values from a single column of an array of arrays. **Parameters**: - `$array` (`array`) - `$column_key` (`string`) -- `$index_key` (`string`) **Returns**: `array` diff --git a/docs/php/builtins/array/array_combine.md b/docs/php/builtins/array/array_combine.md index 8f8b288202..18b4617d4d 100644 --- a/docs/php/builtins/array/array_combine.md +++ b/docs/php/builtins/array/array_combine.md @@ -1,8 +1,8 @@ --- title: "array_combine()" -description: "Lowers `array_combine()` through the legacy hash-building runtime helpers." +description: "Creates an array by using one array for keys and another for values." sidebar: - order: 3 + order: 5 --- ## array_combine() @@ -11,7 +11,7 @@ sidebar: function array_combine(array $keys, array $values): array ``` -Lowers `array_combine()` through the legacy hash-building runtime helpers. +Creates an array by using one array for keys and another for values. **Parameters**: - `$keys` (`array`) diff --git a/docs/php/builtins/array/array_diff.md b/docs/php/builtins/array/array_diff.md index 0bd02f2992..ea514aa384 100644 --- a/docs/php/builtins/array/array_diff.md +++ b/docs/php/builtins/array/array_diff.md @@ -1,8 +1,8 @@ --- title: "array_diff()" -description: "Lowers `array_diff()` for two compatible indexed arrays with pointer-sized payload slots." +description: "Computes the difference of arrays." sidebar: - order: 4 + order: 6 --- ## array_diff() @@ -11,7 +11,7 @@ sidebar: function array_diff(array $array, ...$arrays): array ``` -Lowers `array_diff()` for two compatible indexed arrays with pointer-sized payload slots. +Computes the difference of arrays. **Parameters**: - `$array` (`array`) diff --git a/docs/php/builtins/array/array_diff_assoc.md b/docs/php/builtins/array/array_diff_assoc.md new file mode 100644 index 0000000000..488a870134 --- /dev/null +++ b/docs/php/builtins/array/array_diff_assoc.md @@ -0,0 +1,33 @@ +--- +title: "array_diff_assoc()" +description: "Computes the difference of arrays with additional index check." +sidebar: + order: 7 +--- + +## array_diff_assoc() + +```php +function array_diff_assoc(array $array, ...$arrays): mixed +``` + +Computes the difference of arrays with additional index check. + +**Parameters**: +- `$array` (`array`) +- `...$arrays` — variadic: collects excess arguments into `$arrays`. + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_diff_assoc` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_diff_assoc.md). + diff --git a/docs/php/builtins/array/array_diff_key.md b/docs/php/builtins/array/array_diff_key.md index 1863d34d62..2fd900fa8a 100644 --- a/docs/php/builtins/array/array_diff_key.md +++ b/docs/php/builtins/array/array_diff_key.md @@ -1,8 +1,8 @@ --- title: "array_diff_key()" -description: "Lowers `array_diff_key()` for two associative arrays by filtering first-operand keys." +description: "Computes the difference of arrays using keys for comparison." sidebar: - order: 5 + order: 8 --- ## array_diff_key() @@ -11,7 +11,7 @@ sidebar: function array_diff_key(array $array, ...$arrays): array ``` -Lowers `array_diff_key()` for two associative arrays by filtering first-operand keys. +Computes the difference of arrays using keys for comparison. **Parameters**: - `$array` (`array`) diff --git a/docs/php/builtins/array/array_fill.md b/docs/php/builtins/array/array_fill.md index 7d6a189a21..c6140e68f5 100644 --- a/docs/php/builtins/array/array_fill.md +++ b/docs/php/builtins/array/array_fill.md @@ -1,8 +1,8 @@ --- title: "array_fill()" -description: "Lowers `array_fill()` for pointer-sized scalar and refcounted payloads." +description: "Fill an array with values." sidebar: - order: 6 + order: 9 --- ## array_fill() @@ -11,7 +11,7 @@ sidebar: function array_fill(int $start_index, int $count, mixed $value): array ``` -Lowers `array_fill()` for pointer-sized scalar and refcounted payloads. +Fill an array with values. **Parameters**: - `$start_index` (`int`) diff --git a/docs/php/builtins/array/array_fill_keys.md b/docs/php/builtins/array/array_fill_keys.md index 023a9f4abb..d8c4197c76 100644 --- a/docs/php/builtins/array/array_fill_keys.md +++ b/docs/php/builtins/array/array_fill_keys.md @@ -1,8 +1,8 @@ --- title: "array_fill_keys()" -description: "Lowers `array_fill_keys()` through the legacy hash-building runtime helpers." +description: "Fill an array with values, specifying keys." sidebar: - order: 7 + order: 10 --- ## array_fill_keys() @@ -11,7 +11,7 @@ sidebar: function array_fill_keys(array $keys, mixed $value): array ``` -Lowers `array_fill_keys()` through the legacy hash-building runtime helpers. +Fill an array with values, specifying keys. **Parameters**: - `$keys` (`array`) diff --git a/docs/php/builtins/array/array_filter.md b/docs/php/builtins/array/array_filter.md index cf0b7430c0..0aca1d8604 100644 --- a/docs/php/builtins/array/array_filter.md +++ b/docs/php/builtins/array/array_filter.md @@ -1,22 +1,22 @@ --- title: "array_filter()" -description: "Lowers `array_filter()` for static and first-class callbacks through the runtime helper." +description: "Filters elements of an array using a callback function." sidebar: - order: 8 + order: 11 --- ## array_filter() ```php -function array_filter(array $array, callable $callback, int $mode): array +function array_filter(array $array, callable $callback = null, int $mode = 0): array ``` -Lowers `array_filter()` for static and first-class callbacks through the runtime helper. +Filters elements of an array using a callback function. **Parameters**: - `$array` (`array`) -- `$callback` (`callable`), optional -- `$mode` (`int`), optional +- `$callback` (`callable`), default `null`, optional +- `$mode` (`int`), default `0`, optional **Returns**: `array` diff --git a/docs/php/builtins/array/array_find.md b/docs/php/builtins/array/array_find.md new file mode 100644 index 0000000000..e21471ea11 --- /dev/null +++ b/docs/php/builtins/array/array_find.md @@ -0,0 +1,33 @@ +--- +title: "array_find()" +description: "Returns the first element satisfying a predicate callback, or null." +sidebar: + order: 12 +--- + +## array_find() + +```php +function array_find(mixed $array, mixed $callback): mixed +``` + +Returns the first element satisfying a predicate callback, or null. + +**Parameters**: +- `$array` (`mixed`) +- `$callback` (`mixed`) + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_find` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_find.md). + diff --git a/docs/php/builtins/array/array_flip.md b/docs/php/builtins/array/array_flip.md index be2392afb5..d4a791bde8 100644 --- a/docs/php/builtins/array/array_flip.md +++ b/docs/php/builtins/array/array_flip.md @@ -1,22 +1,22 @@ --- title: "array_flip()" -description: "Lowers `array_flip()` through the legacy hash-building runtime helpers." +description: "Exchanges all keys with their associated values in an array." sidebar: - order: 9 + order: 13 --- ## array_flip() ```php -function array_flip(array $array): float +function array_flip(array $array): array ``` -Lowers `array_flip()` through the legacy hash-building runtime helpers. +Exchanges all keys with their associated values in an array. **Parameters**: - `$array` (`array`) -**Returns**: `float` +**Returns**: `array` _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 bff2c4ba62..55c5ce3ab5 100644 --- a/docs/php/builtins/array/array_intersect.md +++ b/docs/php/builtins/array/array_intersect.md @@ -1,8 +1,8 @@ --- title: "array_intersect()" -description: "Lowers `array_intersect()` for two compatible indexed arrays with pointer-sized payload slots." +description: "Computes the intersection of arrays." sidebar: - order: 10 + order: 14 --- ## array_intersect() @@ -11,7 +11,7 @@ sidebar: function array_intersect(array $array, ...$arrays): array ``` -Lowers `array_intersect()` for two compatible indexed arrays with pointer-sized payload slots. +Computes the intersection of arrays. **Parameters**: - `$array` (`array`) diff --git a/docs/php/builtins/array/array_intersect_assoc.md b/docs/php/builtins/array/array_intersect_assoc.md new file mode 100644 index 0000000000..889e440ea1 --- /dev/null +++ b/docs/php/builtins/array/array_intersect_assoc.md @@ -0,0 +1,33 @@ +--- +title: "array_intersect_assoc()" +description: "Computes the intersection of arrays with additional index check." +sidebar: + order: 15 +--- + +## array_intersect_assoc() + +```php +function array_intersect_assoc(array $array, ...$arrays): mixed +``` + +Computes the intersection of arrays with additional index check. + +**Parameters**: +- `$array` (`array`) +- `...$arrays` — variadic: collects excess arguments into `$arrays`. + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_intersect_assoc` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_intersect_assoc.md). + diff --git a/docs/php/builtins/array/array_intersect_key.md b/docs/php/builtins/array/array_intersect_key.md index 1cdcf85c28..d9fb6a57c0 100644 --- a/docs/php/builtins/array/array_intersect_key.md +++ b/docs/php/builtins/array/array_intersect_key.md @@ -1,8 +1,8 @@ --- title: "array_intersect_key()" -description: "Lowers `array_intersect_key()` for two associative arrays by keeping shared first-operand keys." +description: "Computes the intersection of arrays using keys for comparison." sidebar: - order: 11 + order: 16 --- ## array_intersect_key() @@ -11,7 +11,7 @@ sidebar: function array_intersect_key(array $array, ...$arrays): array ``` -Lowers `array_intersect_key()` for two associative arrays by keeping shared first-operand keys. +Computes the intersection of arrays using keys for comparison. **Parameters**: - `$array` (`array`) diff --git a/docs/php/builtins/array/array_is_list.md b/docs/php/builtins/array/array_is_list.md new file mode 100644 index 0000000000..c5264801c8 --- /dev/null +++ b/docs/php/builtins/array/array_is_list.md @@ -0,0 +1,32 @@ +--- +title: "array_is_list()" +description: "Checks whether an array is a list (sequential 0-based integer keys)." +sidebar: + order: 17 +--- + +## array_is_list() + +```php +function array_is_list(mixed $array): bool +``` + +Checks whether an array is a list (sequential 0-based integer keys). + +**Parameters**: +- `$array` (`mixed`) + +**Returns**: `bool` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_is_list` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_is_list.md). + diff --git a/docs/php/builtins/array/array_key_exists.md b/docs/php/builtins/array/array_key_exists.md index 08e8fb5ae0..bee1b864a3 100644 --- a/docs/php/builtins/array/array_key_exists.md +++ b/docs/php/builtins/array/array_key_exists.md @@ -1,8 +1,8 @@ --- title: "array_key_exists()" -description: "Lowers `array_key_exists()` for indexed arrays and associative arrays." +description: "Checks if the given key or index exists in the array." sidebar: - order: 12 + order: 18 --- ## array_key_exists() @@ -11,7 +11,7 @@ sidebar: function array_key_exists(string $key, array $array): bool ``` -Lowers `array_key_exists()` for indexed arrays and associative arrays. +Checks if the given key or index exists in the array. **Parameters**: - `$key` (`string`) diff --git a/docs/php/builtins/array/array_key_first.md b/docs/php/builtins/array/array_key_first.md new file mode 100644 index 0000000000..64e4ab6b28 --- /dev/null +++ b/docs/php/builtins/array/array_key_first.md @@ -0,0 +1,32 @@ +--- +title: "array_key_first()" +description: "Gets the first key of an array." +sidebar: + order: 19 +--- + +## array_key_first() + +```php +function array_key_first(array $array): mixed +``` + +Gets the first key of an array. + +**Parameters**: +- `$array` (`array`) + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_key_first` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_key_first.md). + diff --git a/docs/php/builtins/array/array_key_last.md b/docs/php/builtins/array/array_key_last.md new file mode 100644 index 0000000000..50da0ddb35 --- /dev/null +++ b/docs/php/builtins/array/array_key_last.md @@ -0,0 +1,32 @@ +--- +title: "array_key_last()" +description: "Gets the last key of an array." +sidebar: + order: 20 +--- + +## array_key_last() + +```php +function array_key_last(array $array): mixed +``` + +Gets the last key of an array. + +**Parameters**: +- `$array` (`array`) + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_key_last` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_key_last.md). + diff --git a/docs/php/builtins/array/array_keys.md b/docs/php/builtins/array/array_keys.md index 26c3b9baf2..dd9eab185a 100644 --- a/docs/php/builtins/array/array_keys.md +++ b/docs/php/builtins/array/array_keys.md @@ -1,22 +1,20 @@ --- title: "array_keys()" -description: "Lowers `array_keys()` for indexed arrays and associative arrays." +description: "Returns all the keys of an array." sidebar: - order: 13 + order: 21 --- ## array_keys() ```php -function array_keys(array $array, string $filter_value, bool $strict): array +function array_keys(array $array): array ``` -Lowers `array_keys()` for indexed arrays and associative arrays. +Returns all the keys of an array. **Parameters**: - `$array` (`array`) -- `$filter_value` (`string`) -- `$strict` (`bool`) **Returns**: `array` diff --git a/docs/php/builtins/array/array_map.md b/docs/php/builtins/array/array_map.md index 945cc70a41..c1dcfac6ca 100644 --- a/docs/php/builtins/array/array_map.md +++ b/docs/php/builtins/array/array_map.md @@ -1,8 +1,8 @@ --- title: "array_map()" -description: "Lowers `array_map()` through the callback runtime helper matching the callback result type." +description: "Applies a callback to the elements of an array." sidebar: - order: 14 + order: 22 --- ## array_map() @@ -11,7 +11,7 @@ sidebar: function array_map(callable $callback, array $array, ...$arrays): array ``` -Lowers `array_map()` through the callback runtime helper matching the callback result type. +Applies a callback to the elements of an array. **Parameters**: - `$callback` (`callable`) diff --git a/docs/php/builtins/array/array_merge.md b/docs/php/builtins/array/array_merge.md index dd26a53e25..944a0185fb 100644 --- a/docs/php/builtins/array/array_merge.md +++ b/docs/php/builtins/array/array_merge.md @@ -1,8 +1,8 @@ --- title: "array_merge()" -description: "Lowers `array_merge()` for two compatible indexed arrays with 8-byte payload slots." +description: "Merges the elements of two arrays." sidebar: - order: 15 + order: 23 --- ## array_merge() @@ -11,7 +11,7 @@ sidebar: function array_merge(...$arrays): array ``` -Lowers `array_merge()` for two compatible indexed arrays with 8-byte payload slots. +Merges the elements of two arrays. **Parameters**: - `...$arrays` — variadic: collects excess arguments into `$arrays`. diff --git a/docs/php/builtins/array/array_merge_recursive.md b/docs/php/builtins/array/array_merge_recursive.md new file mode 100644 index 0000000000..4cccbd6f2a --- /dev/null +++ b/docs/php/builtins/array/array_merge_recursive.md @@ -0,0 +1,32 @@ +--- +title: "array_merge_recursive()" +description: "Recursively merges two arrays, combining scalar collisions into lists." +sidebar: + order: 24 +--- + +## array_merge_recursive() + +```php +function array_merge_recursive(...$arrays): array +``` + +Recursively merges two arrays, combining scalar collisions into lists. + +**Parameters**: +- `...$arrays` — variadic: collects excess arguments into `$arrays`. + +**Returns**: `array` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_merge_recursive` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_merge_recursive.md). + diff --git a/docs/php/builtins/array/array_multisort.md b/docs/php/builtins/array/array_multisort.md new file mode 100644 index 0000000000..0d03600acb --- /dev/null +++ b/docs/php/builtins/array/array_multisort.md @@ -0,0 +1,33 @@ +--- +title: "array_multisort()" +description: "Sorts multiple arrays or multi-dimensional arrays." +sidebar: + order: 25 +--- + +## array_multisort() + +```php +function array_multisort(array $array1, int $array2): bool +``` + +Sorts multiple arrays or multi-dimensional arrays. + +**Parameters**: +- `$array1` (`array`), passed by reference +- `$array2` (`int`), passed by reference + +**Returns**: `bool` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_multisort` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_multisort.md). + diff --git a/docs/php/builtins/array/array_pad.md b/docs/php/builtins/array/array_pad.md index 435fe7bf58..57e9cdf9fc 100644 --- a/docs/php/builtins/array/array_pad.md +++ b/docs/php/builtins/array/array_pad.md @@ -1,8 +1,8 @@ --- title: "array_pad()" -description: "Lowers `array_pad()` by copying an indexed array and filling missing slots." +description: "Pads an array to the specified length with a value." sidebar: - order: 16 + order: 26 --- ## array_pad() @@ -11,7 +11,7 @@ sidebar: function array_pad(array $array, int $length, mixed $value): array ``` -Lowers `array_pad()` by copying an indexed array and filling missing slots. +Pads an array to the specified length with a value. **Parameters**: - `$array` (`array`) diff --git a/docs/php/builtins/array/array_pop.md b/docs/php/builtins/array/array_pop.md index 51238b0406..640e0c5c4d 100644 --- a/docs/php/builtins/array/array_pop.md +++ b/docs/php/builtins/array/array_pop.md @@ -1,8 +1,8 @@ --- title: "array_pop()" -description: "Lowers `array_pop()` for indexed arrays by mutating length and boxing `T|null` as Mixed." +description: "Pops the element off the end of array." sidebar: - order: 17 + order: 27 --- ## array_pop() @@ -11,7 +11,7 @@ sidebar: function array_pop(array $array): mixed ``` -Lowers `array_pop()` for indexed arrays by mutating length and boxing `T|null` as Mixed. +Pops the element off the end of array. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/array_product.md b/docs/php/builtins/array/array_product.md index a9da7b382c..bcf9ca0e28 100644 --- a/docs/php/builtins/array/array_product.md +++ b/docs/php/builtins/array/array_product.md @@ -1,22 +1,22 @@ --- title: "array_product()" -description: "Lowers `array_product()` over supported indexed-array payloads." +description: "Calculate the product of values in an array." sidebar: - order: 18 + order: 28 --- ## array_product() ```php -function array_product(array $array): float +function array_product(array $array): int ``` -Lowers `array_product()` over supported indexed-array payloads. +Calculate the product of values in an array. **Parameters**: - `$array` (`array`) -**Returns**: `float` +**Returns**: `int` _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 8661acb87f..1024eb9e30 100644 --- a/docs/php/builtins/array/array_push.md +++ b/docs/php/builtins/array/array_push.md @@ -1,8 +1,8 @@ --- title: "array_push()" -description: "Lowers `array_push()` by appending one value and publishing the mutated array." +description: "Pushes one or more elements onto the end of array." sidebar: - order: 19 + order: 29 --- ## array_push() @@ -11,7 +11,7 @@ sidebar: function array_push(array $array, ...$values): void ``` -Lowers `array_push()` by appending one value and publishing the mutated array. +Pushes one or more elements onto the end of array. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/array_rand.md b/docs/php/builtins/array/array_rand.md index 7df0941362..6194bc5301 100644 --- a/docs/php/builtins/array/array_rand.md +++ b/docs/php/builtins/array/array_rand.md @@ -1,21 +1,20 @@ --- title: "array_rand()" -description: "Lowers `array_rand()` for indexed arrays." +description: "Pick one or more random keys out of an array." sidebar: - order: 20 + order: 30 --- ## array_rand() ```php -function array_rand(array $array, int $num): int +function array_rand(array $array): int ``` -Lowers `array_rand()` for indexed arrays. +Pick one or more random keys out of an array. **Parameters**: - `$array` (`array`) -- `$num` (`int`) **Returns**: `int` diff --git a/docs/php/builtins/array/array_reduce.md b/docs/php/builtins/array/array_reduce.md index 2c39521d83..63782c9e33 100644 --- a/docs/php/builtins/array/array_reduce.md +++ b/docs/php/builtins/array/array_reduce.md @@ -1,22 +1,22 @@ --- title: "array_reduce()" -description: "Lowers `array_reduce()` through the callback-driven runtime helper." +description: "Iteratively reduces an array to a single value using a callback function." sidebar: - order: 21 + order: 31 --- ## array_reduce() ```php -function array_reduce(array $array, callable $callback, mixed $initial): int +function array_reduce(array $array, callable $callback, mixed $initial = null): int ``` -Lowers `array_reduce()` through the callback-driven runtime helper. +Iteratively reduces an array to a single value using a callback function. **Parameters**: - `$array` (`array`) - `$callback` (`callable`) -- `$initial` (`mixed`), optional +- `$initial` (`mixed`), default `null`, optional **Returns**: `int` diff --git a/docs/php/builtins/array/array_replace.md b/docs/php/builtins/array/array_replace.md new file mode 100644 index 0000000000..f5964ad250 --- /dev/null +++ b/docs/php/builtins/array/array_replace.md @@ -0,0 +1,33 @@ +--- +title: "array_replace()" +description: "Replaces elements from passed arrays into the first array." +sidebar: + order: 32 +--- + +## array_replace() + +```php +function array_replace(array $array, array $replacements): mixed +``` + +Replaces elements from passed arrays into the first array. + +**Parameters**: +- `$array` (`array`) +- `$replacements` (`array`) + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_replace` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_replace.md). + diff --git a/docs/php/builtins/array/array_replace_recursive.md b/docs/php/builtins/array/array_replace_recursive.md new file mode 100644 index 0000000000..379edc8897 --- /dev/null +++ b/docs/php/builtins/array/array_replace_recursive.md @@ -0,0 +1,33 @@ +--- +title: "array_replace_recursive()" +description: "Replaces elements from passed arrays into the first array recursively." +sidebar: + order: 33 +--- + +## array_replace_recursive() + +```php +function array_replace_recursive(array $array, array $replacements): mixed +``` + +Replaces elements from passed arrays into the first array recursively. + +**Parameters**: +- `$array` (`array`) +- `$replacements` (`array`) + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_replace_recursive` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_replace_recursive.md). + diff --git a/docs/php/builtins/array/array_reverse.md b/docs/php/builtins/array/array_reverse.md index 0e2b408150..6bd856cbd5 100644 --- a/docs/php/builtins/array/array_reverse.md +++ b/docs/php/builtins/array/array_reverse.md @@ -1,21 +1,20 @@ --- title: "array_reverse()" -description: "Lowers `array_reverse()` for indexed arrays with 8-byte payload slots." +description: "Returns an array with the elements in reverse order." sidebar: - order: 22 + order: 34 --- ## array_reverse() ```php -function array_reverse(array $array, bool $preserve_keys): array +function array_reverse(array $array): array ``` -Lowers `array_reverse()` for indexed arrays with 8-byte payload slots. +Returns an array with the elements in reverse order. **Parameters**: - `$array` (`array`) -- `$preserve_keys` (`bool`) **Returns**: `array` diff --git a/docs/php/builtins/array/array_search.md b/docs/php/builtins/array/array_search.md index 709bce75e6..f2e1a42180 100644 --- a/docs/php/builtins/array/array_search.md +++ b/docs/php/builtins/array/array_search.md @@ -1,22 +1,22 @@ --- title: "array_search()" -description: "Lowers `array_search()` for indexed arrays with integer-like payloads." +description: "Searches the array for a given value and returns the first corresponding key if successful." sidebar: - order: 23 + order: 35 --- ## array_search() ```php -function array_search(mixed $needle, array $haystack, bool $strict): mixed +function array_search(mixed $needle, array $haystack, bool $strict = false): mixed ``` -Lowers `array_search()` for indexed arrays with integer-like payloads. +Searches the array for a given value and returns the first corresponding key if successful. **Parameters**: - `$needle` (`mixed`) - `$haystack` (`array`) -- `$strict` (`bool`), optional +- `$strict` (`bool`), default `false`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/array/array_shift.md b/docs/php/builtins/array/array_shift.md index b3f2d07cbb..bd4d8dc9b5 100644 --- a/docs/php/builtins/array/array_shift.md +++ b/docs/php/builtins/array/array_shift.md @@ -1,8 +1,8 @@ --- title: "array_shift()" -description: "Lowers `array_shift()` for indexed arrays by compacting slots and boxing `T|null` as Mixed." +description: "Shifts an element off the beginning of array." sidebar: - order: 24 + order: 36 --- ## array_shift() @@ -11,7 +11,7 @@ sidebar: function array_shift(array $array): mixed ``` -Lowers `array_shift()` for indexed arrays by compacting slots and boxing `T|null` as Mixed. +Shifts an element off the beginning of array. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/array_slice.md b/docs/php/builtins/array/array_slice.md index 506326205f..8abb0326ca 100644 --- a/docs/php/builtins/array/array_slice.md +++ b/docs/php/builtins/array/array_slice.md @@ -1,23 +1,22 @@ --- title: "array_slice()" -description: "Lowers `array_slice()` for indexed arrays with pointer-sized payload slots." +description: "Extracts a slice of an array." sidebar: - order: 25 + order: 37 --- ## array_slice() ```php -function array_slice(array $array, int $offset, int $length, bool $preserve_keys): array +function array_slice(array $array, int $offset, int $length = null): array ``` -Lowers `array_slice()` for indexed arrays with pointer-sized payload slots. +Extracts a slice of an array. **Parameters**: - `$array` (`array`) - `$offset` (`int`) -- `$length` (`int`), optional -- `$preserve_keys` (`bool`) +- `$length` (`int`), default `null`, optional **Returns**: `array` diff --git a/docs/php/builtins/array/array_splice.md b/docs/php/builtins/array/array_splice.md index 8a27973ab7..eba2b7a107 100644 --- a/docs/php/builtins/array/array_splice.md +++ b/docs/php/builtins/array/array_splice.md @@ -1,23 +1,22 @@ --- title: "array_splice()" -description: "Lowers `array_splice()` by mutating an indexed source array and returning removed elements." +description: "Removes a portion of the array and replaces it with something else." sidebar: - order: 26 + order: 38 --- ## array_splice() ```php -function array_splice(array $array, int $offset, int $length, array $replacement): array +function array_splice(array $array, int $offset, int $length = null): array ``` -Lowers `array_splice()` by mutating an indexed source array and returning removed elements. +Removes a portion of the array and replaces it with something else. **Parameters**: - `$array` (`array`), passed by reference - `$offset` (`int`) -- `$length` (`int`), optional -- `$replacement` (`array`) +- `$length` (`int`), default `null`, optional **Returns**: `array` diff --git a/docs/php/builtins/array/array_sum.md b/docs/php/builtins/array/array_sum.md index 047ccd207c..775aceb2fc 100644 --- a/docs/php/builtins/array/array_sum.md +++ b/docs/php/builtins/array/array_sum.md @@ -1,22 +1,22 @@ --- title: "array_sum()" -description: "Lowers `array_sum()` over supported indexed-array payloads." +description: "Calculate the sum of values in an array." sidebar: - order: 27 + order: 39 --- ## array_sum() ```php -function array_sum(array $array): float +function array_sum(array $array): int ``` -Lowers `array_sum()` over supported indexed-array payloads. +Calculate the sum of values in an array. **Parameters**: - `$array` (`array`) -**Returns**: `float` +**Returns**: `int` _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 new file mode 100644 index 0000000000..29fed10285 --- /dev/null +++ b/docs/php/builtins/array/array_udiff.md @@ -0,0 +1,34 @@ +--- +title: "array_udiff()" +description: "Computes the difference of arrays using a callback comparator." +sidebar: + order: 40 +--- + +## array_udiff() + +```php +function array_udiff(array $array1, array $array2, callable $callback): array +``` + +Computes the difference of arrays using a callback comparator. + +**Parameters**: +- `$array1` (`array`) +- `$array2` (`array`) +- `$callback` (`callable`) + +**Returns**: `array` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_udiff` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_udiff.md). + diff --git a/docs/php/builtins/array/array_uintersect.md b/docs/php/builtins/array/array_uintersect.md new file mode 100644 index 0000000000..9ae4631678 --- /dev/null +++ b/docs/php/builtins/array/array_uintersect.md @@ -0,0 +1,34 @@ +--- +title: "array_uintersect()" +description: "Computes the intersection of arrays using a callback comparator." +sidebar: + order: 41 +--- + +## array_uintersect() + +```php +function array_uintersect(array $array1, array $array2, callable $callback): array +``` + +Computes the intersection of arrays using a callback comparator. + +**Parameters**: +- `$array1` (`array`) +- `$array2` (`array`) +- `$callback` (`callable`) + +**Returns**: `array` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_uintersect` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_uintersect.md). + diff --git a/docs/php/builtins/array/array_unique.md b/docs/php/builtins/array/array_unique.md index 0d126d80df..f69c66df25 100644 --- a/docs/php/builtins/array/array_unique.md +++ b/docs/php/builtins/array/array_unique.md @@ -1,21 +1,20 @@ --- title: "array_unique()" -description: "Lowers `array_unique()` for indexed arrays with 8-byte payload slots." +description: "Removes duplicate values from an array." sidebar: - order: 28 + order: 42 --- ## array_unique() ```php -function array_unique(array $array, int $flags): array +function array_unique(array $array): array ``` -Lowers `array_unique()` for indexed arrays with 8-byte payload slots. +Removes duplicate values from an array. **Parameters**: - `$array` (`array`) -- `$flags` (`int`) **Returns**: `array` diff --git a/docs/php/builtins/array/array_unshift.md b/docs/php/builtins/array/array_unshift.md index 2a35a8840b..cd8fa5e2ea 100644 --- a/docs/php/builtins/array/array_unshift.md +++ b/docs/php/builtins/array/array_unshift.md @@ -1,8 +1,8 @@ --- title: "array_unshift()" -description: "Lowers `array_unshift()` by ensuring uniqueness, prepending one scalar value, and returning count." +description: "Prepends one or more elements to the beginning of an array." sidebar: - order: 29 + order: 43 --- ## array_unshift() @@ -11,7 +11,7 @@ sidebar: function array_unshift(array $array, ...$values): int ``` -Lowers `array_unshift()` by ensuring uniqueness, prepending one scalar value, and returning count. +Prepends one or more elements to the beginning of an array. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/array_values.md b/docs/php/builtins/array/array_values.md index bcbec8977b..4e3a852a99 100644 --- a/docs/php/builtins/array/array_values.md +++ b/docs/php/builtins/array/array_values.md @@ -1,8 +1,8 @@ --- title: "array_values()" -description: "Lowers `array_values()` for indexed arrays as an alias or associative arrays as a new values array." +description: "Returns all the values of an array, re-indexed numerically." sidebar: - order: 30 + order: 44 --- ## array_values() @@ -11,7 +11,7 @@ sidebar: function array_values(array $array): array ``` -Lowers `array_values()` for indexed arrays as an alias or associative arrays as a new values array. +Returns all the values of an array, re-indexed numerically. **Parameters**: - `$array` (`array`) diff --git a/docs/php/builtins/array/array_walk.md b/docs/php/builtins/array/array_walk.md index 6e0ffd6787..0b6a72bec4 100644 --- a/docs/php/builtins/array/array_walk.md +++ b/docs/php/builtins/array/array_walk.md @@ -1,22 +1,21 @@ --- title: "array_walk()" -description: "Lowers `array_walk()` through the callback-driven runtime helper." +description: "Applies a user function to every member of an array." sidebar: - order: 31 + order: 45 --- ## array_walk() ```php -function array_walk(array $array, callable $callback, mixed $arg): void +function array_walk(array $array, callable $callback): void ``` -Lowers `array_walk()` through the callback-driven runtime helper. +Applies a user function to every member of an array. **Parameters**: - `$array` (`array`), passed by reference - `$callback` (`callable`) -- `$arg` (`mixed`) **Returns**: `void` diff --git a/docs/php/builtins/array/array_walk_recursive.md b/docs/php/builtins/array/array_walk_recursive.md new file mode 100644 index 0000000000..7de26032cf --- /dev/null +++ b/docs/php/builtins/array/array_walk_recursive.md @@ -0,0 +1,33 @@ +--- +title: "array_walk_recursive()" +description: "Applies a user function recursively to every member of an array." +sidebar: + order: 46 +--- + +## array_walk_recursive() + +```php +function array_walk_recursive(array $array, callable $callback): void +``` + +Applies a user function recursively to every member of an array. + +**Parameters**: +- `$array` (`array`), passed by reference +- `$callback` (`callable`) + +**Returns**: `void` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_walk_recursive` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_walk_recursive.md). + diff --git a/docs/php/builtins/array/arsort.md b/docs/php/builtins/array/arsort.md index c6c3d21df6..b13789bfc9 100644 --- a/docs/php/builtins/array/arsort.md +++ b/docs/php/builtins/array/arsort.md @@ -1,21 +1,20 @@ --- title: "arsort()" -description: "Lowers `arsort()` for indexed integer arrays through the descending value-sort wrapper." +description: "Sorts an array in descending order and maintains index association." sidebar: - order: 32 + order: 47 --- ## arsort() ```php -function arsort(array $array, int $flags): bool +function arsort(array $array): bool ``` -Lowers `arsort()` for indexed integer arrays through the descending value-sort wrapper. +Sorts an array in descending order and maintains index association. **Parameters**: - `$array` (`array`), passed by reference -- `$flags` (`int`) **Returns**: `bool` diff --git a/docs/php/builtins/array/asort.md b/docs/php/builtins/array/asort.md index f85ef46729..cf3377b9de 100644 --- a/docs/php/builtins/array/asort.md +++ b/docs/php/builtins/array/asort.md @@ -1,21 +1,20 @@ --- title: "asort()" -description: "Lowers `asort()` for indexed integer arrays through the value-sort runtime wrapper." +description: "Sorts an array and maintains index association." sidebar: - order: 33 + order: 48 --- ## asort() ```php -function asort(array $array, int $flags): bool +function asort(array $array): bool ``` -Lowers `asort()` for indexed integer arrays through the value-sort runtime wrapper. +Sorts an array and maintains index association. **Parameters**: - `$array` (`array`), passed by reference -- `$flags` (`int`) **Returns**: `bool` diff --git a/docs/php/builtins/array/call_user_func.md b/docs/php/builtins/array/call_user_func.md new file mode 100644 index 0000000000..937fc917e6 --- /dev/null +++ b/docs/php/builtins/array/call_user_func.md @@ -0,0 +1,33 @@ +--- +title: "call_user_func()" +description: "Calls a callback with the given arguments." +sidebar: + order: 49 +--- + +## call_user_func() + +```php +function call_user_func(callable $callback, ...$args): mixed +``` + +Calls a callback with the given arguments. + +**Parameters**: +- `$callback` (`callable`) +- `...$args` — variadic: collects excess arguments into `$args`. + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `call_user_func` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/call_user_func.md). + diff --git a/docs/php/builtins/array/call_user_func_array.md b/docs/php/builtins/array/call_user_func_array.md new file mode 100644 index 0000000000..e6cd18b3a5 --- /dev/null +++ b/docs/php/builtins/array/call_user_func_array.md @@ -0,0 +1,33 @@ +--- +title: "call_user_func_array()" +description: "Calls a callback with an array of parameters." +sidebar: + order: 50 +--- + +## call_user_func_array() + +```php +function call_user_func_array(callable $callback, array $args): mixed +``` + +Calls a callback with an array of parameters. + +**Parameters**: +- `$callback` (`callable`) +- `$args` (`array`) + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `call_user_func_array` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/call_user_func_array.md). + diff --git a/docs/php/builtins/array/count.md b/docs/php/builtins/array/count.md index 7679563958..a865fb07d7 100644 --- a/docs/php/builtins/array/count.md +++ b/docs/php/builtins/array/count.md @@ -1,21 +1,21 @@ --- title: "count()" -description: "Lowers `count(array)` for concrete array values by reading the runtime length header." +description: "Counts all elements in an array or Countable object." sidebar: - order: 34 + order: 51 --- ## count() ```php -function count(array $value, int $mode): int +function count(array $value, int $mode = 0): int ``` -Lowers `count(array)` for concrete array values by reading the runtime length header. +Counts all elements in an array or Countable object. **Parameters**: - `$value` (`array`) -- `$mode` (`int`), optional +- `$mode` (`int`), default `0`, optional **Returns**: `int` diff --git a/docs/php/builtins/array/in_array.md b/docs/php/builtins/array/in_array.md index 609dd9d30f..9064877063 100644 --- a/docs/php/builtins/array/in_array.md +++ b/docs/php/builtins/array/in_array.md @@ -1,24 +1,24 @@ --- title: "in_array()" -description: "Lowers `in_array()` for indexed arrays with scalar or string payloads." +description: "Checks if a value exists in an array." sidebar: - order: 35 + order: 52 --- ## in_array() ```php -function in_array(mixed $needle, array $haystack, bool $strict): mixed +function in_array(mixed $needle, array $haystack, bool $strict = false): bool ``` -Lowers `in_array()` for indexed arrays with scalar or string payloads. +Checks if a value exists in an array. **Parameters**: - `$needle` (`mixed`) - `$haystack` (`array`) -- `$strict` (`bool`), optional +- `$strict` (`bool`), default `false`, optional -**Returns**: `mixed` +**Returns**: `bool` _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 89fcefce4d..62873b00b3 100644 --- a/docs/php/builtins/array/krsort.md +++ b/docs/php/builtins/array/krsort.md @@ -1,21 +1,20 @@ --- title: "krsort()" -description: "Lowers `krsort()` through the legacy reverse key-sort helper surface." +description: "Sorts an array by key in descending order." sidebar: - order: 36 + order: 53 --- ## krsort() ```php -function krsort(array $array, int $flags): bool +function krsort(array $array): bool ``` -Lowers `krsort()` through the legacy reverse key-sort helper surface. +Sorts an array by key in descending order. **Parameters**: - `$array` (`array`), passed by reference -- `$flags` (`int`) **Returns**: `bool` diff --git a/docs/php/builtins/array/ksort.md b/docs/php/builtins/array/ksort.md index 9e7ba6669d..558cadc3c5 100644 --- a/docs/php/builtins/array/ksort.md +++ b/docs/php/builtins/array/ksort.md @@ -1,21 +1,20 @@ --- title: "ksort()" -description: "Lowers `ksort()` through the legacy key-sort helper surface." +description: "Sorts an array by key in ascending order." sidebar: - order: 37 + order: 54 --- ## ksort() ```php -function ksort(array $array, int $flags): bool +function ksort(array $array): bool ``` -Lowers `ksort()` through the legacy key-sort helper surface. +Sorts an array by key in ascending order. **Parameters**: - `$array` (`array`), passed by reference -- `$flags` (`int`) **Returns**: `bool` diff --git a/docs/php/builtins/array/natcasesort.md b/docs/php/builtins/array/natcasesort.md index 3e0e1c49b4..b4f4825c81 100644 --- a/docs/php/builtins/array/natcasesort.md +++ b/docs/php/builtins/array/natcasesort.md @@ -1,8 +1,8 @@ --- title: "natcasesort()" -description: "Lowers `natcasesort()` for indexed integer arrays through the case-insensitive wrapper." +description: "Sorts an array using a case-insensitive natural order algorithm." sidebar: - order: 38 + order: 55 --- ## natcasesort() @@ -11,7 +11,7 @@ sidebar: function natcasesort(array $array): bool ``` -Lowers `natcasesort()` for indexed integer arrays through the case-insensitive wrapper. +Sorts an array using a case-insensitive natural order algorithm. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/natsort.md b/docs/php/builtins/array/natsort.md index 13ea00e623..81eb9f565f 100644 --- a/docs/php/builtins/array/natsort.md +++ b/docs/php/builtins/array/natsort.md @@ -1,8 +1,8 @@ --- title: "natsort()" -description: "Lowers `natsort()` for indexed integer arrays through the natural-sort runtime wrapper." +description: "Sorts an array using a natural order algorithm." sidebar: - order: 39 + order: 56 --- ## natsort() @@ -11,7 +11,7 @@ sidebar: function natsort(array $array): bool ``` -Lowers `natsort()` for indexed integer arrays through the natural-sort runtime wrapper. +Sorts an array using a natural order algorithm. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/range.md b/docs/php/builtins/array/range.md index a87df6b7da..306f693278 100644 --- a/docs/php/builtins/array/range.md +++ b/docs/php/builtins/array/range.md @@ -1,22 +1,21 @@ --- title: "range()" -description: "Lowers `range()` for integer endpoints through the shared runtime constructor." +description: "Create an array containing a range of elements." sidebar: - order: 40 + order: 57 --- ## range() ```php -function range(mixed $start, mixed $end, int $step): array +function range(mixed $start, mixed $end): array ``` -Lowers `range()` for integer endpoints through the shared runtime constructor. +Create an array containing a range of elements. **Parameters**: - `$start` (`mixed`) - `$end` (`mixed`) -- `$step` (`int`) **Returns**: `array` diff --git a/docs/php/builtins/array/rsort.md b/docs/php/builtins/array/rsort.md index b70306099a..9741571ad5 100644 --- a/docs/php/builtins/array/rsort.md +++ b/docs/php/builtins/array/rsort.md @@ -1,21 +1,20 @@ --- title: "rsort()" -description: "Lowers `rsort()` for indexed integer arrays by mutating the source array in place." +description: "Sorts an array in descending order." sidebar: - order: 41 + order: 58 --- ## rsort() ```php -function rsort(array $array, int $flags): bool +function rsort(array $array): bool ``` -Lowers `rsort()` for indexed integer arrays by mutating the source array in place. +Sorts an array in descending order. **Parameters**: - `$array` (`array`), passed by reference -- `$flags` (`int`) **Returns**: `bool` diff --git a/docs/php/builtins/array/shuffle.md b/docs/php/builtins/array/shuffle.md index 9db0e1bb43..35eeb0a290 100644 --- a/docs/php/builtins/array/shuffle.md +++ b/docs/php/builtins/array/shuffle.md @@ -1,8 +1,8 @@ --- title: "shuffle()" -description: "Lowers `shuffle()` for indexed arrays with 8-byte slots by mutating the source array in place." +description: "Shuffles an array into random order." sidebar: - order: 42 + order: 59 --- ## shuffle() @@ -11,7 +11,7 @@ sidebar: function shuffle(array $array): bool ``` -Lowers `shuffle()` for indexed arrays with 8-byte slots by mutating the source array in place. +Shuffles an array into random order. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/sort.md b/docs/php/builtins/array/sort.md index 9d2aa25141..bd02d22310 100644 --- a/docs/php/builtins/array/sort.md +++ b/docs/php/builtins/array/sort.md @@ -1,21 +1,20 @@ --- title: "sort()" -description: "Lowers `sort()` for indexed integer arrays by mutating the source array in place." +description: "Sorts an array in ascending order." sidebar: - order: 43 + order: 60 --- ## sort() ```php -function sort(array $array, int $flags): bool +function sort(array $array): bool ``` -Lowers `sort()` for indexed integer arrays by mutating the source array in place. +Sorts an array in ascending order. **Parameters**: - `$array` (`array`), passed by reference -- `$flags` (`int`) **Returns**: `bool` diff --git a/docs/php/builtins/array/uasort.md b/docs/php/builtins/array/uasort.md index e491619eb2..12cd0cbf14 100644 --- a/docs/php/builtins/array/uasort.md +++ b/docs/php/builtins/array/uasort.md @@ -1,8 +1,8 @@ --- title: "uasort()" -description: "Lowers `uasort()` through the legacy user-sort helper for static comparators." +description: "Sorts an array with a user-defined comparison function and maintains index association." sidebar: - order: 44 + order: 61 --- ## uasort() @@ -11,7 +11,7 @@ sidebar: function uasort(array $array, callable $callback): bool ``` -Lowers `uasort()` through the legacy user-sort helper for static comparators. +Sorts an array with a user-defined comparison function and maintains index association. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/uksort.md b/docs/php/builtins/array/uksort.md index 5cd86d79db..4968295de1 100644 --- a/docs/php/builtins/array/uksort.md +++ b/docs/php/builtins/array/uksort.md @@ -1,8 +1,8 @@ --- title: "uksort()" -description: "Lowers `uksort()` through the legacy user-sort helper for static comparators." +description: "Sorts an array by keys using a user-defined comparison function." sidebar: - order: 45 + order: 62 --- ## uksort() @@ -11,7 +11,7 @@ sidebar: function uksort(array $array, callable $callback): bool ``` -Lowers `uksort()` through the legacy user-sort helper for static comparators. +Sorts an array by keys using a user-defined comparison function. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/array/usort.md b/docs/php/builtins/array/usort.md index eed48b11fb..76afdff10f 100644 --- a/docs/php/builtins/array/usort.md +++ b/docs/php/builtins/array/usort.md @@ -1,8 +1,8 @@ --- title: "usort()" -description: "Lowers `usort()` for indexed integer arrays with a static user comparator." +description: "Sorts an array by values using a user-defined comparison function." sidebar: - order: 46 + order: 63 --- ## usort() @@ -11,7 +11,7 @@ sidebar: function usort(array $array, callable $callback): bool ``` -Lowers `usort()` for indexed integer arrays with a static user comparator. +Sorts an array by values using a user-defined comparison function. **Parameters**: - `$array` (`array`), passed by reference diff --git a/docs/php/builtins/buffer/buffer_free.md b/docs/php/builtins/buffer/buffer_free.md index 8e0db97335..136dba1345 100644 --- a/docs/php/builtins/buffer/buffer_free.md +++ b/docs/php/builtins/buffer/buffer_free.md @@ -2,7 +2,7 @@ title: "buffer_free()" description: "Lowers `buffer_free()` through the direct buffer opcode helper." sidebar: - order: 47 + order: 64 --- ## buffer_free() diff --git a/docs/php/builtins/buffer/buffer_len.md b/docs/php/builtins/buffer/buffer_len.md index af40f1dfe9..07ff13516b 100644 --- a/docs/php/builtins/buffer/buffer_len.md +++ b/docs/php/builtins/buffer/buffer_len.md @@ -2,7 +2,7 @@ title: "buffer_len()" description: "Lowers `buffer_len()` through the direct buffer opcode helper." sidebar: - order: 48 + order: 65 --- ## buffer_len() diff --git a/docs/php/builtins/class.md b/docs/php/builtins/class.md index 1dcb36bb06..38b0b23bd1 100644 --- a/docs/php/builtins/class.md +++ b/docs/php/builtins/class.md @@ -9,22 +9,22 @@ sidebar: | Function | Signature | Returns | |---|---|---| -| [`class_alias()`](./class/class_alias.md) | `(string $class, string $alias, bool $autoload): bool` | `bool` | +| [`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): bool` | `bool` | -| [`class_get_attributes()`](./class/class_get_attributes.md) | `(string $class_name): mixed` | `mixed` | -| [`class_implements()`](./class/class_implements.md) | `(mixed $object_or_class, bool $autoload): mixed` | `mixed` | -| [`class_parents()`](./class/class_parents.md) | `(mixed $object_or_class, bool $autoload): mixed` | `mixed` | -| [`class_uses()`](./class/class_uses.md) | `(mixed $object_or_class, bool $autoload): mixed` | `mixed` | -| [`enum_exists()`](./class/enum_exists.md) | `(string $enum, bool $autoload): bool` | `bool` | +| [`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): string` | `string` | +| [`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): string` | `string` | -| [`interface_exists()`](./class/interface_exists.md) | `(string $interface, bool $autoload): bool` | `bool` | -| [`is_a()`](./class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string): bool` | `bool` | -| [`is_subclass_of()`](./class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string): bool` | `bool` | -| [`trait_exists()`](./class/trait_exists.md) | `(string $trait, bool $autoload): bool` | `bool` | +| [`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 735d2ecb7e..3afbf9fbd2 100644 --- a/docs/php/builtins/class/class_alias.md +++ b/docs/php/builtins/class/class_alias.md @@ -1,22 +1,22 @@ --- title: "class_alias()" -description: "Lowers the defensive `class_alias()` fallback that remains after AOT alias extraction." +description: "Creates an alias for a class." sidebar: - order: 49 + order: 66 --- ## class_alias() ```php -function class_alias(string $class, string $alias, bool $autoload): bool +function class_alias(string $class, string $alias, bool $autoload = true): bool ``` -Lowers the defensive `class_alias()` fallback that remains after AOT alias extraction. +Creates an alias for a class. **Parameters**: - `$class` (`string`) - `$alias` (`string`) -- `$autoload` (`bool`), optional +- `$autoload` (`bool`), default `true`, optional **Returns**: `bool` diff --git a/docs/php/builtins/class/class_attribute_args.md b/docs/php/builtins/class/class_attribute_args.md index e13f613214..554d29d965 100644 --- a/docs/php/builtins/class/class_attribute_args.md +++ b/docs/php/builtins/class/class_attribute_args.md @@ -1,8 +1,8 @@ --- title: "class_attribute_args()" -description: "Lowers `class_attribute_args(class, attr)` into an indexed Mixed array." +description: "Returns the constructor arguments of a named attribute applied to a class." sidebar: - order: 50 + order: 67 --- ## class_attribute_args() @@ -11,7 +11,7 @@ sidebar: function class_attribute_args(string $class_name, string $attribute_name): array ``` -Lowers `class_attribute_args(class, attr)` into an indexed Mixed array. +Returns the constructor arguments of a named attribute applied to a class. **Parameters**: - `$class_name` (`string`) diff --git a/docs/php/builtins/class/class_attribute_names.md b/docs/php/builtins/class/class_attribute_names.md index dca8e06324..78fb12b96c 100644 --- a/docs/php/builtins/class/class_attribute_names.md +++ b/docs/php/builtins/class/class_attribute_names.md @@ -1,8 +1,8 @@ --- title: "class_attribute_names()" -description: "Lowers `class_attribute_names(class)` into an indexed string array." +description: "Returns the list of attribute names applied to a class." sidebar: - order: 51 + order: 68 --- ## class_attribute_names() @@ -11,7 +11,7 @@ sidebar: function class_attribute_names(string $class_name): array ``` -Lowers `class_attribute_names(class)` into an indexed string array. +Returns the list of attribute names applied to a class. **Parameters**: - `$class_name` (`string`) diff --git a/docs/php/builtins/class/class_exists.md b/docs/php/builtins/class/class_exists.md index 79a28325f8..2fad0f1dcd 100644 --- a/docs/php/builtins/class/class_exists.md +++ b/docs/php/builtins/class/class_exists.md @@ -2,20 +2,20 @@ title: "class_exists()" description: "Checks whether the given class has been defined." sidebar: - order: 52 + order: 69 --- ## class_exists() ```php -function class_exists(string $class, bool $autoload): bool +function class_exists(string $class, bool $autoload = true): bool ``` Checks whether the given class has been defined. **Parameters**: - `$class` (`string`) -- `$autoload` (`bool`), optional +- `$autoload` (`bool`), default `true`, optional **Returns**: `bool` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `class_exists` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/class_exists.md). + diff --git a/docs/php/builtins/class/class_get_attributes.md b/docs/php/builtins/class/class_get_attributes.md index 6e83206c2c..1baa768aab 100644 --- a/docs/php/builtins/class/class_get_attributes.md +++ b/docs/php/builtins/class/class_get_attributes.md @@ -1,22 +1,22 @@ --- title: "class_get_attributes()" -description: "Lowers `class_get_attributes(class)` into an array of `ReflectionAttribute` objects." +description: "Returns an array of ReflectionAttribute objects for all attributes of a class." sidebar: - order: 53 + order: 70 --- ## class_get_attributes() ```php -function class_get_attributes(string $class_name): mixed +function class_get_attributes(string $class_name): array ``` -Lowers `class_get_attributes(class)` into an array of `ReflectionAttribute` objects. +Returns an array of ReflectionAttribute objects for all attributes of a class. **Parameters**: - `$class_name` (`string`) -**Returns**: `mixed` +**Returns**: `array` _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 ce2048988a..138b873524 100644 --- a/docs/php/builtins/class/class_implements.md +++ b/docs/php/builtins/class/class_implements.md @@ -1,21 +1,21 @@ --- title: "class_implements()" -description: "class_implements() — class builtin supported by Elephc." +description: "Returns the interfaces which are implemented by the given class or its parents." sidebar: - order: 54 + order: 71 --- ## class_implements() ```php -function class_implements(mixed $object_or_class, bool $autoload): mixed +function class_implements(mixed $object_or_class, bool $autoload = true): mixed ``` -`class_implements()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the interfaces which are implemented by the given class or its parents. **Parameters**: - `$object_or_class` (`mixed`) -- `$autoload` (`bool`), optional +- `$autoload` (`bool`), default `true`, optional **Returns**: `mixed` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `class_implements` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/class_implements.md). + diff --git a/docs/php/builtins/class/class_parents.md b/docs/php/builtins/class/class_parents.md index 75f5b9d93f..8bf00fa244 100644 --- a/docs/php/builtins/class/class_parents.md +++ b/docs/php/builtins/class/class_parents.md @@ -1,21 +1,21 @@ --- title: "class_parents()" -description: "class_parents() — class builtin supported by Elephc." +description: "Returns the parent classes of the given class." sidebar: - order: 55 + order: 72 --- ## class_parents() ```php -function class_parents(mixed $object_or_class, bool $autoload): mixed +function class_parents(mixed $object_or_class, bool $autoload = true): mixed ``` -`class_parents()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the parent classes of the given class. **Parameters**: - `$object_or_class` (`mixed`) -- `$autoload` (`bool`), optional +- `$autoload` (`bool`), default `true`, optional **Returns**: `mixed` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `class_parents` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/class_parents.md). + diff --git a/docs/php/builtins/class/class_uses.md b/docs/php/builtins/class/class_uses.md index 6bd354052b..b2da4b4185 100644 --- a/docs/php/builtins/class/class_uses.md +++ b/docs/php/builtins/class/class_uses.md @@ -1,21 +1,21 @@ --- title: "class_uses()" -description: "class_uses() — class builtin supported by Elephc." +description: "Returns the traits used by the given class." sidebar: - order: 56 + order: 73 --- ## class_uses() ```php -function class_uses(mixed $object_or_class, bool $autoload): mixed +function class_uses(mixed $object_or_class, bool $autoload = true): mixed ``` -`class_uses()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the traits used by the given class. **Parameters**: - `$object_or_class` (`mixed`) -- `$autoload` (`bool`), optional +- `$autoload` (`bool`), default `true`, optional **Returns**: `mixed` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `class_uses` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/class_uses.md). + diff --git a/docs/php/builtins/class/enum_exists.md b/docs/php/builtins/class/enum_exists.md index f0537c4404..a1389d8667 100644 --- a/docs/php/builtins/class/enum_exists.md +++ b/docs/php/builtins/class/enum_exists.md @@ -1,21 +1,21 @@ --- title: "enum_exists()" -description: "enum_exists() — class builtin supported by Elephc." +description: "Checks if the enum has been defined." sidebar: - order: 57 + order: 74 --- ## enum_exists() ```php -function enum_exists(string $enum, bool $autoload): bool +function enum_exists(string $enum, bool $autoload = true): bool ``` -`enum_exists()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks if the enum has been defined. **Parameters**: - `$enum` (`string`) -- `$autoload` (`bool`), optional +- `$autoload` (`bool`), default `true`, optional **Returns**: `bool` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `enum_exists` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/enum_exists.md). + diff --git a/docs/php/builtins/class/function_exists.md b/docs/php/builtins/class/function_exists.md index 97dd99bba0..e358d8e6d0 100644 --- a/docs/php/builtins/class/function_exists.md +++ b/docs/php/builtins/class/function_exists.md @@ -1,8 +1,8 @@ --- title: "function_exists()" -description: "Lowers `function_exists(\"name\")` for compile-time string names." +description: "Returns true if the given function has been defined." sidebar: - order: 58 + order: 75 --- ## function_exists() @@ -11,7 +11,7 @@ sidebar: function function_exists(string $function): bool ``` -Lowers `function_exists("name")` for compile-time string names. +Returns true if the given function has been defined. **Parameters**: - `$function` (`string`) diff --git a/docs/php/builtins/class/get_class.md b/docs/php/builtins/class/get_class.md index 6934374b10..082cda5c3e 100644 --- a/docs/php/builtins/class/get_class.md +++ b/docs/php/builtins/class/get_class.md @@ -1,20 +1,20 @@ --- title: "get_class()" -description: "get_class() — class builtin supported by Elephc." +description: "Returns the name of the class of an object." sidebar: - order: 59 + order: 76 --- ## get_class() ```php -function get_class(object $object): string +function get_class(object $object = null): string ``` -`get_class()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the name of the class of an object. **Parameters**: -- `$object` (`object`), optional +- `$object` (`object`), default `null`, optional **Returns**: `string` @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `get_class` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/get_class.md). + diff --git a/docs/php/builtins/class/get_declared_classes.md b/docs/php/builtins/class/get_declared_classes.md index 9f8a8da8d3..150f682d7e 100644 --- a/docs/php/builtins/class/get_declared_classes.md +++ b/docs/php/builtins/class/get_declared_classes.md @@ -1,8 +1,8 @@ --- title: "get_declared_classes()" -description: "get_declared_classes() — class builtin supported by Elephc." +description: "Returns an array of the names of the defined classes." sidebar: - order: 60 + order: 77 --- ## get_declared_classes() @@ -11,7 +11,7 @@ sidebar: function get_declared_classes(): array ``` -`get_declared_classes()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns an array of the names of the defined classes. **Parameters**: none. @@ -25,3 +25,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `get_declared_classes` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/get_declared_classes.md). + diff --git a/docs/php/builtins/class/get_declared_interfaces.md b/docs/php/builtins/class/get_declared_interfaces.md index 8c466956fa..0994cbbb8c 100644 --- a/docs/php/builtins/class/get_declared_interfaces.md +++ b/docs/php/builtins/class/get_declared_interfaces.md @@ -1,8 +1,8 @@ --- title: "get_declared_interfaces()" -description: "get_declared_interfaces() — class builtin supported by Elephc." +description: "Returns an array of all declared interfaces." sidebar: - order: 61 + order: 78 --- ## get_declared_interfaces() @@ -11,7 +11,7 @@ sidebar: function get_declared_interfaces(): array ``` -`get_declared_interfaces()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns an array of all declared interfaces. **Parameters**: none. @@ -25,3 +25,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `get_declared_interfaces` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/get_declared_interfaces.md). + diff --git a/docs/php/builtins/class/get_declared_traits.md b/docs/php/builtins/class/get_declared_traits.md index 35c17ad588..0c4cc105b9 100644 --- a/docs/php/builtins/class/get_declared_traits.md +++ b/docs/php/builtins/class/get_declared_traits.md @@ -1,8 +1,8 @@ --- title: "get_declared_traits()" -description: "get_declared_traits() — class builtin supported by Elephc." +description: "Returns an array of all declared traits." sidebar: - order: 62 + order: 79 --- ## get_declared_traits() @@ -11,7 +11,7 @@ sidebar: function get_declared_traits(): array ``` -`get_declared_traits()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns an array of all declared traits. **Parameters**: none. @@ -25,3 +25,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `get_declared_traits` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/get_declared_traits.md). + diff --git a/docs/php/builtins/class/get_parent_class.md b/docs/php/builtins/class/get_parent_class.md index 3012dc737f..35ae146525 100644 --- a/docs/php/builtins/class/get_parent_class.md +++ b/docs/php/builtins/class/get_parent_class.md @@ -1,20 +1,20 @@ --- title: "get_parent_class()" -description: "get_parent_class() — class builtin supported by Elephc." +description: "Returns the name of the parent class of an object or class." sidebar: - order: 63 + order: 80 --- ## get_parent_class() ```php -function get_parent_class(mixed $object_or_class): string +function get_parent_class(mixed $object_or_class = null): string ``` -`get_parent_class()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the name of the parent class of an object or class. **Parameters**: -- `$object_or_class` (`mixed`), optional +- `$object_or_class` (`mixed`), default `null`, optional **Returns**: `string` @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `get_parent_class` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/get_parent_class.md). + diff --git a/docs/php/builtins/class/interface_exists.md b/docs/php/builtins/class/interface_exists.md index 961c3dc4fa..3e39f7d827 100644 --- a/docs/php/builtins/class/interface_exists.md +++ b/docs/php/builtins/class/interface_exists.md @@ -1,21 +1,21 @@ --- title: "interface_exists()" -description: "interface_exists() — class builtin supported by Elephc." +description: "Checks if the interface has been defined." sidebar: - order: 64 + order: 81 --- ## interface_exists() ```php -function interface_exists(string $interface, bool $autoload): bool +function interface_exists(string $interface, bool $autoload = true): bool ``` -`interface_exists()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks if the interface has been defined. **Parameters**: - `$interface` (`string`) -- `$autoload` (`bool`), optional +- `$autoload` (`bool`), default `true`, optional **Returns**: `bool` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `interface_exists` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/interface_exists.md). + diff --git a/docs/php/builtins/class/is_a.md b/docs/php/builtins/class/is_a.md index ff0eb87d10..6eda7d1fac 100644 --- a/docs/php/builtins/class/is_a.md +++ b/docs/php/builtins/class/is_a.md @@ -1,22 +1,22 @@ --- title: "is_a()" -description: "is_a() — class builtin supported by Elephc." +description: "Checks whether an object is of a given type or has it as one of its parents." sidebar: - order: 65 + order: 82 --- ## is_a() ```php -function is_a(object $object_or_class, string $class, bool $allow_string): bool +function is_a(object $object_or_class, string $class, bool $allow_string = false): bool ``` -`is_a()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks whether an object is of a given type or has it as one of its parents. **Parameters**: - `$object_or_class` (`object`) - `$class` (`string`) -- `$allow_string` (`bool`), optional +- `$allow_string` (`bool`), default `false`, optional **Returns**: `bool` @@ -28,3 +28,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `is_a` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/is_a.md). + diff --git a/docs/php/builtins/class/is_subclass_of.md b/docs/php/builtins/class/is_subclass_of.md index a106efd3e9..72822ee334 100644 --- a/docs/php/builtins/class/is_subclass_of.md +++ b/docs/php/builtins/class/is_subclass_of.md @@ -1,22 +1,22 @@ --- title: "is_subclass_of()" -description: "is_subclass_of() — class builtin supported by Elephc." +description: "Checks if the object has a given class as one of its parents or implements it." sidebar: - order: 66 + order: 83 --- ## is_subclass_of() ```php -function is_subclass_of(mixed $object_or_class, string $class, bool $allow_string): bool +function is_subclass_of(mixed $object_or_class, string $class, bool $allow_string = true): bool ``` -`is_subclass_of()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks if the object has a given class as one of its parents or implements it. **Parameters**: - `$object_or_class` (`mixed`) - `$class` (`string`) -- `$allow_string` (`bool`), optional +- `$allow_string` (`bool`), default `true`, optional **Returns**: `bool` @@ -28,3 +28,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `is_subclass_of` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/is_subclass_of.md). + diff --git a/docs/php/builtins/class/trait_exists.md b/docs/php/builtins/class/trait_exists.md index 5353b1e9dc..de92f3a1c8 100644 --- a/docs/php/builtins/class/trait_exists.md +++ b/docs/php/builtins/class/trait_exists.md @@ -1,21 +1,21 @@ --- title: "trait_exists()" -description: "trait_exists() — class builtin supported by Elephc." +description: "Checks whether the trait exists." sidebar: - order: 67 + order: 84 --- ## trait_exists() ```php -function trait_exists(string $trait, bool $autoload): bool +function trait_exists(string $trait, bool $autoload = true): bool ``` -`trait_exists()` is a class builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks whether the trait exists. **Parameters**: - `$trait` (`string`) -- `$autoload` (`bool`), optional +- `$autoload` (`bool`), default `true`, optional **Returns**: `bool` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `trait_exists` is implemented in the compiler, see [the internals page](../../../internals/builtins/class/trait_exists.md). + diff --git a/docs/php/builtins/date.md b/docs/php/builtins/date.md index f90f6a8893..d5c0b90298 100644 --- a/docs/php/builtins/date.md +++ b/docs/php/builtins/date.md @@ -10,15 +10,15 @@ sidebar: | Function | Signature | Returns | |---|---|---| | [`checkdate()`](./date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | -| [`date()`](./date/date.md) | `(string $format, int $timestamp): string` | `string` | +| [`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): array` | `array` | -| [`gmdate()`](./date/gmdate.md) | `(string $format, int $timestamp): string` | `string` | +| [`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): mixed` | `mixed` | -| [`localtime()`](./date/localtime.md) | `(int $timestamp, bool $associative): array` | `array` | -| [`microtime()`](./date/microtime.md) | `(bool $as_float): 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): mixed` | `mixed` | +| [`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 6c8c220b05..3f9f33d68f 100644 --- a/docs/php/builtins/date/checkdate.md +++ b/docs/php/builtins/date/checkdate.md @@ -1,8 +1,8 @@ --- title: "checkdate()" -description: "Lowers `checkdate(month, day, year)` through the shared Gregorian-validation runtime helper." +description: "Validates a Gregorian date." sidebar: - order: 68 + order: 85 --- ## checkdate() @@ -11,7 +11,7 @@ sidebar: function checkdate(int $month, int $day, int $year): bool ``` -Lowers `checkdate(month, day, year)` through the shared Gregorian-validation runtime helper. +Validates a Gregorian date. **Parameters**: - `$month` (`int`) diff --git a/docs/php/builtins/date/date.md b/docs/php/builtins/date/date.md index ee8553ff59..8251d76f38 100644 --- a/docs/php/builtins/date/date.md +++ b/docs/php/builtins/date/date.md @@ -1,21 +1,21 @@ --- title: "date()" -description: "Lowers `date(format, timestamp?)` through the shared formatter runtime helper." +description: "Formats a local time/date." sidebar: - order: 69 + order: 86 --- ## date() ```php -function date(string $format, int $timestamp): string +function date(string $format, int $timestamp = null): string ``` -Lowers `date(format, timestamp?)` through the shared formatter runtime helper. +Formats a local time/date. **Parameters**: - `$format` (`string`) -- `$timestamp` (`int`), optional +- `$timestamp` (`int`), default `null`, optional **Returns**: `string` diff --git a/docs/php/builtins/date/date_default_timezone_get.md b/docs/php/builtins/date/date_default_timezone_get.md index 656acb7332..52e9b0f470 100644 --- a/docs/php/builtins/date/date_default_timezone_get.md +++ b/docs/php/builtins/date/date_default_timezone_get.md @@ -1,8 +1,8 @@ --- title: "date_default_timezone_get()" -description: "Lowers `date_default_timezone_get()` through the shared runtime helper." +description: "Gets the default timezone." sidebar: - order: 70 + order: 87 --- ## date_default_timezone_get() @@ -11,7 +11,7 @@ sidebar: function date_default_timezone_get(): string ``` -Lowers `date_default_timezone_get()` through the shared runtime helper. +Gets the default timezone. **Parameters**: none. diff --git a/docs/php/builtins/date/date_default_timezone_set.md b/docs/php/builtins/date/date_default_timezone_set.md index 897253d238..8f83807597 100644 --- a/docs/php/builtins/date/date_default_timezone_set.md +++ b/docs/php/builtins/date/date_default_timezone_set.md @@ -1,8 +1,8 @@ --- title: "date_default_timezone_set()" -description: "Lowers `date_default_timezone_set(timezoneId)` through the shared runtime helper." +description: "Sets the default timezone." sidebar: - order: 71 + order: 88 --- ## date_default_timezone_set() @@ -11,7 +11,7 @@ sidebar: function date_default_timezone_set(string $timezoneId): bool ``` -Lowers `date_default_timezone_set(timezoneId)` through the shared runtime helper. +Sets the default timezone. **Parameters**: - `$timezoneId` (`string`) diff --git a/docs/php/builtins/date/getdate.md b/docs/php/builtins/date/getdate.md index 7b1abee859..ac3b17ed08 100644 --- a/docs/php/builtins/date/getdate.md +++ b/docs/php/builtins/date/getdate.md @@ -1,20 +1,20 @@ --- title: "getdate()" -description: "Lowers `getdate([$timestamp])` through the shared decomposition runtime helper." +description: "Returns date/time information." sidebar: - order: 72 + order: 89 --- ## getdate() ```php -function getdate(int $timestamp): array +function getdate(int $timestamp = null): array ``` -Lowers `getdate([$timestamp])` through the shared decomposition runtime helper. +Returns date/time information. **Parameters**: -- `$timestamp` (`int`), optional +- `$timestamp` (`int`), default `null`, optional **Returns**: `array` diff --git a/docs/php/builtins/date/gmdate.md b/docs/php/builtins/date/gmdate.md index 4c0e02afcb..58cc570ae0 100644 --- a/docs/php/builtins/date/gmdate.md +++ b/docs/php/builtins/date/gmdate.md @@ -1,21 +1,21 @@ --- title: "gmdate()" -description: "Lowers `gmdate(format[, timestamp])`: the UTC counterpart of `date()`." +description: "Formats a GMT/UTC date and time." sidebar: - order: 73 + order: 90 --- ## gmdate() ```php -function gmdate(string $format, int $timestamp): string +function gmdate(string $format, int $timestamp = null): string ``` -Lowers `gmdate(format[, timestamp])`: the UTC counterpart of `date()`. +Formats a GMT/UTC date and time. **Parameters**: - `$format` (`string`) -- `$timestamp` (`int`), optional +- `$timestamp` (`int`), default `null`, optional **Returns**: `string` diff --git a/docs/php/builtins/date/gmmktime.md b/docs/php/builtins/date/gmmktime.md index 7243f47661..69b8f37f89 100644 --- a/docs/php/builtins/date/gmmktime.md +++ b/docs/php/builtins/date/gmmktime.md @@ -1,8 +1,8 @@ --- title: "gmmktime()" -description: "Lowers `gmmktime(...)`: the UTC counterpart of `mktime()`." +description: "Returns the Unix timestamp for a GMT date." sidebar: - order: 74 + order: 91 --- ## gmmktime() @@ -11,7 +11,7 @@ sidebar: function gmmktime(int $hour, int $minute, int $second, int $month, int $day, int $year): int ``` -Lowers `gmmktime(...)`: the UTC counterpart of `mktime()`. +Returns the Unix timestamp for a GMT date. **Parameters**: - `$hour` (`int`) diff --git a/docs/php/builtins/date/hrtime.md b/docs/php/builtins/date/hrtime.md index 74af0c3f4e..764f21dffc 100644 --- a/docs/php/builtins/date/hrtime.md +++ b/docs/php/builtins/date/hrtime.md @@ -1,20 +1,20 @@ --- title: "hrtime()" -description: "Lowers `hrtime([$as_number])` through the monotonic-clock runtime helper." +description: "Returns the current high-resolution time." sidebar: - order: 75 + order: 92 --- ## hrtime() ```php -function hrtime(bool $as_number): mixed +function hrtime(bool $as_number = false): mixed ``` -Lowers `hrtime([$as_number])` through the monotonic-clock runtime helper. +Returns the current high-resolution time. **Parameters**: -- `$as_number` (`bool`), optional +- `$as_number` (`bool`), default `false`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/date/localtime.md b/docs/php/builtins/date/localtime.md index 9e75f9dff6..7afafc1967 100644 --- a/docs/php/builtins/date/localtime.md +++ b/docs/php/builtins/date/localtime.md @@ -1,21 +1,21 @@ --- title: "localtime()" -description: "Lowers `localtime([$timestamp[, $associative]])` through the shared decomposition runtime helper." +description: "Returns the local time." sidebar: - order: 76 + order: 93 --- ## localtime() ```php -function localtime(int $timestamp, bool $associative): array +function localtime(int $timestamp = -1, bool $associative = false): array ``` -Lowers `localtime([$timestamp[, $associative]])` through the shared decomposition runtime helper. +Returns the local time. **Parameters**: -- `$timestamp` (`int`), optional -- `$associative` (`bool`), optional +- `$timestamp` (`int`), default `-1`, optional +- `$associative` (`bool`), default `false`, optional **Returns**: `array` diff --git a/docs/php/builtins/date/microtime.md b/docs/php/builtins/date/microtime.md index 4594c5c060..22abe5ed1f 100644 --- a/docs/php/builtins/date/microtime.md +++ b/docs/php/builtins/date/microtime.md @@ -1,22 +1,22 @@ --- title: "microtime()" -description: "Lowers `microtime()` / `microtime(true)` / `microtime(false)` / `microtime($flag)`." +description: "Returns the current Unix timestamp with microseconds." sidebar: - order: 77 + order: 94 --- ## microtime() ```php -function microtime(bool $as_float): int +function microtime(bool $as_float = false): mixed ``` -Lowers `microtime()` / `microtime(true)` / `microtime(false)` / `microtime($flag)`. +Returns the current Unix timestamp with microseconds. **Parameters**: -- `$as_float` (`bool`), optional +- `$as_float` (`bool`), default `false`, optional -**Returns**: `int` +**Returns**: `mixed` _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 a7966d9acd..109320d678 100644 --- a/docs/php/builtins/date/mktime.md +++ b/docs/php/builtins/date/mktime.md @@ -1,8 +1,8 @@ --- title: "mktime()" -description: "Lowers `mktime(hour, minute, second, month, day, year)` through the runtime helper." +description: "Returns the Unix timestamp for a date." sidebar: - order: 78 + order: 95 --- ## mktime() @@ -11,7 +11,7 @@ sidebar: function mktime(int $hour, int $minute, int $second, int $month, int $day, int $year): int ``` -Lowers `mktime(hour, minute, second, month, day, year)` through the runtime helper. +Returns the Unix timestamp for a date. **Parameters**: - `$hour` (`int`) diff --git a/docs/php/builtins/date/strtotime.md b/docs/php/builtins/date/strtotime.md index 6144ee199c..b367f34f5c 100644 --- a/docs/php/builtins/date/strtotime.md +++ b/docs/php/builtins/date/strtotime.md @@ -1,21 +1,21 @@ --- title: "strtotime()" -description: "Lowers `strtotime(datetime[, baseTimestamp])` through the shared parser runtime helper." +description: "Parses an English textual datetime description into a Unix timestamp." sidebar: - order: 79 + order: 96 --- ## strtotime() ```php -function strtotime(string $datetime, int $baseTimestamp): mixed +function strtotime(string $datetime, int $baseTimestamp = null): mixed ``` -Lowers `strtotime(datetime[, baseTimestamp])` through the shared parser runtime helper. +Parses an English textual datetime description into a Unix timestamp. **Parameters**: - `$datetime` (`string`) -- `$baseTimestamp` (`int`), optional +- `$baseTimestamp` (`int`), default `null`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/date/time.md b/docs/php/builtins/date/time.md index d9a2656171..a100865cf0 100644 --- a/docs/php/builtins/date/time.md +++ b/docs/php/builtins/date/time.md @@ -1,8 +1,8 @@ --- title: "time()" -description: "Lowers `time()` through the shared wall-clock runtime helper." +description: "Returns the current Unix timestamp." sidebar: - order: 80 + order: 97 --- ## time() @@ -11,7 +11,7 @@ sidebar: function time(): int ``` -Lowers `time()` through the shared wall-clock runtime helper. +Returns the current Unix timestamp. **Parameters**: none. diff --git a/docs/php/builtins/filesystem.md b/docs/php/builtins/filesystem.md index 9689190f34..1edc01cb60 100644 --- a/docs/php/builtins/filesystem.md +++ b/docs/php/builtins/filesystem.md @@ -9,14 +9,14 @@ sidebar: | Function | Signature | Returns | |---|---|---| -| [`basename()`](./filesystem/basename.md) | `(string $path, string $suffix): string` | `string` | +| [`basename()`](./filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | | [`chdir()`](./filesystem/chdir.md) | `(string $directory): bool` | `bool` | -| [`chgrp()`](./filesystem/chgrp.md) | `(string $filename, int $group): 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, int $user): bool` | `bool` | -| [`clearstatcache()`](./filesystem/clearstatcache.md) | `(bool $clear_realpath_cache, string $filename): void` | `void` | -| [`copy()`](./filesystem/copy.md) | `(string $from, string $to, mixed $context): bool` | `bool` | -| [`dirname()`](./filesystem/dirname.md) | `(string $path, int $levels): string` | `string` | +| [`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` | @@ -29,10 +29,10 @@ sidebar: | [`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): bool` | `bool` | +| [`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, bool $local_only): mixed` | `mixed` | -| [`glob()`](./filesystem/glob.md) | `(string $pattern, int $flags): array` | `array` | +| [`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` | @@ -40,27 +40,27 @@ sidebar: | [`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, int $group): bool` | `bool` | -| [`lchown()`](./filesystem/lchown.md) | `(string $filename, int $user): 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, int $permissions, bool $recursive, bool $context): bool` | `bool` | -| [`pathinfo()`](./filesystem/pathinfo.md) | `(string $path, int $flags): 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, bool $use_include_path, mixed $context): mixed` | `mixed` | +| [`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, mixed $context): bool` | `bool` | -| [`rmdir()`](./filesystem/rmdir.md) | `(string $directory, mixed $context = null): bool` | `bool` | -| [`scandir()`](./filesystem/scandir.md) | `(string $directory, int $sorting_order, mixed $context): array` | `array` | +| [`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, int $atime): bool` | `bool` | -| [`umask()`](./filesystem/umask.md) | `(int $mask): int` | `int` | +| [`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 2e236dabd6..b7fcae60c7 100644 --- a/docs/php/builtins/filesystem/basename.md +++ b/docs/php/builtins/filesystem/basename.md @@ -1,21 +1,21 @@ --- title: "basename()" -description: "Lowers `basename(path, suffix?)` through the target-aware runtime helper." +description: "Returns the trailing name component of a path." sidebar: - order: 81 + order: 98 --- ## basename() ```php -function basename(string $path, string $suffix): string +function basename(string $path, string $suffix = ''): string ``` -Lowers `basename(path, suffix?)` through the target-aware runtime helper. +Returns the trailing name component of a path. **Parameters**: - `$path` (`string`) -- `$suffix` (`string`), optional +- `$suffix` (`string`), default `''`, optional **Returns**: `string` diff --git a/docs/php/builtins/filesystem/chdir.md b/docs/php/builtins/filesystem/chdir.md index 7c6ad31368..79eb4bd9bb 100644 --- a/docs/php/builtins/filesystem/chdir.md +++ b/docs/php/builtins/filesystem/chdir.md @@ -1,8 +1,8 @@ --- title: "chdir()" -description: "Lowers `chdir(path)` through the target-aware runtime helper." +description: "Changes the current directory." sidebar: - order: 82 + order: 99 --- ## chdir() @@ -11,7 +11,7 @@ sidebar: function chdir(string $directory): bool ``` -Lowers `chdir(path)` through the target-aware runtime helper. +Changes the current directory. **Parameters**: - `$directory` (`string`) diff --git a/docs/php/builtins/filesystem/chgrp.md b/docs/php/builtins/filesystem/chgrp.md index f85124f9c1..a5f7d23632 100644 --- a/docs/php/builtins/filesystem/chgrp.md +++ b/docs/php/builtins/filesystem/chgrp.md @@ -1,21 +1,21 @@ --- title: "chgrp()" -description: "Lowers `chgrp(path, group)` for integer GIDs and string group names." +description: "Changes file group." sidebar: - order: 83 + order: 100 --- ## chgrp() ```php -function chgrp(string $filename, int $group): bool +function chgrp(string $filename, string $group): bool ``` -Lowers `chgrp(path, group)` for integer GIDs and string group names. +Changes file group. **Parameters**: - `$filename` (`string`) -- `$group` (`int`) +- `$group` (`string`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/chmod.md b/docs/php/builtins/filesystem/chmod.md index bf66bc2fdf..23a5012e9e 100644 --- a/docs/php/builtins/filesystem/chmod.md +++ b/docs/php/builtins/filesystem/chmod.md @@ -1,8 +1,8 @@ --- title: "chmod()" -description: "Lowers `chmod(path, mode)` through the target-aware runtime helper." +description: "Changes file mode." sidebar: - order: 84 + order: 101 --- ## chmod() @@ -11,7 +11,7 @@ sidebar: function chmod(string $filename, int $permissions): bool ``` -Lowers `chmod(path, mode)` through the target-aware runtime helper. +Changes file mode. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/chown.md b/docs/php/builtins/filesystem/chown.md index ce1e097850..26d53c1398 100644 --- a/docs/php/builtins/filesystem/chown.md +++ b/docs/php/builtins/filesystem/chown.md @@ -1,21 +1,21 @@ --- title: "chown()" -description: "Lowers `chown(path, owner)` for integer UIDs and string user names." +description: "Changes file owner." sidebar: - order: 85 + order: 102 --- ## chown() ```php -function chown(string $filename, int $user): bool +function chown(string $filename, string $user): bool ``` -Lowers `chown(path, owner)` for integer UIDs and string user names. +Changes file owner. **Parameters**: - `$filename` (`string`) -- `$user` (`int`) +- `$user` (`string`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/clearstatcache.md b/docs/php/builtins/filesystem/clearstatcache.md index 9ab178bb77..47f99f901c 100644 --- a/docs/php/builtins/filesystem/clearstatcache.md +++ b/docs/php/builtins/filesystem/clearstatcache.md @@ -1,21 +1,21 @@ --- title: "clearstatcache()" -description: "Lowers `clearstatcache(...)` as an ordered no-op after EIR operand evaluation." +description: "Clears file status cache." sidebar: - order: 86 + order: 103 --- ## clearstatcache() ```php -function clearstatcache(bool $clear_realpath_cache, string $filename): void +function clearstatcache(bool $clear_realpath_cache = false, string $filename = ''): void ``` -Lowers `clearstatcache(...)` as an ordered no-op after EIR operand evaluation. +Clears file status cache. **Parameters**: -- `$clear_realpath_cache` (`bool`), optional -- `$filename` (`string`), optional +- `$clear_realpath_cache` (`bool`), default `false`, optional +- `$filename` (`string`), default `''`, optional **Returns**: `void` diff --git a/docs/php/builtins/filesystem/copy.md b/docs/php/builtins/filesystem/copy.md index ba7916871c..3d08aa2588 100644 --- a/docs/php/builtins/filesystem/copy.md +++ b/docs/php/builtins/filesystem/copy.md @@ -1,22 +1,21 @@ --- title: "copy()" -description: "Lowers `copy(source, dest)` through the target-aware runtime helper." +description: "Copies a file." sidebar: - order: 87 + order: 104 --- ## copy() ```php -function copy(string $from, string $to, mixed $context): bool +function copy(string $from, string $to): bool ``` -Lowers `copy(source, dest)` through the target-aware runtime helper. +Copies a file. **Parameters**: - `$from` (`string`) - `$to` (`string`) -- `$context` (`mixed`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/dirname.md b/docs/php/builtins/filesystem/dirname.md index 29130f25cb..4fe76dc5f5 100644 --- a/docs/php/builtins/filesystem/dirname.md +++ b/docs/php/builtins/filesystem/dirname.md @@ -1,21 +1,21 @@ --- title: "dirname()" -description: "Lowers `dirname(path, levels?)` through the target-aware runtime helper." +description: "Returns a parent directory's path." sidebar: - order: 88 + order: 105 --- ## dirname() ```php -function dirname(string $path, int $levels): string +function dirname(string $path, int $levels = 1): string ``` -Lowers `dirname(path, levels?)` through the target-aware runtime helper. +Returns a parent directory's path. **Parameters**: - `$path` (`string`) -- `$levels` (`int`), optional +- `$levels` (`int`), default `1`, optional **Returns**: `string` diff --git a/docs/php/builtins/filesystem/disk_free_space.md b/docs/php/builtins/filesystem/disk_free_space.md index c61e5d35af..a13b08e44d 100644 --- a/docs/php/builtins/filesystem/disk_free_space.md +++ b/docs/php/builtins/filesystem/disk_free_space.md @@ -1,8 +1,8 @@ --- title: "disk_free_space()" -description: "Lowers `disk_free_space(path)` through the shared disk-space runtime helper." +description: "Returns available space on filesystem or disk partition." sidebar: - order: 89 + order: 106 --- ## disk_free_space() @@ -11,7 +11,7 @@ sidebar: function disk_free_space(string $directory): float ``` -Lowers `disk_free_space(path)` through the shared disk-space runtime helper. +Returns available space on filesystem or disk partition. **Parameters**: - `$directory` (`string`) diff --git a/docs/php/builtins/filesystem/disk_total_space.md b/docs/php/builtins/filesystem/disk_total_space.md index 704c9054cd..b774d51383 100644 --- a/docs/php/builtins/filesystem/disk_total_space.md +++ b/docs/php/builtins/filesystem/disk_total_space.md @@ -1,8 +1,8 @@ --- title: "disk_total_space()" -description: "Lowers `disk_total_space(path)` through the shared disk-space runtime helper." +description: "Returns the total size of a filesystem or disk partition." sidebar: - order: 90 + order: 107 --- ## disk_total_space() @@ -11,7 +11,7 @@ sidebar: function disk_total_space(string $directory): float ``` -Lowers `disk_total_space(path)` through the shared disk-space runtime helper. +Returns the total size of a filesystem or disk partition. **Parameters**: - `$directory` (`string`) diff --git a/docs/php/builtins/filesystem/file_exists.md b/docs/php/builtins/filesystem/file_exists.md index 66c40286ae..526d596428 100644 --- a/docs/php/builtins/filesystem/file_exists.md +++ b/docs/php/builtins/filesystem/file_exists.md @@ -1,8 +1,8 @@ --- title: "file_exists()" -description: "Lowers `file_exists(path)` through the target-aware runtime stat helper." +description: "Checks whether a file or directory exists." sidebar: - order: 91 + order: 108 --- ## file_exists() @@ -11,7 +11,7 @@ sidebar: function file_exists(string $filename): bool ``` -Lowers `file_exists(path)` through the target-aware runtime stat helper. +Checks whether a file or directory exists. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/fileatime.md b/docs/php/builtins/filesystem/fileatime.md index f65f31a763..61b1f09e2d 100644 --- a/docs/php/builtins/filesystem/fileatime.md +++ b/docs/php/builtins/filesystem/fileatime.md @@ -1,8 +1,8 @@ --- title: "fileatime()" -description: "Lowers `fileatime(path)` and boxes the runtime integer-or-false result." +description: "Gets last access time of file." sidebar: - order: 92 + order: 109 --- ## fileatime() @@ -11,7 +11,7 @@ sidebar: function fileatime(string $filename): mixed ``` -Lowers `fileatime(path)` and boxes the runtime integer-or-false result. +Gets last access time of file. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/filectime.md b/docs/php/builtins/filesystem/filectime.md index d11c5e8a2b..ea697c09f0 100644 --- a/docs/php/builtins/filesystem/filectime.md +++ b/docs/php/builtins/filesystem/filectime.md @@ -1,8 +1,8 @@ --- title: "filectime()" -description: "Lowers `filectime(path)` and boxes the runtime integer-or-false result." +description: "Gets inode change time of file." sidebar: - order: 93 + order: 110 --- ## filectime() @@ -11,7 +11,7 @@ sidebar: function filectime(string $filename): mixed ``` -Lowers `filectime(path)` and boxes the runtime integer-or-false result. +Gets inode change time of file. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/filegroup.md b/docs/php/builtins/filesystem/filegroup.md index 20b3af7a3a..18bfc38b86 100644 --- a/docs/php/builtins/filesystem/filegroup.md +++ b/docs/php/builtins/filesystem/filegroup.md @@ -1,8 +1,8 @@ --- title: "filegroup()" -description: "Lowers `filegroup(path)` and boxes the runtime integer-or-false result." +description: "Gets file group." sidebar: - order: 94 + order: 111 --- ## filegroup() @@ -11,7 +11,7 @@ sidebar: function filegroup(string $filename): mixed ``` -Lowers `filegroup(path)` and boxes the runtime integer-or-false result. +Gets file group. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/fileinode.md b/docs/php/builtins/filesystem/fileinode.md index 8586ef112e..2110a291ef 100644 --- a/docs/php/builtins/filesystem/fileinode.md +++ b/docs/php/builtins/filesystem/fileinode.md @@ -1,8 +1,8 @@ --- title: "fileinode()" -description: "Lowers `fileinode(path)` and boxes the runtime integer-or-false result." +description: "Gets file inode." sidebar: - order: 95 + order: 112 --- ## fileinode() @@ -11,7 +11,7 @@ sidebar: function fileinode(string $filename): mixed ``` -Lowers `fileinode(path)` and boxes the runtime integer-or-false result. +Gets file inode. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/filemtime.md b/docs/php/builtins/filesystem/filemtime.md index 6f9996350b..dfd9aba90e 100644 --- a/docs/php/builtins/filesystem/filemtime.md +++ b/docs/php/builtins/filesystem/filemtime.md @@ -1,8 +1,8 @@ --- title: "filemtime()" -description: "Lowers `filemtime(path)` through the target-aware runtime stat helper." +description: "Gets file modification time." sidebar: - order: 96 + order: 113 --- ## filemtime() @@ -11,7 +11,7 @@ sidebar: function filemtime(string $filename): int ``` -Lowers `filemtime(path)` through the target-aware runtime stat helper. +Gets file modification time. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/fileowner.md b/docs/php/builtins/filesystem/fileowner.md index a68514d8e4..b49e713523 100644 --- a/docs/php/builtins/filesystem/fileowner.md +++ b/docs/php/builtins/filesystem/fileowner.md @@ -1,8 +1,8 @@ --- title: "fileowner()" -description: "Lowers `fileowner(path)` and boxes the runtime integer-or-false result." +description: "Gets file owner." sidebar: - order: 97 + order: 114 --- ## fileowner() @@ -11,7 +11,7 @@ sidebar: function fileowner(string $filename): mixed ``` -Lowers `fileowner(path)` and boxes the runtime integer-or-false result. +Gets file owner. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/fileperms.md b/docs/php/builtins/filesystem/fileperms.md index c8dad103d6..ebb7453ba1 100644 --- a/docs/php/builtins/filesystem/fileperms.md +++ b/docs/php/builtins/filesystem/fileperms.md @@ -1,8 +1,8 @@ --- title: "fileperms()" -description: "Lowers `fileperms(path)` and boxes the runtime integer-or-false result." +description: "Gets file permissions." sidebar: - order: 98 + order: 115 --- ## fileperms() @@ -11,7 +11,7 @@ sidebar: function fileperms(string $filename): mixed ``` -Lowers `fileperms(path)` and boxes the runtime integer-or-false result. +Gets file permissions. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/filesize.md b/docs/php/builtins/filesystem/filesize.md index 5952cf7dab..c655357e10 100644 --- a/docs/php/builtins/filesystem/filesize.md +++ b/docs/php/builtins/filesystem/filesize.md @@ -1,8 +1,8 @@ --- title: "filesize()" -description: "Lowers `filesize(path)` through the target-aware runtime stat helper." +description: "Gets file size." sidebar: - order: 99 + order: 116 --- ## filesize() @@ -11,7 +11,7 @@ sidebar: function filesize(string $filename): int ``` -Lowers `filesize(path)` through the target-aware runtime stat helper. +Gets file size. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/filetype.md b/docs/php/builtins/filesystem/filetype.md index 66cd54fbde..61027757ed 100644 --- a/docs/php/builtins/filesystem/filetype.md +++ b/docs/php/builtins/filesystem/filetype.md @@ -1,8 +1,8 @@ --- title: "filetype()" -description: "Lowers `filetype(path)` and boxes the runtime string-or-false result." +description: "Gets file type." sidebar: - order: 100 + order: 117 --- ## filetype() @@ -11,7 +11,7 @@ sidebar: function filetype(string $filename): mixed ``` -Lowers `filetype(path)` and boxes the runtime string-or-false result. +Gets file type. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/fnmatch.md b/docs/php/builtins/filesystem/fnmatch.md index 5446f70c83..0f8791cc5f 100644 --- a/docs/php/builtins/filesystem/fnmatch.md +++ b/docs/php/builtins/filesystem/fnmatch.md @@ -1,22 +1,22 @@ --- title: "fnmatch()" -description: "Lowers `fnmatch(pattern, filename, flags?)` through the target-aware runtime helper." +description: "Matches a filename against a pattern." sidebar: - order: 101 + order: 118 --- ## fnmatch() ```php -function fnmatch(string $pattern, string $filename, int $flags): bool +function fnmatch(string $pattern, string $filename, int $flags = 0): bool ``` -Lowers `fnmatch(pattern, filename, flags?)` through the target-aware runtime helper. +Matches a filename against a pattern. **Parameters**: - `$pattern` (`string`) - `$filename` (`string`) -- `$flags` (`int`), optional +- `$flags` (`int`), default `0`, optional **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/getcwd.md b/docs/php/builtins/filesystem/getcwd.md index 22e344daa1..330eabaefa 100644 --- a/docs/php/builtins/filesystem/getcwd.md +++ b/docs/php/builtins/filesystem/getcwd.md @@ -1,8 +1,8 @@ --- title: "getcwd()" -description: "Lowers `getcwd()` through the target-aware runtime helper." +description: "Gets the current working directory." sidebar: - order: 102 + order: 119 --- ## getcwd() @@ -11,7 +11,7 @@ sidebar: function getcwd(): string ``` -Lowers `getcwd()` through the target-aware runtime helper. +Gets the current working directory. **Parameters**: none. diff --git a/docs/php/builtins/filesystem/getenv.md b/docs/php/builtins/filesystem/getenv.md index 22d21a071a..4d5a9b30e5 100644 --- a/docs/php/builtins/filesystem/getenv.md +++ b/docs/php/builtins/filesystem/getenv.md @@ -1,21 +1,20 @@ --- title: "getenv()" -description: "Lowers `getenv(name)` through the target-aware environment lookup helper." +description: "Gets the value of an environment variable." sidebar: - order: 103 + order: 120 --- ## getenv() ```php -function getenv(string $name, bool $local_only): mixed +function getenv(string $name): mixed ``` -Lowers `getenv(name)` through the target-aware environment lookup helper. +Gets the value of an environment variable. **Parameters**: - `$name` (`string`) -- `$local_only` (`bool`) **Returns**: `mixed` diff --git a/docs/php/builtins/filesystem/glob.md b/docs/php/builtins/filesystem/glob.md index 69de45fa57..af23fa2cf9 100644 --- a/docs/php/builtins/filesystem/glob.md +++ b/docs/php/builtins/filesystem/glob.md @@ -1,21 +1,20 @@ --- title: "glob()" -description: "Lowers `glob(pattern)` through the target-aware runtime glob expansion helper." +description: "Finds pathnames matching a pattern." sidebar: - order: 104 + order: 121 --- ## glob() ```php -function glob(string $pattern, int $flags): array +function glob(string $pattern): array ``` -Lowers `glob(pattern)` through the target-aware runtime glob expansion helper. +Finds pathnames matching a pattern. **Parameters**: - `$pattern` (`string`) -- `$flags` (`int`) **Returns**: `array` diff --git a/docs/php/builtins/filesystem/is_dir.md b/docs/php/builtins/filesystem/is_dir.md index 9cc80b3398..967dd2819a 100644 --- a/docs/php/builtins/filesystem/is_dir.md +++ b/docs/php/builtins/filesystem/is_dir.md @@ -1,8 +1,8 @@ --- title: "is_dir()" -description: "Lowers `is_dir(path)` through the target-aware runtime stat helper." +description: "Tells whether the filename is a directory." sidebar: - order: 105 + order: 122 --- ## is_dir() @@ -11,7 +11,7 @@ sidebar: function is_dir(string $filename): bool ``` -Lowers `is_dir(path)` through the target-aware runtime stat helper. +Tells whether the filename is a directory. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/is_executable.md b/docs/php/builtins/filesystem/is_executable.md index f980150dcb..08b532dac1 100644 --- a/docs/php/builtins/filesystem/is_executable.md +++ b/docs/php/builtins/filesystem/is_executable.md @@ -1,8 +1,8 @@ --- title: "is_executable()" -description: "Lowers `is_executable(path)` through the target-aware runtime access helper." +description: "Tells whether the filename is executable." sidebar: - order: 106 + order: 123 --- ## is_executable() @@ -11,7 +11,7 @@ sidebar: function is_executable(string $filename): bool ``` -Lowers `is_executable(path)` through the target-aware runtime access helper. +Tells whether the filename is executable. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/is_file.md b/docs/php/builtins/filesystem/is_file.md index 4ba4c31771..b54871fded 100644 --- a/docs/php/builtins/filesystem/is_file.md +++ b/docs/php/builtins/filesystem/is_file.md @@ -1,8 +1,8 @@ --- title: "is_file()" -description: "Lowers `is_file(path)` through the target-aware runtime stat helper." +description: "Tells whether the filename is a regular file." sidebar: - order: 107 + order: 124 --- ## is_file() @@ -11,7 +11,7 @@ sidebar: function is_file(string $filename): bool ``` -Lowers `is_file(path)` through the target-aware runtime stat helper. +Tells whether the filename is a regular file. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/is_link.md b/docs/php/builtins/filesystem/is_link.md index 79d6b1c84d..96a1ee53ae 100644 --- a/docs/php/builtins/filesystem/is_link.md +++ b/docs/php/builtins/filesystem/is_link.md @@ -1,8 +1,8 @@ --- title: "is_link()" -description: "Lowers `is_link(path)` through the target-aware runtime lstat helper." +description: "Tells whether the filename is a symbolic link." sidebar: - order: 108 + order: 125 --- ## is_link() @@ -11,7 +11,7 @@ sidebar: function is_link(string $filename): bool ``` -Lowers `is_link(path)` through the target-aware runtime lstat helper. +Tells whether the filename is a symbolic link. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/is_readable.md b/docs/php/builtins/filesystem/is_readable.md index c326ec4d1b..c024a11243 100644 --- a/docs/php/builtins/filesystem/is_readable.md +++ b/docs/php/builtins/filesystem/is_readable.md @@ -1,8 +1,8 @@ --- title: "is_readable()" -description: "Lowers `is_readable(path)` through the target-aware runtime access helper." +description: "Tells whether the filename is readable." sidebar: - order: 109 + order: 126 --- ## is_readable() @@ -11,7 +11,7 @@ sidebar: function is_readable(string $filename): bool ``` -Lowers `is_readable(path)` through the target-aware runtime access helper. +Tells whether the filename is readable. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/is_writable.md b/docs/php/builtins/filesystem/is_writable.md index c456f9adea..2d121f3082 100644 --- a/docs/php/builtins/filesystem/is_writable.md +++ b/docs/php/builtins/filesystem/is_writable.md @@ -1,8 +1,8 @@ --- title: "is_writable()" -description: "Lowers `is_writable(path)` through the target-aware runtime access helper." +description: "Tells whether the filename is writable." sidebar: - order: 110 + order: 127 --- ## is_writable() @@ -11,7 +11,7 @@ sidebar: function is_writable(string $filename): bool ``` -Lowers `is_writable(path)` through the target-aware runtime access helper. +Tells whether the filename is writable. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/is_writeable.md b/docs/php/builtins/filesystem/is_writeable.md index 16e799a06d..d5c732bdcb 100644 --- a/docs/php/builtins/filesystem/is_writeable.md +++ b/docs/php/builtins/filesystem/is_writeable.md @@ -1,8 +1,8 @@ --- title: "is_writeable()" -description: "Lowers `is_writeable(path)`, PHP's alias of `is_writable(path)`." +description: "Tells whether the filename is writable (alias of is_writable)." sidebar: - order: 111 + order: 128 --- ## is_writeable() @@ -11,7 +11,7 @@ sidebar: function is_writeable(string $filename): bool ``` -Lowers `is_writeable(path)`, PHP's alias of `is_writable(path)`. +Tells whether the filename is writable (alias of is_writable). **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/lchgrp.md b/docs/php/builtins/filesystem/lchgrp.md index ae444a6d1d..60157747b1 100644 --- a/docs/php/builtins/filesystem/lchgrp.md +++ b/docs/php/builtins/filesystem/lchgrp.md @@ -1,21 +1,21 @@ --- title: "lchgrp()" -description: "Lowers `lchgrp(path, group)` for integer GIDs and string group names without following symlinks." +description: "Changes group ownership of a symlink." sidebar: - order: 112 + order: 129 --- ## lchgrp() ```php -function lchgrp(string $filename, int $group): bool +function lchgrp(string $filename, string $group): bool ``` -Lowers `lchgrp(path, group)` for integer GIDs and string group names without following symlinks. +Changes group ownership of a symlink. **Parameters**: - `$filename` (`string`) -- `$group` (`int`) +- `$group` (`string`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/lchown.md b/docs/php/builtins/filesystem/lchown.md index 0cf64a7488..7e1b51edc2 100644 --- a/docs/php/builtins/filesystem/lchown.md +++ b/docs/php/builtins/filesystem/lchown.md @@ -1,21 +1,21 @@ --- title: "lchown()" -description: "Lowers `lchown(path, owner)` for integer UIDs and string user names without following symlinks." +description: "Changes user ownership of a symlink." sidebar: - order: 113 + order: 130 --- ## lchown() ```php -function lchown(string $filename, int $user): bool +function lchown(string $filename, string $user): bool ``` -Lowers `lchown(path, owner)` for integer UIDs and string user names without following symlinks. +Changes user ownership of a symlink. **Parameters**: - `$filename` (`string`) -- `$user` (`int`) +- `$user` (`string`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/link.md b/docs/php/builtins/filesystem/link.md index c42a390ef7..a91aa299a4 100644 --- a/docs/php/builtins/filesystem/link.md +++ b/docs/php/builtins/filesystem/link.md @@ -1,8 +1,8 @@ --- title: "link()" -description: "Lowers `link(oldpath, newpath)` through the target-aware libc wrapper." +description: "Creates a hard link." sidebar: - order: 114 + order: 131 --- ## link() @@ -11,7 +11,7 @@ sidebar: function link(string $target, string $link): bool ``` -Lowers `link(oldpath, newpath)` through the target-aware libc wrapper. +Creates a hard link. **Parameters**: - `$target` (`string`) diff --git a/docs/php/builtins/filesystem/linkinfo.md b/docs/php/builtins/filesystem/linkinfo.md index 1ec551ad0d..dbf0e821e3 100644 --- a/docs/php/builtins/filesystem/linkinfo.md +++ b/docs/php/builtins/filesystem/linkinfo.md @@ -1,8 +1,8 @@ --- title: "linkinfo()" -description: "Lowers `linkinfo(path)` through the target-aware runtime lstat helper." +description: "Gets information about a link." sidebar: - order: 115 + order: 132 --- ## linkinfo() @@ -11,7 +11,7 @@ sidebar: function linkinfo(string $path): int ``` -Lowers `linkinfo(path)` through the target-aware runtime lstat helper. +Gets information about a link. **Parameters**: - `$path` (`string`) diff --git a/docs/php/builtins/filesystem/lstat.md b/docs/php/builtins/filesystem/lstat.md index 355c7c73cb..9872189521 100644 --- a/docs/php/builtins/filesystem/lstat.md +++ b/docs/php/builtins/filesystem/lstat.md @@ -1,8 +1,8 @@ --- title: "lstat()" -description: "Lowers `lstat(path)` and boxes the runtime lstat array or PHP false result." +description: "Gives information about a file or symbolic link." sidebar: - order: 116 + order: 133 --- ## lstat() @@ -11,7 +11,7 @@ sidebar: function lstat(string $filename): mixed ``` -Lowers `lstat(path)` and boxes the runtime lstat array or PHP false result. +Gives information about a file or symbolic link. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/mkdir.md b/docs/php/builtins/filesystem/mkdir.md index 1a33b7e555..b10b78219f 100644 --- a/docs/php/builtins/filesystem/mkdir.md +++ b/docs/php/builtins/filesystem/mkdir.md @@ -1,23 +1,20 @@ --- title: "mkdir()" -description: "Lowers `mkdir(path)` through the target-aware runtime helper." +description: "Makes a directory." sidebar: - order: 117 + order: 134 --- ## mkdir() ```php -function mkdir(string $directory, int $permissions, bool $recursive, bool $context): bool +function mkdir(string $directory): bool ``` -Lowers `mkdir(path)` through the target-aware runtime helper. +Makes a directory. **Parameters**: - `$directory` (`string`) -- `$permissions` (`int`) -- `$recursive` (`bool`) -- `$context` (`bool`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/pathinfo.md b/docs/php/builtins/filesystem/pathinfo.md index 90abcea3b2..6f048c5f92 100644 --- a/docs/php/builtins/filesystem/pathinfo.md +++ b/docs/php/builtins/filesystem/pathinfo.md @@ -1,23 +1,23 @@ --- title: "pathinfo()" -description: "Lowers `pathinfo(path, flags?)` through string, array, or boxed dynamic helpers." +description: "Returns information about a file path." sidebar: - order: 118 + order: 135 --- ## pathinfo() ```php -function pathinfo(string $path, int $flags): mixed +function pathinfo(string $path, int $flags = 15): array ``` -Lowers `pathinfo(path, flags?)` through string, array, or boxed dynamic helpers. +Returns information about a file path. **Parameters**: - `$path` (`string`) -- `$flags` (`int`), optional +- `$flags` (`int`), default `15`, optional -**Returns**: `mixed` +**Returns**: `array` _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 7e8c5ea429..3be34a079a 100644 --- a/docs/php/builtins/filesystem/putenv.md +++ b/docs/php/builtins/filesystem/putenv.md @@ -1,8 +1,8 @@ --- title: "putenv()" -description: "Lowers `putenv(assignment)` by copying the environment string into persistent heap storage." +description: "Sets an environment variable." sidebar: - order: 119 + order: 136 --- ## putenv() @@ -11,7 +11,7 @@ sidebar: function putenv(string $assignment): bool ``` -Lowers `putenv(assignment)` by copying the environment string into persistent heap storage. +Sets an environment variable. **Parameters**: - `$assignment` (`string`) diff --git a/docs/php/builtins/filesystem/readfile.md b/docs/php/builtins/filesystem/readfile.md index 3323edef6b..4ae8854ae2 100644 --- a/docs/php/builtins/filesystem/readfile.md +++ b/docs/php/builtins/filesystem/readfile.md @@ -1,22 +1,20 @@ --- title: "readfile()" -description: "Lowers `readfile(path)` and boxes the runtime byte-count-or-false result." +description: "Outputs a file." sidebar: - order: 120 + order: 137 --- ## readfile() ```php -function readfile(string $filename, bool $use_include_path, mixed $context): mixed +function readfile(string $filename): mixed ``` -Lowers `readfile(path)` and boxes the runtime byte-count-or-false result. +Outputs a file. **Parameters**: - `$filename` (`string`) -- `$use_include_path` (`bool`) -- `$context` (`mixed`) **Returns**: `mixed` diff --git a/docs/php/builtins/filesystem/readlink.md b/docs/php/builtins/filesystem/readlink.md index df2603debf..3225ce379b 100644 --- a/docs/php/builtins/filesystem/readlink.md +++ b/docs/php/builtins/filesystem/readlink.md @@ -1,8 +1,8 @@ --- title: "readlink()" -description: "Lowers `readlink(path)` and boxes the owned runtime string-or-false result." +description: "Returns the target of a symbolic link." sidebar: - order: 121 + order: 138 --- ## readlink() @@ -11,7 +11,7 @@ sidebar: function readlink(string $path): mixed ``` -Lowers `readlink(path)` and boxes the owned runtime string-or-false result. +Returns the target of a symbolic link. **Parameters**: - `$path` (`string`) diff --git a/docs/php/builtins/filesystem/realpath.md b/docs/php/builtins/filesystem/realpath.md index 0c81b14823..ad0d795625 100644 --- a/docs/php/builtins/filesystem/realpath.md +++ b/docs/php/builtins/filesystem/realpath.md @@ -1,8 +1,8 @@ --- title: "realpath()" -description: "Lowers `realpath(path)` and boxes the owned runtime string-or-false result." +description: "Returns canonicalized absolute pathname." sidebar: - order: 122 + order: 139 --- ## realpath() @@ -11,7 +11,7 @@ sidebar: function realpath(string $path): mixed ``` -Lowers `realpath(path)` and boxes the owned runtime string-or-false result. +Returns canonicalized absolute pathname. **Parameters**: - `$path` (`string`) diff --git a/docs/php/builtins/filesystem/realpath_cache_get.md b/docs/php/builtins/filesystem/realpath_cache_get.md index 5ab44c4e61..0780ccfffa 100644 --- a/docs/php/builtins/filesystem/realpath_cache_get.md +++ b/docs/php/builtins/filesystem/realpath_cache_get.md @@ -1,8 +1,8 @@ --- title: "realpath_cache_get()" -description: "Lowers `realpath_cache_get()` to elephc's empty realpath-cache view." +description: "Returns realpath cache entries." sidebar: - order: 123 + order: 140 --- ## realpath_cache_get() @@ -11,7 +11,7 @@ sidebar: function realpath_cache_get(): array ``` -Lowers `realpath_cache_get()` to elephc's empty realpath-cache view. +Returns realpath cache entries. **Parameters**: none. diff --git a/docs/php/builtins/filesystem/realpath_cache_size.md b/docs/php/builtins/filesystem/realpath_cache_size.md index f9b5534d52..7c7b7796e6 100644 --- a/docs/php/builtins/filesystem/realpath_cache_size.md +++ b/docs/php/builtins/filesystem/realpath_cache_size.md @@ -1,8 +1,8 @@ --- title: "realpath_cache_size()" -description: "Lowers `realpath_cache_size()` to zero because elephc has no realpath cache." +description: "Returns the amount of memory used by the realpath cache." sidebar: - order: 124 + order: 141 --- ## realpath_cache_size() @@ -11,7 +11,7 @@ sidebar: function realpath_cache_size(): int ``` -Lowers `realpath_cache_size()` to zero because elephc has no realpath cache. +Returns the amount of memory used by the realpath cache. **Parameters**: none. diff --git a/docs/php/builtins/filesystem/rename.md b/docs/php/builtins/filesystem/rename.md index 2b6ab3a6e5..0075032d5d 100644 --- a/docs/php/builtins/filesystem/rename.md +++ b/docs/php/builtins/filesystem/rename.md @@ -1,22 +1,21 @@ --- title: "rename()" -description: "Lowers `rename(from, to)` through the target-aware runtime helper." +description: "Renames a file or directory." sidebar: - order: 125 + order: 142 --- ## rename() ```php -function rename(string $from, string $to, mixed $context): bool +function rename(string $from, string $to): bool ``` -Lowers `rename(from, to)` through the target-aware runtime helper. +Renames a file or directory. **Parameters**: - `$from` (`string`) - `$to` (`string`) -- `$context` (`mixed`) **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/rmdir.md b/docs/php/builtins/filesystem/rmdir.md index 97480b75ed..ac6c8e1a93 100644 --- a/docs/php/builtins/filesystem/rmdir.md +++ b/docs/php/builtins/filesystem/rmdir.md @@ -1,21 +1,20 @@ --- title: "rmdir()" -description: "Lowers `rmdir(path)` through the target-aware runtime helper." +description: "Removes a directory." sidebar: - order: 126 + order: 143 --- ## rmdir() ```php -function rmdir(string $directory, mixed $context = null): bool +function rmdir(string $directory): bool ``` -Lowers `rmdir(path)` through the target-aware runtime helper. +Removes a directory. **Parameters**: - `$directory` (`string`) -- `$context` (`mixed`), default `null`, optional **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/scandir.md b/docs/php/builtins/filesystem/scandir.md index d020359c53..8540afa3a0 100644 --- a/docs/php/builtins/filesystem/scandir.md +++ b/docs/php/builtins/filesystem/scandir.md @@ -1,22 +1,20 @@ --- title: "scandir()" -description: "Lowers `scandir(path)` through the target-aware runtime directory listing helper." +description: "Lists files and directories inside the specified path." sidebar: - order: 127 + order: 144 --- ## scandir() ```php -function scandir(string $directory, int $sorting_order, mixed $context): array +function scandir(string $directory): array ``` -Lowers `scandir(path)` through the target-aware runtime directory listing helper. +Lists files and directories inside the specified path. **Parameters**: - `$directory` (`string`) -- `$sorting_order` (`int`) -- `$context` (`mixed`) **Returns**: `array` diff --git a/docs/php/builtins/filesystem/stat.md b/docs/php/builtins/filesystem/stat.md index 38855cb57d..3179e14368 100644 --- a/docs/php/builtins/filesystem/stat.md +++ b/docs/php/builtins/filesystem/stat.md @@ -1,8 +1,8 @@ --- title: "stat()" -description: "Lowers `stat(path)` and boxes the runtime stat array or PHP false result." +description: "Gives information about a file." sidebar: - order: 128 + order: 145 --- ## stat() @@ -11,7 +11,7 @@ sidebar: function stat(string $filename): mixed ``` -Lowers `stat(path)` and boxes the runtime stat array or PHP false result. +Gives information about a file. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/filesystem/symlink.md b/docs/php/builtins/filesystem/symlink.md index da6d795f31..7bd1e3a55c 100644 --- a/docs/php/builtins/filesystem/symlink.md +++ b/docs/php/builtins/filesystem/symlink.md @@ -1,8 +1,8 @@ --- title: "symlink()" -description: "Lowers `symlink(target, link)` through the target-aware libc wrapper." +description: "Creates a symbolic link." sidebar: - order: 129 + order: 146 --- ## symlink() @@ -11,7 +11,7 @@ sidebar: function symlink(string $target, string $link): bool ``` -Lowers `symlink(target, link)` through the target-aware libc wrapper. +Creates a symbolic link. **Parameters**: - `$target` (`string`) diff --git a/docs/php/builtins/filesystem/sys_get_temp_dir.md b/docs/php/builtins/filesystem/sys_get_temp_dir.md index 2d58033529..e910e45349 100644 --- a/docs/php/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/php/builtins/filesystem/sys_get_temp_dir.md @@ -1,8 +1,8 @@ --- title: "sys_get_temp_dir()" -description: "Lowers `sys_get_temp_dir()` as the project's hardcoded `/tmp` string." +description: "Returns the directory path used for temporary files." sidebar: - order: 130 + order: 147 --- ## sys_get_temp_dir() @@ -11,7 +11,7 @@ sidebar: function sys_get_temp_dir(): string ``` -Lowers `sys_get_temp_dir()` as the project's hardcoded `/tmp` string. +Returns the directory path used for temporary files. **Parameters**: none. diff --git a/docs/php/builtins/filesystem/tempnam.md b/docs/php/builtins/filesystem/tempnam.md index 30d316c868..a8ac18a230 100644 --- a/docs/php/builtins/filesystem/tempnam.md +++ b/docs/php/builtins/filesystem/tempnam.md @@ -1,8 +1,8 @@ --- title: "tempnam()" -description: "Lowers `tempnam(directory, prefix)` through the target-aware runtime helper." +description: "Creates a file with a unique filename." sidebar: - order: 131 + order: 148 --- ## tempnam() @@ -11,7 +11,7 @@ sidebar: function tempnam(string $directory, string $prefix): string ``` -Lowers `tempnam(directory, prefix)` through the target-aware runtime helper. +Creates a file with a unique filename. **Parameters**: - `$directory` (`string`) diff --git a/docs/php/builtins/filesystem/tmpfile.md b/docs/php/builtins/filesystem/tmpfile.md index e7178ace0f..9919570752 100644 --- a/docs/php/builtins/filesystem/tmpfile.md +++ b/docs/php/builtins/filesystem/tmpfile.md @@ -1,8 +1,8 @@ --- title: "tmpfile()" -description: "Lowers `tmpfile()` and boxes the anonymous stream descriptor or PHP false." +description: "Creates a temporary file." sidebar: - order: 132 + order: 149 --- ## tmpfile() @@ -11,7 +11,7 @@ sidebar: function tmpfile(): mixed ``` -Lowers `tmpfile()` and boxes the anonymous stream descriptor or PHP false. +Creates a temporary file. **Parameters**: none. diff --git a/docs/php/builtins/filesystem/touch.md b/docs/php/builtins/filesystem/touch.md index 2d78d049d8..7bd652ac00 100644 --- a/docs/php/builtins/filesystem/touch.md +++ b/docs/php/builtins/filesystem/touch.md @@ -1,22 +1,22 @@ --- title: "touch()" -description: "Lowers `touch(path, mtime?, atime?)` through the target-aware runtime helper." +description: "Sets access and modification time of a file." sidebar: - order: 133 + order: 150 --- ## touch() ```php -function touch(string $filename, int $mtime, int $atime): bool +function touch(string $filename, int $mtime = null, int $atime = null): bool ``` -Lowers `touch(path, mtime?, atime?)` through the target-aware runtime helper. +Sets access and modification time of a file. **Parameters**: - `$filename` (`string`) -- `$mtime` (`int`), optional -- `$atime` (`int`), optional +- `$mtime` (`int`), default `null`, optional +- `$atime` (`int`), default `null`, optional **Returns**: `bool` diff --git a/docs/php/builtins/filesystem/umask.md b/docs/php/builtins/filesystem/umask.md index 17b4d79e2b..c11ca2d250 100644 --- a/docs/php/builtins/filesystem/umask.md +++ b/docs/php/builtins/filesystem/umask.md @@ -1,20 +1,20 @@ --- title: "umask()" -description: "Lowers `umask(mask?)` through the target-aware runtime helper." +description: "Changes the current umask." sidebar: - order: 134 + order: 151 --- ## umask() ```php -function umask(int $mask): int +function umask(int $mask = null): int ``` -Lowers `umask(mask?)` through the target-aware runtime helper. +Changes the current umask. **Parameters**: -- `$mask` (`int`), optional +- `$mask` (`int`), default `null`, optional **Returns**: `int` diff --git a/docs/php/builtins/filesystem/unlink.md b/docs/php/builtins/filesystem/unlink.md index 668a0aa0bb..1910b9daa3 100644 --- a/docs/php/builtins/filesystem/unlink.md +++ b/docs/php/builtins/filesystem/unlink.md @@ -1,8 +1,8 @@ --- title: "unlink()" -description: "Lowers `unlink(path)` through the target-aware runtime helper." +description: "Deletes a file." sidebar: - order: 135 + order: 152 --- ## unlink() @@ -11,7 +11,7 @@ sidebar: function unlink(string $filename): bool ``` -Lowers `unlink(path)` through the target-aware runtime helper. +Deletes a file. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/io.md b/docs/php/builtins/io.md index a088a88551..135caf50f7 100644 --- a/docs/php/builtins/io.md +++ b/docs/php/builtins/io.md @@ -15,24 +15,24 @@ sidebar: | [`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, string $separator, string $enclosure, string $escape): array` | `array` | -| [`fgets()`](./io/fgets.md) | `(resource $stream, int $length): mixed` | `mixed` | -| [`file()`](./io/file.md) | `(string $filename, int $flags, mixed $context): array` | `array` | -| [`file_get_contents()`](./io/file_get_contents.md) | `(string $filename, bool $use_include_path, mixed $context, int $offset, int $length): mixed` | `mixed` | -| [`file_put_contents()`](./io/file_put_contents.md) | `(string $filename, mixed $data, int $flags = 0, mixed $context = null): int` | `int` | -| [`flock()`](./io/flock.md) | `(resource $stream, int $operation, bool $would_block): bool` | `bool` | -| [`fopen()`](./io/fopen.md) | `(string $filename, string $mode, bool $use_include_path, mixed $context): 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 = '"', string $escape = '\\', string $eol = '\n'): 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): int` | `int` | +| [`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 $length): int` | `int` | +| [`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` | @@ -40,49 +40,49 @@ sidebar: | [`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, array $options = []): 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, array $params): mixed` | `mixed` | -| [`stream_context_get_default()`](./io/stream_context_get_default.md) | `(array $options): mixed` | `mixed` | -| [`stream_context_get_options()`](./io/stream_context_get_options.md) | `(resource $stream_or_context): array` | `array` | +| [`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, mixed $value): bool` | `bool` | +| [`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, int $offset): mixed` | `mixed` | +| [`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, int $offset): mixed` | `mixed` | +| [`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_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): int` | `int` | +| [`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): bool` | `bool` | +| [`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, string $peer_name): mixed` | `mixed` | -| [`stream_socket_client()`](./io/stream_socket_client.md) | `(string $address, int $error_code, int $error_message, string $timeout, float $flags): mixed` | `mixed` | -| [`stream_socket_enable_crypto()`](./io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method, resource $session_stream): bool` | `bool` | +| [`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, string $address): mixed` | `mixed` | -| [`stream_socket_sendto()`](./io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags, string $address): mixed` | `mixed` | -| [`stream_socket_server()`](./io/stream_socket_server.md) | `(string $address, int $error_code, int $error_message): 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): 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 ed8d4b26c4..d2f0c88f2a 100644 --- a/docs/php/builtins/io/closedir.md +++ b/docs/php/builtins/io/closedir.md @@ -1,8 +1,8 @@ --- title: "closedir()" -description: "Lowers `closedir(dir_handle)` for libc, glob, and userspace-wrapper handles." +description: "Closes directory handle." sidebar: - order: 136 + order: 153 --- ## closedir() @@ -11,7 +11,7 @@ sidebar: function closedir(resource $dir_handle): void ``` -Lowers `closedir(dir_handle)` for libc, glob, and userspace-wrapper handles. +Closes directory handle. **Parameters**: - `$dir_handle` (`resource`) diff --git a/docs/php/builtins/io/fclose.md b/docs/php/builtins/io/fclose.md index 1b598abadc..d9c8c44990 100644 --- a/docs/php/builtins/io/fclose.md +++ b/docs/php/builtins/io/fclose.md @@ -1,8 +1,8 @@ --- title: "fclose()" -description: "Lowers `fclose(stream)` after validating and unboxing the stream handle." +description: "Closes an open file pointer." sidebar: - order: 137 + order: 154 --- ## fclose() @@ -11,7 +11,7 @@ sidebar: function fclose(resource $stream): bool ``` -Lowers `fclose(stream)` after validating and unboxing the stream handle. +Closes an open file pointer. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fdatasync.md b/docs/php/builtins/io/fdatasync.md index 1f72778234..26b0b5562e 100644 --- a/docs/php/builtins/io/fdatasync.md +++ b/docs/php/builtins/io/fdatasync.md @@ -1,8 +1,8 @@ --- title: "fdatasync()" -description: "Lowers `fdatasync(stream)` through the shared fd data-sync runtime helper." +description: "Synchronizes data (but not meta-data) to file." sidebar: - order: 138 + order: 155 --- ## fdatasync() @@ -11,7 +11,7 @@ sidebar: function fdatasync(resource $stream): bool ``` -Lowers `fdatasync(stream)` through the shared fd data-sync runtime helper. +Synchronizes data (but not meta-data) to file. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/feof.md b/docs/php/builtins/io/feof.md index 6dd04bc93a..fcb60ca74e 100644 --- a/docs/php/builtins/io/feof.md +++ b/docs/php/builtins/io/feof.md @@ -1,8 +1,8 @@ --- title: "feof()" -description: "Lowers `feof(stream)` through the runtime EOF-flag table helper." +description: "Tests for end-of-file on a file pointer." sidebar: - order: 139 + order: 156 --- ## feof() @@ -11,7 +11,7 @@ sidebar: function feof(resource $stream): bool ``` -Lowers `feof(stream)` through the runtime EOF-flag table helper. +Tests for end-of-file on a file pointer. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fflush.md b/docs/php/builtins/io/fflush.md index 414bf77371..ba052577bc 100644 --- a/docs/php/builtins/io/fflush.md +++ b/docs/php/builtins/io/fflush.md @@ -1,8 +1,8 @@ --- title: "fflush()" -description: "Lowers `fflush(stream)` through the shared fd flush runtime helper." +description: "Flushes the output to a file." sidebar: - order: 140 + order: 157 --- ## fflush() @@ -11,7 +11,7 @@ sidebar: function fflush(resource $stream): bool ``` -Lowers `fflush(stream)` through the shared fd flush runtime helper. +Flushes the output to a file. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fgetc.md b/docs/php/builtins/io/fgetc.md index ac11449344..f69c10d508 100644 --- a/docs/php/builtins/io/fgetc.md +++ b/docs/php/builtins/io/fgetc.md @@ -1,8 +1,8 @@ --- title: "fgetc()" -description: "Lowers `fgetc(stream)` and boxes the one-byte string or PHP false result." +description: "Gets a character from the given file pointer." sidebar: - order: 141 + order: 158 --- ## fgetc() @@ -11,7 +11,7 @@ sidebar: function fgetc(resource $stream): mixed ``` -Lowers `fgetc(stream)` and boxes the one-byte string or PHP false result. +Gets a character from the given file pointer. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fgetcsv.md b/docs/php/builtins/io/fgetcsv.md index 84571aad5d..4baf3d3a40 100644 --- a/docs/php/builtins/io/fgetcsv.md +++ b/docs/php/builtins/io/fgetcsv.md @@ -1,24 +1,22 @@ --- title: "fgetcsv()" -description: "Lowers `fgetcsv(stream, separator?, enclosure?)` through the CSV row runtime helper." +description: "Gets line from file pointer and parse for CSV fields." sidebar: - order: 142 + order: 159 --- ## fgetcsv() ```php -function fgetcsv(resource $stream, int $length, string $separator, string $enclosure, string $escape): array +function fgetcsv(resource $stream, int $length = null, string $separator = ','): array ``` -Lowers `fgetcsv(stream, separator?, enclosure?)` through the CSV row runtime helper. +Gets line from file pointer and parse for CSV fields. **Parameters**: - `$stream` (`resource`) -- `$length` (`int`), optional -- `$separator` (`string`), optional -- `$enclosure` (`string`) -- `$escape` (`string`) +- `$length` (`int`), default `null`, optional +- `$separator` (`string`), default `','`, optional **Returns**: `array` diff --git a/docs/php/builtins/io/fgets.md b/docs/php/builtins/io/fgets.md index c1132dafea..2150096f36 100644 --- a/docs/php/builtins/io/fgets.md +++ b/docs/php/builtins/io/fgets.md @@ -1,21 +1,20 @@ --- title: "fgets()" -description: "Lowers `fgets(stream)` through the shared line-read runtime helper." +description: "Gets line from file pointer." sidebar: - order: 143 + order: 160 --- ## fgets() ```php -function fgets(resource $stream, int $length): mixed +function fgets(resource $stream): mixed ``` -Lowers `fgets(stream)` through the shared line-read runtime helper. +Gets line from file pointer. **Parameters**: - `$stream` (`resource`) -- `$length` (`int`) **Returns**: `mixed` diff --git a/docs/php/builtins/io/file.md b/docs/php/builtins/io/file.md index 6ccbb4e83e..b6176accd9 100644 --- a/docs/php/builtins/io/file.md +++ b/docs/php/builtins/io/file.md @@ -1,22 +1,20 @@ --- title: "file()" -description: "Lowers `file(path)` through the target-aware runtime line-array helper." +description: "Reads an entire file into an array." sidebar: - order: 144 + order: 161 --- ## file() ```php -function file(string $filename, int $flags, mixed $context): array +function file(string $filename): array ``` -Lowers `file(path)` through the target-aware runtime line-array helper. +Reads an entire file into an array. **Parameters**: - `$filename` (`string`) -- `$flags` (`int`) -- `$context` (`mixed`) **Returns**: `array` diff --git a/docs/php/builtins/io/file_get_contents.md b/docs/php/builtins/io/file_get_contents.md index d85f744797..e8d8a6fc69 100644 --- a/docs/php/builtins/io/file_get_contents.md +++ b/docs/php/builtins/io/file_get_contents.md @@ -1,24 +1,20 @@ --- title: "file_get_contents()" -description: "Lowers `file_get_contents(path)` and boxes the runtime string-or-false result." +description: "Reads an entire file into a string." sidebar: - order: 145 + order: 162 --- ## file_get_contents() ```php -function file_get_contents(string $filename, bool $use_include_path, mixed $context, int $offset, int $length): mixed +function file_get_contents(string $filename): mixed ``` -Lowers `file_get_contents(path)` and boxes the runtime string-or-false result. +Reads an entire file into a string. **Parameters**: - `$filename` (`string`) -- `$use_include_path` (`bool`) -- `$context` (`mixed`) -- `$offset` (`int`) -- `$length` (`int`) **Returns**: `mixed` diff --git a/docs/php/builtins/io/file_put_contents.md b/docs/php/builtins/io/file_put_contents.md index 12517fdf10..0cebb841a2 100644 --- a/docs/php/builtins/io/file_put_contents.md +++ b/docs/php/builtins/io/file_put_contents.md @@ -1,23 +1,21 @@ --- title: "file_put_contents()" -description: "Lowers `file_put_contents(path, data)` through the target-aware runtime writer." +description: "Writes data to a file." sidebar: - order: 146 + order: 163 --- ## file_put_contents() ```php -function file_put_contents(string $filename, mixed $data, int $flags = 0, mixed $context = null): int +function file_put_contents(string $filename, string $data): int ``` -Lowers `file_put_contents(path, data)` through the target-aware runtime writer. +Writes data to a file. **Parameters**: - `$filename` (`string`) -- `$data` (`mixed`) -- `$flags` (`int`), default `0`, optional -- `$context` (`mixed`), default `null`, optional +- `$data` (`string`) **Returns**: `int` diff --git a/docs/php/builtins/io/flock.md b/docs/php/builtins/io/flock.md index 8c3d14e075..efd46e23e8 100644 --- a/docs/php/builtins/io/flock.md +++ b/docs/php/builtins/io/flock.md @@ -1,22 +1,22 @@ --- title: "flock()" -description: "Lowers `flock(stream, operation, would_block?)` through the libc flock wrapper." +description: "Portable advisory file locking." sidebar: - order: 147 + order: 164 --- ## flock() ```php -function flock(resource $stream, int $operation, bool $would_block): bool +function flock(resource $stream, int $operation, bool $would_block = null): bool ``` -Lowers `flock(stream, operation, would_block?)` through the libc flock wrapper. +Portable advisory file locking. **Parameters**: - `$stream` (`resource`) - `$operation` (`int`) -- `$would_block` (`bool`), passed by reference, optional +- `$would_block` (`bool`), passed by reference, default `null`, optional **Returns**: `bool` diff --git a/docs/php/builtins/io/fopen.md b/docs/php/builtins/io/fopen.md index b43c87cc94..706973d49d 100644 --- a/docs/php/builtins/io/fopen.md +++ b/docs/php/builtins/io/fopen.md @@ -1,23 +1,23 @@ --- title: "fopen()" -description: "Lowers `fopen(filename, mode)` and boxes stream resources or PHP false." +description: "Opens file or URL." sidebar: - order: 148 + order: 165 --- ## fopen() ```php -function fopen(string $filename, string $mode, bool $use_include_path, mixed $context): mixed +function fopen(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed ``` -Lowers `fopen(filename, mode)` and boxes stream resources or PHP false. +Opens file or URL. **Parameters**: - `$filename` (`string`) - `$mode` (`string`) -- `$use_include_path` (`bool`), optional -- `$context` (`mixed`), optional +- `$use_include_path` (`bool`), default `false`, optional +- `$context` (`mixed`), default `null`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/fpassthru.md b/docs/php/builtins/io/fpassthru.md index d67b3d3d16..676aa59290 100644 --- a/docs/php/builtins/io/fpassthru.md +++ b/docs/php/builtins/io/fpassthru.md @@ -1,8 +1,8 @@ --- title: "fpassthru()" -description: "Lowers `fpassthru(stream)` through the remaining-bytes stream runtime helper." +description: "Output all remaining data on a file pointer." sidebar: - order: 149 + order: 166 --- ## fpassthru() @@ -11,7 +11,7 @@ sidebar: function fpassthru(resource $stream): int ``` -Lowers `fpassthru(stream)` through the remaining-bytes stream runtime helper. +Output all remaining data on a file pointer. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fprintf.md b/docs/php/builtins/io/fprintf.md index 772fc6064a..f9ceafb81e 100644 --- a/docs/php/builtins/io/fprintf.md +++ b/docs/php/builtins/io/fprintf.md @@ -1,8 +1,8 @@ --- title: "fprintf()" -description: "Lowers `fprintf(stream, format, values...)` as `sprintf()` plus stream write." +description: "Write a formatted string to a stream." sidebar: - order: 150 + order: 167 --- ## fprintf() @@ -11,7 +11,7 @@ sidebar: function fprintf(resource $stream, string $format, ...$values): int ``` -Lowers `fprintf(stream, format, values...)` as `sprintf()` plus stream write. +Write a formatted string to a stream. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fputcsv.md b/docs/php/builtins/io/fputcsv.md index 25f7b6d898..3f004533f4 100644 --- a/docs/php/builtins/io/fputcsv.md +++ b/docs/php/builtins/io/fputcsv.md @@ -1,25 +1,23 @@ --- title: "fputcsv()" -description: "Lowers `fputcsv(stream, fields, separator?, enclosure?)` for string arrays." +description: "Format line as CSV and write to file pointer." sidebar: - order: 151 + order: 168 --- ## fputcsv() ```php -function fputcsv(resource $stream, array $fields, string $separator = ',', string $enclosure = '"', string $escape = '\\', string $eol = '\n'): int +function fputcsv(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int ``` -Lowers `fputcsv(stream, fields, separator?, enclosure?)` for string arrays. +Format line as CSV and write to file pointer. **Parameters**: - `$stream` (`resource`) - `$fields` (`array`) - `$separator` (`string`), default `','`, optional - `$enclosure` (`string`), default `'"'`, optional -- `$escape` (`string`), default `'\\'`, optional -- `$eol` (`string`), default `'\n'`, optional **Returns**: `int` diff --git a/docs/php/builtins/io/fread.md b/docs/php/builtins/io/fread.md index 088ef4668b..17dbb2afc9 100644 --- a/docs/php/builtins/io/fread.md +++ b/docs/php/builtins/io/fread.md @@ -1,8 +1,8 @@ --- title: "fread()" -description: "Lowers `fread(stream, length)` using the shared runtime file-read helper." +description: "Binary-safe file read." sidebar: - order: 152 + order: 169 --- ## fread() @@ -11,7 +11,7 @@ sidebar: function fread(resource $stream, int $length): string ``` -Lowers `fread(stream, length)` using the shared runtime file-read helper. +Binary-safe file read. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fscanf.md b/docs/php/builtins/io/fscanf.md index c3214190fb..67d2cd9551 100644 --- a/docs/php/builtins/io/fscanf.md +++ b/docs/php/builtins/io/fscanf.md @@ -1,8 +1,8 @@ --- title: "fscanf()" -description: "Lowers `fscanf(stream, format)` through `__rt_fgets` and `__rt_sscanf`." +description: "Parses input from a file according to a format." sidebar: - order: 153 + order: 170 --- ## fscanf() @@ -11,7 +11,7 @@ sidebar: function fscanf(resource $stream, string $format, ...$vars): array ``` -Lowers `fscanf(stream, format)` through `__rt_fgets` and `__rt_sscanf`. +Parses input from a file according to a format. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fseek.md b/docs/php/builtins/io/fseek.md index 4780f460c0..66fea389be 100644 --- a/docs/php/builtins/io/fseek.md +++ b/docs/php/builtins/io/fseek.md @@ -1,22 +1,22 @@ --- title: "fseek()" -description: "Lowers `fseek(stream, offset, whence?)` and clears EOF state on success." +description: "Seeks on a file pointer." sidebar: - order: 154 + order: 171 --- ## fseek() ```php -function fseek(resource $stream, int $offset, int $whence): int +function fseek(resource $stream, int $offset, int $whence = 0): int ``` -Lowers `fseek(stream, offset, whence?)` and clears EOF state on success. +Seeks on a file pointer. **Parameters**: - `$stream` (`resource`) - `$offset` (`int`) -- `$whence` (`int`), optional +- `$whence` (`int`), default `0`, optional **Returns**: `int` diff --git a/docs/php/builtins/io/fstat.md b/docs/php/builtins/io/fstat.md index fc5f96b10d..673d0c0302 100644 --- a/docs/php/builtins/io/fstat.md +++ b/docs/php/builtins/io/fstat.md @@ -1,8 +1,8 @@ --- title: "fstat()" -description: "Lowers `fstat(stream)` and boxes the runtime stat array or PHP false result." +description: "Gets information about a file using an open file pointer." sidebar: - order: 155 + order: 172 --- ## fstat() @@ -11,7 +11,7 @@ sidebar: function fstat(resource $stream): mixed ``` -Lowers `fstat(stream)` and boxes the runtime stat array or PHP false result. +Gets information about a file using an open file pointer. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fsync.md b/docs/php/builtins/io/fsync.md index 2c9a709eb4..58a001a4b6 100644 --- a/docs/php/builtins/io/fsync.md +++ b/docs/php/builtins/io/fsync.md @@ -1,8 +1,8 @@ --- title: "fsync()" -description: "Lowers `fsync(stream)` through the shared fd sync runtime helper." +description: "Synchronizes changes to the file (including meta-data)." sidebar: - order: 156 + order: 173 --- ## fsync() @@ -11,7 +11,7 @@ sidebar: function fsync(resource $stream): bool ``` -Lowers `fsync(stream)` through the shared fd sync runtime helper. +Synchronizes changes to the file (including meta-data). **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/ftell.md b/docs/php/builtins/io/ftell.md index 8849fb75ff..5fc9fee511 100644 --- a/docs/php/builtins/io/ftell.md +++ b/docs/php/builtins/io/ftell.md @@ -1,8 +1,8 @@ --- title: "ftell()" -description: "Lowers `ftell(stream)` as `lseek(fd, 0, SEEK_CUR)`." +description: "Returns the current position of the file read/write pointer." sidebar: - order: 157 + order: 174 --- ## ftell() @@ -11,7 +11,7 @@ sidebar: function ftell(resource $stream): int ``` -Lowers `ftell(stream)` as `lseek(fd, 0, SEEK_CUR)`. +Returns the current position of the file read/write pointer. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/ftruncate.md b/docs/php/builtins/io/ftruncate.md index 23e3799e0d..cfa0d10725 100644 --- a/docs/php/builtins/io/ftruncate.md +++ b/docs/php/builtins/io/ftruncate.md @@ -1,8 +1,8 @@ --- title: "ftruncate()" -description: "Lowers `ftruncate(stream, size)` through the shared fd truncate runtime helper." +description: "Truncates a file to a given length." sidebar: - order: 158 + order: 175 --- ## ftruncate() @@ -11,7 +11,7 @@ sidebar: function ftruncate(resource $stream, int $size): bool ``` -Lowers `ftruncate(stream, size)` through the shared fd truncate runtime helper. +Truncates a file to a given length. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/fwrite.md b/docs/php/builtins/io/fwrite.md index 94db0e707f..3edc50c485 100644 --- a/docs/php/builtins/io/fwrite.md +++ b/docs/php/builtins/io/fwrite.md @@ -1,22 +1,21 @@ --- title: "fwrite()" -description: "Lowers `fwrite(stream, data)` and returns the number of bytes written." +description: "Binary-safe file write." sidebar: - order: 159 + order: 176 --- ## fwrite() ```php -function fwrite(resource $stream, string $data, int $length): int +function fwrite(resource $stream, string $data): int ``` -Lowers `fwrite(stream, data)` and returns the number of bytes written. +Binary-safe file write. **Parameters**: - `$stream` (`resource`) - `$data` (`string`) -- `$length` (`int`) **Returns**: `int` diff --git a/docs/php/builtins/io/gethostbyaddr.md b/docs/php/builtins/io/gethostbyaddr.md index 3eeea88fcc..740bd536b8 100644 --- a/docs/php/builtins/io/gethostbyaddr.md +++ b/docs/php/builtins/io/gethostbyaddr.md @@ -1,8 +1,8 @@ --- title: "gethostbyaddr()" -description: "Lowers `gethostbyaddr(address)` and boxes malformed addresses as PHP `false`." +description: "Gets the Internet host name corresponding to a given IP address." sidebar: - order: 160 + order: 177 --- ## gethostbyaddr() @@ -11,7 +11,7 @@ sidebar: function gethostbyaddr(string $ip): mixed ``` -Lowers `gethostbyaddr(address)` and boxes malformed addresses as PHP `false`. +Gets the Internet host name corresponding to a given IP address. **Parameters**: - `$ip` (`string`) diff --git a/docs/php/builtins/io/gethostbyname.md b/docs/php/builtins/io/gethostbyname.md index d457771454..669006764a 100644 --- a/docs/php/builtins/io/gethostbyname.md +++ b/docs/php/builtins/io/gethostbyname.md @@ -1,8 +1,8 @@ --- title: "gethostbyname()" -description: "Lowers `gethostbyname(hostname)` through the shared runtime resolver." +description: "Gets the IPv4 address corresponding to the given Internet host name." sidebar: - order: 161 + order: 178 --- ## gethostbyname() @@ -11,7 +11,7 @@ sidebar: function gethostbyname(string $hostname): string ``` -Lowers `gethostbyname(hostname)` through the shared runtime resolver. +Gets the IPv4 address corresponding to the given Internet host name. **Parameters**: - `$hostname` (`string`) diff --git a/docs/php/builtins/io/gethostname.md b/docs/php/builtins/io/gethostname.md index dddd62481a..102c48204e 100644 --- a/docs/php/builtins/io/gethostname.md +++ b/docs/php/builtins/io/gethostname.md @@ -1,8 +1,8 @@ --- title: "gethostname()" -description: "Lowers `gethostname()` through the shared runtime helper." +description: "Gets the standard host name for the local machine." sidebar: - order: 162 + order: 179 --- ## gethostname() @@ -11,7 +11,7 @@ sidebar: function gethostname(): string ``` -Lowers `gethostname()` through the shared runtime helper. +Gets the standard host name for the local machine. **Parameters**: none. diff --git a/docs/php/builtins/io/getprotobyname.md b/docs/php/builtins/io/getprotobyname.md index 3efcd91b0a..bddcbae93a 100644 --- a/docs/php/builtins/io/getprotobyname.md +++ b/docs/php/builtins/io/getprotobyname.md @@ -1,8 +1,8 @@ --- title: "getprotobyname()" -description: "Lowers `getprotobyname(protocol)` and boxes a missing entry as PHP `false`." +description: "Gets the protocol number associated with the given protocol name." sidebar: - order: 163 + order: 180 --- ## getprotobyname() @@ -11,7 +11,7 @@ sidebar: function getprotobyname(string $protocol): mixed ``` -Lowers `getprotobyname(protocol)` and boxes a missing entry as PHP `false`. +Gets the protocol number associated with the given protocol name. **Parameters**: - `$protocol` (`string`) diff --git a/docs/php/builtins/io/getprotobynumber.md b/docs/php/builtins/io/getprotobynumber.md index 0c5245c1e6..14de680e4a 100644 --- a/docs/php/builtins/io/getprotobynumber.md +++ b/docs/php/builtins/io/getprotobynumber.md @@ -1,8 +1,8 @@ --- title: "getprotobynumber()" -description: "Lowers `getprotobynumber(number)` and boxes a missing entry as PHP `false`." +description: "Gets the protocol name associated with the given protocol number." sidebar: - order: 164 + order: 181 --- ## getprotobynumber() @@ -11,7 +11,7 @@ sidebar: function getprotobynumber(int $protocol): mixed ``` -Lowers `getprotobynumber(number)` and boxes a missing entry as PHP `false`. +Gets the protocol name associated with the given protocol number. **Parameters**: - `$protocol` (`int`) diff --git a/docs/php/builtins/io/getservbyname.md b/docs/php/builtins/io/getservbyname.md index 0a83def186..92114158d2 100644 --- a/docs/php/builtins/io/getservbyname.md +++ b/docs/php/builtins/io/getservbyname.md @@ -1,8 +1,8 @@ --- title: "getservbyname()" -description: "Lowers `getservbyname(service, protocol)` and boxes a missing entry as PHP `false`." +description: "Gets port number associated with an Internet service and protocol." sidebar: - order: 165 + order: 182 --- ## getservbyname() @@ -11,7 +11,7 @@ sidebar: function getservbyname(string $service, string $protocol): mixed ``` -Lowers `getservbyname(service, protocol)` and boxes a missing entry as PHP `false`. +Gets port number associated with an Internet service and protocol. **Parameters**: - `$service` (`string`) diff --git a/docs/php/builtins/io/getservbyport.md b/docs/php/builtins/io/getservbyport.md index df13a6c953..ac10941245 100644 --- a/docs/php/builtins/io/getservbyport.md +++ b/docs/php/builtins/io/getservbyport.md @@ -1,8 +1,8 @@ --- title: "getservbyport()" -description: "Lowers `getservbyport(port, protocol)` and boxes a missing entry as PHP `false`." +description: "Gets the Internet service that corresponds to a port and protocol." sidebar: - order: 166 + order: 183 --- ## getservbyport() @@ -11,7 +11,7 @@ sidebar: function getservbyport(int $port, string $protocol): mixed ``` -Lowers `getservbyport(port, protocol)` and boxes a missing entry as PHP `false`. +Gets the Internet service that corresponds to a port and protocol. **Parameters**: - `$port` (`int`) diff --git a/docs/php/builtins/io/hash_file.md b/docs/php/builtins/io/hash_file.md index 5bf645692f..7307465076 100644 --- a/docs/php/builtins/io/hash_file.md +++ b/docs/php/builtins/io/hash_file.md @@ -1,23 +1,22 @@ --- title: "hash_file()" -description: "Lowers `hash_file(algo, filename, binary?)` by reading bytes then hashing them." +description: "Generates a hash value using the contents of a given file." sidebar: - order: 167 + order: 184 --- ## hash_file() ```php -function hash_file(string $algo, string $filename, bool $binary = false, array $options = []): mixed +function hash_file(string $algo, string $filename, bool $binary = false): mixed ``` -Lowers `hash_file(algo, filename, binary?)` by reading bytes then hashing them. +Generates a hash value using the contents of a given file. **Parameters**: - `$algo` (`string`) - `$filename` (`string`) - `$binary` (`bool`), default `false`, optional -- `$options` (`array`), default `[]`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/opendir.md b/docs/php/builtins/io/opendir.md index 5526b7bc46..8666aaa8ec 100644 --- a/docs/php/builtins/io/opendir.md +++ b/docs/php/builtins/io/opendir.md @@ -1,8 +1,8 @@ --- title: "opendir()" -description: "Lowers `opendir(path)` and boxes the directory stream as `resource|false`." +description: "Open directory handle." sidebar: - order: 168 + order: 185 --- ## opendir() @@ -11,7 +11,7 @@ sidebar: function opendir(string $directory): mixed ``` -Lowers `opendir(path)` and boxes the directory stream as `resource|false`. +Open directory handle. **Parameters**: - `$directory` (`string`) diff --git a/docs/php/builtins/io/readdir.md b/docs/php/builtins/io/readdir.md index de19712664..2626d69f9e 100644 --- a/docs/php/builtins/io/readdir.md +++ b/docs/php/builtins/io/readdir.md @@ -1,8 +1,8 @@ --- title: "readdir()" -description: "Lowers `readdir(dir_handle)` for libc, glob, and userspace-wrapper handles." +description: "Read entry from directory handle." sidebar: - order: 169 + order: 186 --- ## readdir() @@ -11,7 +11,7 @@ sidebar: function readdir(resource $dir_handle): mixed ``` -Lowers `readdir(dir_handle)` for libc, glob, and userspace-wrapper handles. +Read entry from directory handle. **Parameters**: - `$dir_handle` (`resource`) diff --git a/docs/php/builtins/io/rewind.md b/docs/php/builtins/io/rewind.md index 87d6688a8e..af56e86a73 100644 --- a/docs/php/builtins/io/rewind.md +++ b/docs/php/builtins/io/rewind.md @@ -1,8 +1,8 @@ --- title: "rewind()" -description: "Lowers `rewind(stream)` as `lseek(fd, 0, SEEK_SET)` and clears EOF state on success." +description: "Rewind the position of a file pointer." sidebar: - order: 170 + order: 187 --- ## rewind() @@ -11,7 +11,7 @@ sidebar: function rewind(resource $stream): bool ``` -Lowers `rewind(stream)` as `lseek(fd, 0, SEEK_SET)` and clears EOF state on success. +Rewind the position of a file pointer. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/rewinddir.md b/docs/php/builtins/io/rewinddir.md index 2c739dbeda..379f10be46 100644 --- a/docs/php/builtins/io/rewinddir.md +++ b/docs/php/builtins/io/rewinddir.md @@ -1,8 +1,8 @@ --- title: "rewinddir()" -description: "Lowers `rewinddir(dir_handle)` for libc, glob, and userspace-wrapper handles." +description: "Rewind directory handle." sidebar: - order: 171 + order: 188 --- ## rewinddir() @@ -11,7 +11,7 @@ sidebar: function rewinddir(resource $dir_handle): void ``` -Lowers `rewinddir(dir_handle)` for libc, glob, and userspace-wrapper handles. +Rewind directory handle. **Parameters**: - `$dir_handle` (`resource`) diff --git a/docs/php/builtins/io/stream_bucket_make_writeable.md b/docs/php/builtins/io/stream_bucket_make_writeable.md index e13379cb29..9fd73afea8 100644 --- a/docs/php/builtins/io/stream_bucket_make_writeable.md +++ b/docs/php/builtins/io/stream_bucket_make_writeable.md @@ -1,8 +1,8 @@ --- title: "stream_bucket_make_writeable()" -description: "Lowers `stream_bucket_make_writeable(brigade)` by popping the brigade head." +description: "Returns a bucket object from the brigade for use in a stream filter." sidebar: - order: 172 + order: 189 --- ## stream_bucket_make_writeable() @@ -11,7 +11,7 @@ sidebar: function stream_bucket_make_writeable(mixed $brigade): mixed ``` -Lowers `stream_bucket_make_writeable(brigade)` by popping the brigade head. +Returns a bucket object from the brigade for use in a stream filter. **Parameters**: - `$brigade` (`mixed`) diff --git a/docs/php/builtins/io/stream_bucket_new.md b/docs/php/builtins/io/stream_bucket_new.md index dbd1a316fb..ebea411b29 100644 --- a/docs/php/builtins/io/stream_bucket_new.md +++ b/docs/php/builtins/io/stream_bucket_new.md @@ -1,8 +1,8 @@ --- title: "stream_bucket_new()" -description: "Lowers `stream_bucket_new(stream, data)` into a stdClass-backed bucket object." +description: "Creates a new bucket for use in a stream filter." sidebar: - order: 173 + order: 190 --- ## stream_bucket_new() @@ -11,7 +11,7 @@ sidebar: function stream_bucket_new(resource $stream, string $buffer): mixed ``` -Lowers `stream_bucket_new(stream, data)` into a stdClass-backed bucket object. +Creates a new bucket for use in a stream filter. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_context_create.md b/docs/php/builtins/io/stream_context_create.md index efcb84249b..7e556f6436 100644 --- a/docs/php/builtins/io/stream_context_create.md +++ b/docs/php/builtins/io/stream_context_create.md @@ -1,21 +1,21 @@ --- title: "stream_context_create()" -description: "Lowers `stream_context_create(options?, params?)`." +description: "Creates a stream context." sidebar: - order: 174 + order: 191 --- ## stream_context_create() ```php -function stream_context_create(array $options, array $params): mixed +function stream_context_create(array $options = null, array $params = null): mixed ``` -Lowers `stream_context_create(options?, params?)`. +Creates a stream context. **Parameters**: -- `$options` (`array`), optional -- `$params` (`array`), optional +- `$options` (`array`), default `null`, optional +- `$params` (`array`), default `null`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_context_get_default.md b/docs/php/builtins/io/stream_context_get_default.md index d1d8b4aec9..ad032c2764 100644 --- a/docs/php/builtins/io/stream_context_get_default.md +++ b/docs/php/builtins/io/stream_context_get_default.md @@ -1,20 +1,20 @@ --- title: "stream_context_get_default()" -description: "Lowers `stream_context_get_default(options?)`." +description: "Retrieves the default stream context." sidebar: - order: 175 + order: 192 --- ## stream_context_get_default() ```php -function stream_context_get_default(array $options): mixed +function stream_context_get_default(array $options = null): mixed ``` -Lowers `stream_context_get_default(options?)`. +Retrieves the default stream context. **Parameters**: -- `$options` (`array`), optional +- `$options` (`array`), default `null`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_context_get_options.md b/docs/php/builtins/io/stream_context_get_options.md index 6f29337abd..ffca97723d 100644 --- a/docs/php/builtins/io/stream_context_get_options.md +++ b/docs/php/builtins/io/stream_context_get_options.md @@ -1,20 +1,20 @@ --- title: "stream_context_get_options()" -description: "Lowers `stream_context_get_options(context)`." +description: "Retrieves options for the specified stream context." sidebar: - order: 176 + order: 193 --- ## stream_context_get_options() ```php -function stream_context_get_options(resource $stream_or_context): array +function stream_context_get_options(resource $context): array ``` -Lowers `stream_context_get_options(context)`. +Retrieves options for the specified stream context. **Parameters**: -- `$stream_or_context` (`resource`) +- `$context` (`resource`) **Returns**: `array` diff --git a/docs/php/builtins/io/stream_context_get_params.md b/docs/php/builtins/io/stream_context_get_params.md index a5e110bdc9..836973a127 100644 --- a/docs/php/builtins/io/stream_context_get_params.md +++ b/docs/php/builtins/io/stream_context_get_params.md @@ -1,8 +1,8 @@ --- title: "stream_context_get_params()" -description: "Lowers `stream_context_get_params(context)` to an empty associative hash." +description: "Retrieves parameters from the specified stream context." sidebar: - order: 177 + order: 194 --- ## stream_context_get_params() @@ -11,7 +11,7 @@ sidebar: function stream_context_get_params(resource $context): array ``` -Lowers `stream_context_get_params(context)` to an empty associative hash. +Retrieves parameters from the specified stream context. **Parameters**: - `$context` (`resource`) diff --git a/docs/php/builtins/io/stream_context_set_default.md b/docs/php/builtins/io/stream_context_set_default.md index cec4427094..afab538c14 100644 --- a/docs/php/builtins/io/stream_context_set_default.md +++ b/docs/php/builtins/io/stream_context_set_default.md @@ -1,8 +1,8 @@ --- title: "stream_context_set_default()" -description: "Lowers `stream_context_set_default(options)`." +description: "Sets the default stream context." sidebar: - order: 178 + order: 195 --- ## stream_context_set_default() @@ -11,7 +11,7 @@ sidebar: function stream_context_set_default(array $options): mixed ``` -Lowers `stream_context_set_default(options)`. +Sets the default stream context. **Parameters**: - `$options` (`array`) diff --git a/docs/php/builtins/io/stream_context_set_option.md b/docs/php/builtins/io/stream_context_set_option.md index 9c1e26475c..2178ce7484 100644 --- a/docs/php/builtins/io/stream_context_set_option.md +++ b/docs/php/builtins/io/stream_context_set_option.md @@ -1,23 +1,23 @@ --- title: "stream_context_set_option()" -description: "Lowers `stream_context_set_option(context, options)` and the four-argument form." +description: "Sets an option on the specified context." sidebar: - order: 179 + order: 196 --- ## stream_context_set_option() ```php -function stream_context_set_option(resource $context, string $wrapper_or_options, string $option_name, mixed $value): bool +function stream_context_set_option(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool ``` -Lowers `stream_context_set_option(context, options)` and the four-argument form. +Sets an option on the specified context. **Parameters**: - `$context` (`resource`) - `$wrapper_or_options` (`string`) -- `$option_name` (`string`), optional -- `$value` (`mixed`), optional +- `$option_name` (`string`), default `null`, optional +- `$value` (`mixed`), default `null`, optional **Returns**: `bool` diff --git a/docs/php/builtins/io/stream_context_set_params.md b/docs/php/builtins/io/stream_context_set_params.md index 83c60a1acf..da7dcc12fa 100644 --- a/docs/php/builtins/io/stream_context_set_params.md +++ b/docs/php/builtins/io/stream_context_set_params.md @@ -1,8 +1,8 @@ --- title: "stream_context_set_params()" -description: "Lowers `stream_context_set_params(context, params)` as an accepted parameter update." +description: "Sets parameters on the specified context." sidebar: - order: 180 + order: 197 --- ## stream_context_set_params() @@ -11,7 +11,7 @@ sidebar: function stream_context_set_params(resource $context, array $params): bool ``` -Lowers `stream_context_set_params(context, params)` as an accepted parameter update. +Sets parameters on the specified context. **Parameters**: - `$context` (`resource`) diff --git a/docs/php/builtins/io/stream_copy_to_stream.md b/docs/php/builtins/io/stream_copy_to_stream.md index f1002112db..1083006e1d 100644 --- a/docs/php/builtins/io/stream_copy_to_stream.md +++ b/docs/php/builtins/io/stream_copy_to_stream.md @@ -1,23 +1,23 @@ --- title: "stream_copy_to_stream()" -description: "Lowers `stream_copy_to_stream(from, to, length?, offset?)` through wrapper-aware read/write loops." +description: "Copies data from one stream to another." sidebar: - order: 181 + order: 198 --- ## stream_copy_to_stream() ```php -function stream_copy_to_stream(resource $from, resource $to, int $length, int $offset): mixed +function stream_copy_to_stream(resource $from, resource $to, int $length = null, int $offset = -1): mixed ``` -Lowers `stream_copy_to_stream(from, to, length?, offset?)` through wrapper-aware read/write loops. +Copies data from one stream to another. **Parameters**: - `$from` (`resource`) - `$to` (`resource`) -- `$length` (`int`), optional -- `$offset` (`int`), optional +- `$length` (`int`), default `null`, optional +- `$offset` (`int`), default `-1`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_filter_register.md b/docs/php/builtins/io/stream_filter_register.md index 9b0cda282b..055527fdcc 100644 --- a/docs/php/builtins/io/stream_filter_register.md +++ b/docs/php/builtins/io/stream_filter_register.md @@ -1,8 +1,8 @@ --- title: "stream_filter_register()" -description: "Lowers `stream_filter_register(filter_name, class)` into the user-filter registry helper." +description: "Registers a user-defined stream filter." sidebar: - order: 182 + order: 199 --- ## stream_filter_register() @@ -11,7 +11,7 @@ sidebar: function stream_filter_register(string $filter_name, string $class): bool ``` -Lowers `stream_filter_register(filter_name, class)` into the user-filter registry helper. +Registers a user-defined stream filter. **Parameters**: - `$filter_name` (`string`) diff --git a/docs/php/builtins/io/stream_filter_remove.md b/docs/php/builtins/io/stream_filter_remove.md index 308997b86e..b03bbe6b8a 100644 --- a/docs/php/builtins/io/stream_filter_remove.md +++ b/docs/php/builtins/io/stream_filter_remove.md @@ -1,8 +1,8 @@ --- title: "stream_filter_remove()" -description: "Lowers `stream_filter_remove(filter)` and clears both direction tables for the fd." +description: "Removes a filter from a stream." sidebar: - order: 183 + order: 200 --- ## stream_filter_remove() @@ -11,7 +11,7 @@ sidebar: function stream_filter_remove(resource $stream_filter): bool ``` -Lowers `stream_filter_remove(filter)` and clears both direction tables for the fd. +Removes a filter from a stream. **Parameters**: - `$stream_filter` (`resource`) diff --git a/docs/php/builtins/io/stream_get_contents.md b/docs/php/builtins/io/stream_get_contents.md index 16ae4fe20a..6e41982c9a 100644 --- a/docs/php/builtins/io/stream_get_contents.md +++ b/docs/php/builtins/io/stream_get_contents.md @@ -1,22 +1,22 @@ --- title: "stream_get_contents()" -description: "Lowers `stream_get_contents(stream, length?, offset?)` to `string|false`." +description: "Reads remainder of a stream into a string." sidebar: - order: 184 + order: 201 --- ## stream_get_contents() ```php -function stream_get_contents(resource $stream, int $length, int $offset): mixed +function stream_get_contents(resource $stream, int $length = null, int $offset = -1): mixed ``` -Lowers `stream_get_contents(stream, length?, offset?)` to `string|false`. +Reads remainder of a stream into a string. **Parameters**: - `$stream` (`resource`) -- `$length` (`int`), optional -- `$offset` (`int`), optional +- `$length` (`int`), default `null`, optional +- `$offset` (`int`), default `-1`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_get_filters.md b/docs/php/builtins/io/stream_get_filters.md index fd5819f508..813204a4ec 100644 --- a/docs/php/builtins/io/stream_get_filters.md +++ b/docs/php/builtins/io/stream_get_filters.md @@ -1,8 +1,8 @@ --- title: "stream_get_filters()" -description: "Lowers `stream_get_filters()` to the static built-in filter list." +description: "Retrieves list of registered filters." sidebar: - order: 185 + order: 202 --- ## stream_get_filters() @@ -11,7 +11,7 @@ sidebar: function stream_get_filters(): array ``` -Lowers `stream_get_filters()` to the static built-in filter list. +Retrieves list of registered filters. **Parameters**: none. diff --git a/docs/php/builtins/io/stream_get_line.md b/docs/php/builtins/io/stream_get_line.md index 7e326c4399..ed332ef462 100644 --- a/docs/php/builtins/io/stream_get_line.md +++ b/docs/php/builtins/io/stream_get_line.md @@ -1,22 +1,22 @@ --- title: "stream_get_line()" -description: "Lowers `stream_get_line(stream, length, ending?)`." +description: "Gets line from stream resource up to a given delimiter." sidebar: - order: 186 + order: 203 --- ## stream_get_line() ```php -function stream_get_line(resource $stream, int $length, string $ending): string +function stream_get_line(resource $stream, int $length, string $ending = ''): string ``` -Lowers `stream_get_line(stream, length, ending?)`. +Gets line from stream resource up to a given delimiter. **Parameters**: - `$stream` (`resource`) - `$length` (`int`) -- `$ending` (`string`), optional +- `$ending` (`string`), default `''`, optional **Returns**: `string` diff --git a/docs/php/builtins/io/stream_get_meta_data.md b/docs/php/builtins/io/stream_get_meta_data.md index f1f5328891..4f8a0c5105 100644 --- a/docs/php/builtins/io/stream_get_meta_data.md +++ b/docs/php/builtins/io/stream_get_meta_data.md @@ -1,8 +1,8 @@ --- title: "stream_get_meta_data()" -description: "Lowers `stream_get_meta_data(stream)` through the metadata runtime helper." +description: "Retrieves metadata from streams/file pointers." sidebar: - order: 187 + order: 204 --- ## stream_get_meta_data() @@ -11,7 +11,7 @@ sidebar: function stream_get_meta_data(resource $stream): array ``` -Lowers `stream_get_meta_data(stream)` through the metadata runtime helper. +Retrieves metadata from streams/file pointers. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_get_transports.md b/docs/php/builtins/io/stream_get_transports.md index 5c1530b975..3e306c33b7 100644 --- a/docs/php/builtins/io/stream_get_transports.md +++ b/docs/php/builtins/io/stream_get_transports.md @@ -1,8 +1,8 @@ --- title: "stream_get_transports()" -description: "Lowers `stream_get_transports()` to the static transport list." +description: "Retrieves list of registered socket transports." sidebar: - order: 188 + order: 205 --- ## stream_get_transports() @@ -11,7 +11,7 @@ sidebar: function stream_get_transports(): array ``` -Lowers `stream_get_transports()` to the static transport list. +Retrieves list of registered socket transports. **Parameters**: none. diff --git a/docs/php/builtins/io/stream_get_wrappers.md b/docs/php/builtins/io/stream_get_wrappers.md index b70a0382c0..c87abb259c 100644 --- a/docs/php/builtins/io/stream_get_wrappers.md +++ b/docs/php/builtins/io/stream_get_wrappers.md @@ -1,8 +1,8 @@ --- title: "stream_get_wrappers()" -description: "Lowers `stream_get_wrappers()` to the static built-in wrapper list." +description: "Retrieves list of registered streams." sidebar: - order: 189 + order: 206 --- ## stream_get_wrappers() @@ -11,7 +11,7 @@ sidebar: function stream_get_wrappers(): array ``` -Lowers `stream_get_wrappers()` to the static built-in wrapper list. +Retrieves list of registered streams. **Parameters**: none. diff --git a/docs/php/builtins/io/stream_is_local.md b/docs/php/builtins/io/stream_is_local.md index 8a44541102..b30b1f8b69 100644 --- a/docs/php/builtins/io/stream_is_local.md +++ b/docs/php/builtins/io/stream_is_local.md @@ -1,8 +1,8 @@ --- title: "stream_is_local()" -description: "Lowers `stream_is_local(stream)` as a true predicate after evaluating its argument." +description: "Checks if a stream is a local stream." sidebar: - order: 190 + order: 207 --- ## stream_is_local() @@ -11,7 +11,7 @@ sidebar: function stream_is_local(resource $stream): bool ``` -Lowers `stream_is_local(stream)` as a true predicate after evaluating its argument. +Checks if a stream is a local stream. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_isatty.md b/docs/php/builtins/io/stream_isatty.md index 7db33f239a..59e169fb0a 100644 --- a/docs/php/builtins/io/stream_isatty.md +++ b/docs/php/builtins/io/stream_isatty.md @@ -1,8 +1,8 @@ --- title: "stream_isatty()" -description: "Lowers `stream_isatty(stream)`." +description: "Checks if a stream is a TTY." sidebar: - order: 191 + order: 208 --- ## stream_isatty() @@ -11,7 +11,7 @@ sidebar: function stream_isatty(resource $stream): bool ``` -Lowers `stream_isatty(stream)`. +Checks if a stream is a TTY. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_resolve_include_path.md b/docs/php/builtins/io/stream_resolve_include_path.md index 65e716d700..079a7c35b6 100644 --- a/docs/php/builtins/io/stream_resolve_include_path.md +++ b/docs/php/builtins/io/stream_resolve_include_path.md @@ -1,8 +1,8 @@ --- title: "stream_resolve_include_path()" -description: "Lowers `stream_resolve_include_path(filename)` as realpath-backed `string|false`." +description: "Resolves filename against the include path." sidebar: - order: 192 + order: 209 --- ## stream_resolve_include_path() @@ -11,7 +11,7 @@ sidebar: function stream_resolve_include_path(string $filename): mixed ``` -Lowers `stream_resolve_include_path(filename)` as realpath-backed `string|false`. +Resolves filename against the include path. **Parameters**: - `$filename` (`string`) diff --git a/docs/php/builtins/io/stream_select.md b/docs/php/builtins/io/stream_select.md index 7663f34ee2..b5681c50ab 100644 --- a/docs/php/builtins/io/stream_select.md +++ b/docs/php/builtins/io/stream_select.md @@ -1,24 +1,24 @@ --- title: "stream_select()" -description: "Lowers `stream_select(read, write, except, seconds, microseconds?)`." +description: "Runs the equivalent of the select() system call on the given arrays of streams." sidebar: - order: 193 + order: 210 --- ## stream_select() ```php -function stream_select(array $read, array $write, array $except, int $seconds, int $microseconds): int +function stream_select(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int ``` -Lowers `stream_select(read, write, except, seconds, microseconds?)`. +Runs the equivalent of the select() system call on the given arrays of streams. **Parameters**: - `$read` (`array`), passed by reference - `$write` (`array`), passed by reference - `$except` (`array`), passed by reference - `$seconds` (`int`) -- `$microseconds` (`int`), optional +- `$microseconds` (`int`), default `0`, optional **Returns**: `int` diff --git a/docs/php/builtins/io/stream_set_blocking.md b/docs/php/builtins/io/stream_set_blocking.md index 726641298a..37866fddaf 100644 --- a/docs/php/builtins/io/stream_set_blocking.md +++ b/docs/php/builtins/io/stream_set_blocking.md @@ -1,8 +1,8 @@ --- title: "stream_set_blocking()" -description: "Lowers `stream_set_blocking(stream, enable)`." +description: "Sets blocking/non-blocking mode on a stream." sidebar: - order: 194 + order: 211 --- ## stream_set_blocking() @@ -11,7 +11,7 @@ sidebar: function stream_set_blocking(resource $stream, bool $enable): bool ``` -Lowers `stream_set_blocking(stream, enable)`. +Sets blocking/non-blocking mode on a stream. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_set_chunk_size.md b/docs/php/builtins/io/stream_set_chunk_size.md index 2d75b46495..1a8db790b5 100644 --- a/docs/php/builtins/io/stream_set_chunk_size.md +++ b/docs/php/builtins/io/stream_set_chunk_size.md @@ -1,8 +1,8 @@ --- title: "stream_set_chunk_size()" -description: "Lowers `stream_set_chunk_size(stream, size)` and returns the previous size." +description: "Sets the read chunk size on a stream." sidebar: - order: 195 + order: 212 --- ## stream_set_chunk_size() @@ -11,7 +11,7 @@ sidebar: function stream_set_chunk_size(resource $stream, int $size): int ``` -Lowers `stream_set_chunk_size(stream, size)` and returns the previous size. +Sets the read chunk size on a stream. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_set_read_buffer.md b/docs/php/builtins/io/stream_set_read_buffer.md index ee518b9b45..fcbfcaa434 100644 --- a/docs/php/builtins/io/stream_set_read_buffer.md +++ b/docs/php/builtins/io/stream_set_read_buffer.md @@ -1,8 +1,8 @@ --- title: "stream_set_read_buffer()" -description: "Lowers stream read/write buffer setters as successful no-ops." +description: "Sets the read file buffering on a stream." sidebar: - order: 196 + order: 213 --- ## stream_set_read_buffer() @@ -11,7 +11,7 @@ sidebar: function stream_set_read_buffer(resource $stream, int $size): int ``` -Lowers stream read/write buffer setters as successful no-ops. +Sets the read file buffering on a stream. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_set_timeout.md b/docs/php/builtins/io/stream_set_timeout.md index d508b651c0..990624b8c6 100644 --- a/docs/php/builtins/io/stream_set_timeout.md +++ b/docs/php/builtins/io/stream_set_timeout.md @@ -1,22 +1,22 @@ --- title: "stream_set_timeout()" -description: "Lowers `stream_set_timeout(stream, seconds, microseconds?)`." +description: "Sets timeout period on a stream." sidebar: - order: 197 + order: 214 --- ## stream_set_timeout() ```php -function stream_set_timeout(resource $stream, int $seconds, int $microseconds): bool +function stream_set_timeout(resource $stream, int $seconds, int $microseconds = 0): bool ``` -Lowers `stream_set_timeout(stream, seconds, microseconds?)`. +Sets timeout period on a stream. **Parameters**: - `$stream` (`resource`) - `$seconds` (`int`) -- `$microseconds` (`int`), optional +- `$microseconds` (`int`), default `0`, optional **Returns**: `bool` diff --git a/docs/php/builtins/io/stream_set_write_buffer.md b/docs/php/builtins/io/stream_set_write_buffer.md index 4f1f556c3e..3ffbc69c95 100644 --- a/docs/php/builtins/io/stream_set_write_buffer.md +++ b/docs/php/builtins/io/stream_set_write_buffer.md @@ -1,8 +1,8 @@ --- title: "stream_set_write_buffer()" -description: "Lowers stream read/write buffer setters as successful no-ops." +description: "Sets the write file buffering on a stream." sidebar: - order: 198 + order: 215 --- ## stream_set_write_buffer() @@ -11,7 +11,7 @@ sidebar: function stream_set_write_buffer(resource $stream, int $size): int ``` -Lowers stream read/write buffer setters as successful no-ops. +Sets the write file buffering on a stream. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_socket_accept.md b/docs/php/builtins/io/stream_socket_accept.md index d5f25d99b5..4f49662b6e 100644 --- a/docs/php/builtins/io/stream_socket_accept.md +++ b/docs/php/builtins/io/stream_socket_accept.md @@ -1,22 +1,22 @@ --- title: "stream_socket_accept()" -description: "Lowers `stream_socket_accept(server, timeout?, peer_name?)`." +description: "Accept a connection on a socket created by stream_socket_server()." sidebar: - order: 199 + order: 216 --- ## stream_socket_accept() ```php -function stream_socket_accept(resource $socket, float $timeout, string $peer_name): mixed +function stream_socket_accept(resource $socket, float $timeout = null, string $peer_name = null): mixed ``` -Lowers `stream_socket_accept(server, timeout?, peer_name?)`. +Accept a connection on a socket created by stream_socket_server(). **Parameters**: - `$socket` (`resource`) -- `$timeout` (`float`), optional -- `$peer_name` (`string`), passed by reference, optional +- `$timeout` (`float`), default `null`, optional +- `$peer_name` (`string`), passed by reference, default `null`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_socket_client.md b/docs/php/builtins/io/stream_socket_client.md index 5410d21fc0..d8133a2e64 100644 --- a/docs/php/builtins/io/stream_socket_client.md +++ b/docs/php/builtins/io/stream_socket_client.md @@ -1,24 +1,20 @@ --- title: "stream_socket_client()" -description: "Lowers `stream_socket_client(address)` and records the connected host for TLS defaults." +description: "Open Internet or Unix domain socket connection." sidebar: - order: 200 + order: 217 --- ## stream_socket_client() ```php -function stream_socket_client(string $address, int $error_code, int $error_message, string $timeout, float $flags): mixed +function stream_socket_client(string $address): mixed ``` -Lowers `stream_socket_client(address)` and records the connected host for TLS defaults. +Open Internet or Unix domain socket connection. **Parameters**: - `$address` (`string`) -- `$error_code` (`int`), passed by reference -- `$error_message` (`int`), passed by reference -- `$timeout` (`string`) -- `$flags` (`float`) **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_socket_enable_crypto.md b/docs/php/builtins/io/stream_socket_enable_crypto.md index a78f082e5c..c3a720b3b4 100644 --- a/docs/php/builtins/io/stream_socket_enable_crypto.md +++ b/docs/php/builtins/io/stream_socket_enable_crypto.md @@ -1,23 +1,23 @@ --- title: "stream_socket_enable_crypto()" -description: "Lowers `stream_socket_enable_crypto(stream, enable, method?, session_stream?)`." +description: "Turns encryption on/off on an already connected socket." sidebar: - order: 201 + order: 218 --- ## stream_socket_enable_crypto() ```php -function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method, resource $session_stream): bool +function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool ``` -Lowers `stream_socket_enable_crypto(stream, enable, method?, session_stream?)`. +Turns encryption on/off on an already connected socket. **Parameters**: - `$stream` (`resource`) - `$enable` (`bool`) -- `$crypto_method` (`int`), optional -- `$session_stream` (`resource`), optional +- `$crypto_method` (`int`), default `null`, optional +- `$session_stream` (`resource`), default `null`, optional **Returns**: `bool` diff --git a/docs/php/builtins/io/stream_socket_get_name.md b/docs/php/builtins/io/stream_socket_get_name.md index 2e47f8e0b5..66a9f9833d 100644 --- a/docs/php/builtins/io/stream_socket_get_name.md +++ b/docs/php/builtins/io/stream_socket_get_name.md @@ -1,8 +1,8 @@ --- title: "stream_socket_get_name()" -description: "Lowers `stream_socket_get_name(socket, remote)` and boxes `string|false`." +description: "Retrieve the name of the local or remote sockets." sidebar: - order: 202 + order: 219 --- ## stream_socket_get_name() @@ -11,7 +11,7 @@ sidebar: function stream_socket_get_name(resource $socket, bool $remote): mixed ``` -Lowers `stream_socket_get_name(socket, remote)` and boxes `string|false`. +Retrieve the name of the local or remote sockets. **Parameters**: - `$socket` (`resource`) diff --git a/docs/php/builtins/io/stream_socket_pair.md b/docs/php/builtins/io/stream_socket_pair.md index 4d0ab9aef7..678755e505 100644 --- a/docs/php/builtins/io/stream_socket_pair.md +++ b/docs/php/builtins/io/stream_socket_pair.md @@ -1,8 +1,8 @@ --- title: "stream_socket_pair()" -description: "Lowers `stream_socket_pair(domain, type, protocol)` and boxes `array|false`." +description: "Creates a pair of connected, indistinguishable socket streams." sidebar: - order: 203 + order: 220 --- ## stream_socket_pair() @@ -11,7 +11,7 @@ sidebar: function stream_socket_pair(int $domain, int $type, int $protocol): mixed ``` -Lowers `stream_socket_pair(domain, type, protocol)` and boxes `array|false`. +Creates a pair of connected, indistinguishable socket streams. **Parameters**: - `$domain` (`int`) diff --git a/docs/php/builtins/io/stream_socket_recvfrom.md b/docs/php/builtins/io/stream_socket_recvfrom.md index 89c8b5ce5e..3b549eae0e 100644 --- a/docs/php/builtins/io/stream_socket_recvfrom.md +++ b/docs/php/builtins/io/stream_socket_recvfrom.md @@ -1,23 +1,23 @@ --- title: "stream_socket_recvfrom()" -description: "Lowers `stream_socket_recvfrom(socket, length, flags?, address?)`." +description: "Receives data from a socket, connected or not." sidebar: - order: 204 + order: 221 --- ## stream_socket_recvfrom() ```php -function stream_socket_recvfrom(resource $socket, int $length, int $flags, string $address): mixed +function stream_socket_recvfrom(resource $socket, int $length, int $flags = 0, string $address = ''): mixed ``` -Lowers `stream_socket_recvfrom(socket, length, flags?, address?)`. +Receives data from a socket, connected or not. **Parameters**: - `$socket` (`resource`) - `$length` (`int`) -- `$flags` (`int`), optional -- `$address` (`string`), passed by reference, optional +- `$flags` (`int`), default `0`, optional +- `$address` (`string`), passed by reference, default `''`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_socket_sendto.md b/docs/php/builtins/io/stream_socket_sendto.md index 03d3206d1f..2ae31eb084 100644 --- a/docs/php/builtins/io/stream_socket_sendto.md +++ b/docs/php/builtins/io/stream_socket_sendto.md @@ -1,23 +1,23 @@ --- title: "stream_socket_sendto()" -description: "Lowers `stream_socket_sendto(socket, data, flags?, address?)` and boxes `int|false`." +description: "Sends a message to a socket, whether it is connected or not." sidebar: - order: 205 + order: 222 --- ## stream_socket_sendto() ```php -function stream_socket_sendto(resource $socket, string $data, int $flags, string $address): mixed +function stream_socket_sendto(resource $socket, string $data, int $flags = 0, string $address = ''): mixed ``` -Lowers `stream_socket_sendto(socket, data, flags?, address?)` and boxes `int|false`. +Sends a message to a socket, whether it is connected or not. **Parameters**: - `$socket` (`resource`) - `$data` (`string`) -- `$flags` (`int`), optional -- `$address` (`string`), optional +- `$flags` (`int`), default `0`, optional +- `$address` (`string`), default `''`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_socket_server.md b/docs/php/builtins/io/stream_socket_server.md index 6934314d72..658e6bb5c5 100644 --- a/docs/php/builtins/io/stream_socket_server.md +++ b/docs/php/builtins/io/stream_socket_server.md @@ -1,22 +1,20 @@ --- title: "stream_socket_server()" -description: "Lowers `stream_socket_server(address)` and boxes `resource|false`." +description: "Create an Internet or Unix domain server socket." sidebar: - order: 206 + order: 223 --- ## stream_socket_server() ```php -function stream_socket_server(string $address, int $error_code, int $error_message): mixed +function stream_socket_server(string $address): mixed ``` -Lowers `stream_socket_server(address)` and boxes `resource|false`. +Create an Internet or Unix domain server socket. **Parameters**: - `$address` (`string`) -- `$error_code` (`int`), passed by reference -- `$error_message` (`int`), passed by reference **Returns**: `mixed` diff --git a/docs/php/builtins/io/stream_socket_shutdown.md b/docs/php/builtins/io/stream_socket_shutdown.md index e76b0b7c36..e36aaa1fe5 100644 --- a/docs/php/builtins/io/stream_socket_shutdown.md +++ b/docs/php/builtins/io/stream_socket_shutdown.md @@ -1,8 +1,8 @@ --- title: "stream_socket_shutdown()" -description: "Lowers `stream_socket_shutdown(stream, mode)`." +description: "Shutdown a full-duplex connection." sidebar: - order: 207 + order: 224 --- ## stream_socket_shutdown() @@ -11,7 +11,7 @@ sidebar: function stream_socket_shutdown(resource $stream, int $mode): bool ``` -Lowers `stream_socket_shutdown(stream, mode)`. +Shutdown a full-duplex connection. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_supports_lock.md b/docs/php/builtins/io/stream_supports_lock.md index a42ba91025..0cde57ca47 100644 --- a/docs/php/builtins/io/stream_supports_lock.md +++ b/docs/php/builtins/io/stream_supports_lock.md @@ -1,8 +1,8 @@ --- title: "stream_supports_lock()" -description: "Lowers `stream_supports_lock(stream)` as true after resource unboxing." +description: "Tells whether the stream supports locking." sidebar: - order: 208 + order: 225 --- ## stream_supports_lock() @@ -11,7 +11,7 @@ sidebar: function stream_supports_lock(resource $stream): bool ``` -Lowers `stream_supports_lock(stream)` as true after resource unboxing. +Tells whether the stream supports locking. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/io/stream_wrapper_register.md b/docs/php/builtins/io/stream_wrapper_register.md index 571c5ba82b..b1eb6e6363 100644 --- a/docs/php/builtins/io/stream_wrapper_register.md +++ b/docs/php/builtins/io/stream_wrapper_register.md @@ -1,22 +1,22 @@ --- title: "stream_wrapper_register()" -description: "Lowers `stream_wrapper_register(protocol, class, flags?)`." +description: "Registers a URL wrapper implemented as a PHP class." sidebar: - order: 209 + order: 226 --- ## stream_wrapper_register() ```php -function stream_wrapper_register(string $protocol, string $class, int $flags): bool +function stream_wrapper_register(string $protocol, string $class, int $flags = 0): bool ``` -Lowers `stream_wrapper_register(protocol, class, flags?)`. +Registers a URL wrapper implemented as a PHP class. **Parameters**: - `$protocol` (`string`) - `$class` (`string`) -- `$flags` (`int`), optional +- `$flags` (`int`), default `0`, optional **Returns**: `bool` diff --git a/docs/php/builtins/io/stream_wrapper_restore.md b/docs/php/builtins/io/stream_wrapper_restore.md index 800c5494e5..b3909c2564 100644 --- a/docs/php/builtins/io/stream_wrapper_restore.md +++ b/docs/php/builtins/io/stream_wrapper_restore.md @@ -1,8 +1,8 @@ --- title: "stream_wrapper_restore()" -description: "Lowers `stream_wrapper_restore(protocol)` as a successful no-op." +description: "Restores a previously unregistered built-in wrapper." sidebar: - order: 210 + order: 227 --- ## stream_wrapper_restore() @@ -11,7 +11,7 @@ sidebar: function stream_wrapper_restore(string $protocol): bool ``` -Lowers `stream_wrapper_restore(protocol)` as a successful no-op. +Restores a previously unregistered built-in wrapper. **Parameters**: - `$protocol` (`string`) diff --git a/docs/php/builtins/io/stream_wrapper_unregister.md b/docs/php/builtins/io/stream_wrapper_unregister.md index 979b724015..0b96c1b0ea 100644 --- a/docs/php/builtins/io/stream_wrapper_unregister.md +++ b/docs/php/builtins/io/stream_wrapper_unregister.md @@ -1,8 +1,8 @@ --- title: "stream_wrapper_unregister()" -description: "Lowers `stream_wrapper_unregister(protocol)`." +description: "Unregisters a previously registered URL wrapper." sidebar: - order: 211 + order: 228 --- ## stream_wrapper_unregister() @@ -11,7 +11,7 @@ sidebar: function stream_wrapper_unregister(string $protocol): bool ``` -Lowers `stream_wrapper_unregister(protocol)`. +Unregisters a previously registered URL wrapper. **Parameters**: - `$protocol` (`string`) diff --git a/docs/php/builtins/io/vfprintf.md b/docs/php/builtins/io/vfprintf.md index 74d7db24b0..faef5f65ab 100644 --- a/docs/php/builtins/io/vfprintf.md +++ b/docs/php/builtins/io/vfprintf.md @@ -1,8 +1,8 @@ --- title: "vfprintf()" -description: "Lowers `vfprintf(stream, format, values)` through `__rt_vsprintf` then fwrite." +description: "Write a formatted string to a stream." sidebar: - order: 212 + order: 229 --- ## vfprintf() @@ -11,7 +11,7 @@ sidebar: function vfprintf(resource $stream, string $format, array $values): int ``` -Lowers `vfprintf(stream, format, values)` through `__rt_vsprintf` then fwrite. +Write a formatted string to a stream. **Parameters**: - `$stream` (`resource`) diff --git a/docs/php/builtins/json.md b/docs/php/builtins/json.md index f9499035dd..09a9e16654 100644 --- a/docs/php/builtins/json.md +++ b/docs/php/builtins/json.md @@ -9,8 +9,8 @@ sidebar: | Function | Signature | Returns | |---|---|---| -| [`json_decode()`](./json/json_decode.md) | `(string $json, bool $associative, int $depth, int $flags): mixed` | `mixed` | -| [`json_encode()`](./json/json_encode.md) | `(mixed $value, int $flags, int $depth): string` | `string` | +| [`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, int $flags): bool` | `bool` | +| [`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 f045832ee3..d3ff351578 100644 --- a/docs/php/builtins/json/json_decode.md +++ b/docs/php/builtins/json/json_decode.md @@ -1,23 +1,23 @@ --- title: "json_decode()" -description: "Lowers `json_decode(json, associative?, depth?, flags?)` through the shared JSON decoder runtime." +description: "Decodes a JSON string." sidebar: - order: 213 + order: 230 --- ## json_decode() ```php -function json_decode(string $json, bool $associative, int $depth, int $flags): mixed +function json_decode(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed ``` -Lowers `json_decode(json, associative?, depth?, flags?)` through the shared JSON decoder runtime. +Decodes a JSON string. **Parameters**: - `$json` (`string`) -- `$associative` (`bool`), optional -- `$depth` (`int`), optional -- `$flags` (`int`), optional +- `$associative` (`bool`), default `null`, optional +- `$depth` (`int`), default `512`, optional +- `$flags` (`int`), default `0`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/json/json_encode.md b/docs/php/builtins/json/json_encode.md index 070f2473b9..183c7517f9 100644 --- a/docs/php/builtins/json/json_encode.md +++ b/docs/php/builtins/json/json_encode.md @@ -1,22 +1,22 @@ --- title: "json_encode()" -description: "Lowers `json_encode(value, flags?, depth?)` through the shared JSON encoder runtime." +description: "Returns the JSON representation of a value." sidebar: - order: 214 + order: 231 --- ## json_encode() ```php -function json_encode(mixed $value, int $flags, int $depth): string +function json_encode(mixed $value, int $flags = 0, int $depth = 512): string ``` -Lowers `json_encode(value, flags?, depth?)` through the shared JSON encoder runtime. +Returns the JSON representation of a value. **Parameters**: - `$value` (`mixed`) -- `$flags` (`int`), optional -- `$depth` (`int`), optional +- `$flags` (`int`), default `0`, optional +- `$depth` (`int`), default `512`, optional **Returns**: `string` diff --git a/docs/php/builtins/json/json_last_error.md b/docs/php/builtins/json/json_last_error.md index 0b8f0c9556..4873936518 100644 --- a/docs/php/builtins/json/json_last_error.md +++ b/docs/php/builtins/json/json_last_error.md @@ -1,8 +1,8 @@ --- title: "json_last_error()" -description: "Lowers `json_last_error()` by reading the shared runtime error-code symbol." +description: "Returns the last error (if any) occurred during the last JSON encoding/decoding." sidebar: - order: 215 + order: 232 --- ## json_last_error() @@ -11,7 +11,7 @@ sidebar: function json_last_error(): int ``` -Lowers `json_last_error()` by reading the shared runtime error-code symbol. +Returns the last error (if any) occurred during the last JSON encoding/decoding. **Parameters**: none. diff --git a/docs/php/builtins/json/json_last_error_msg.md b/docs/php/builtins/json/json_last_error_msg.md index da2d0c5d47..fc3d0d284e 100644 --- a/docs/php/builtins/json/json_last_error_msg.md +++ b/docs/php/builtins/json/json_last_error_msg.md @@ -1,8 +1,8 @@ --- title: "json_last_error_msg()" -description: "Lowers `json_last_error_msg()` through the runtime message lookup table." +description: "Returns the error string of the last json_encode() or json_decode() call." sidebar: - order: 216 + order: 233 --- ## json_last_error_msg() @@ -11,7 +11,7 @@ sidebar: function json_last_error_msg(): string ``` -Lowers `json_last_error_msg()` through the runtime message lookup table. +Returns the error string of the last json_encode() or json_decode() call. **Parameters**: none. diff --git a/docs/php/builtins/json/json_validate.md b/docs/php/builtins/json/json_validate.md index 113b111476..fbcd6e4743 100644 --- a/docs/php/builtins/json/json_validate.md +++ b/docs/php/builtins/json/json_validate.md @@ -1,22 +1,22 @@ --- title: "json_validate()" -description: "Lowers `json_validate(json, depth?, flags?)` into the shared validator runtime." +description: "Checks if a string contains valid JSON." sidebar: - order: 217 + order: 234 --- ## json_validate() ```php -function json_validate(string $json, int $depth, int $flags): bool +function json_validate(string $json, int $depth = 512, int $flags = 0): bool ``` -Lowers `json_validate(json, depth?, flags?)` into the shared validator runtime. +Checks if a string contains valid JSON. **Parameters**: - `$json` (`string`) -- `$depth` (`int`), optional -- `$flags` (`int`), optional +- `$depth` (`int`), default `512`, optional +- `$flags` (`int`), default `0`, optional **Returns**: `bool` diff --git a/docs/php/builtins/math.md b/docs/php/builtins/math.md index f7b95c244c..59fb40fa71 100644 --- a/docs/php/builtins/math.md +++ b/docs/php/builtins/math.md @@ -15,7 +15,7 @@ sidebar: | [`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): string` | `string` | +| [`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` | @@ -28,18 +28,19 @@ sidebar: | [`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): float` | `float` | +| [`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): float` | `float` | -| [`min()`](./math/min.md) | `(mixed $value, ...$values): 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_bytes()`](./math/random_bytes.md) | `(int $length): string` | `string` | | [`random_int()`](./math/random_int.md) | `(int $min, int $max): int` | `int` | -| [`round()`](./math/round.md) | `(float $num, int $precision): float` | `float` | +| [`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` | diff --git a/docs/php/builtins/math/abs.md b/docs/php/builtins/math/abs.md index 0214047182..a1bd48d104 100644 --- a/docs/php/builtins/math/abs.md +++ b/docs/php/builtins/math/abs.md @@ -1,8 +1,8 @@ --- title: "abs()" -description: "Lowers `abs()` for concrete integer-like and floating operands." +description: "Absolute value." sidebar: - order: 218 + order: 235 --- ## abs() @@ -11,7 +11,7 @@ sidebar: function abs(int $num): mixed ``` -Lowers `abs()` for concrete integer-like and floating operands. +Absolute value. **Parameters**: - `$num` (`int`) diff --git a/docs/php/builtins/math/acos.md b/docs/php/builtins/math/acos.md index 3ff6cbafa5..acf751064a 100644 --- a/docs/php/builtins/math/acos.md +++ b/docs/php/builtins/math/acos.md @@ -1,8 +1,8 @@ --- title: "acos()" -description: "acos() — math builtin supported by Elephc." +description: "Returns the arccosine of a number in radians." sidebar: - order: 219 + order: 236 --- ## acos() @@ -11,7 +11,7 @@ sidebar: function acos(float $num): float ``` -`acos()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the arccosine of a number in radians. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `acos` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/acos.md). + diff --git a/docs/php/builtins/math/asin.md b/docs/php/builtins/math/asin.md index c7d1967347..e8a5038ca1 100644 --- a/docs/php/builtins/math/asin.md +++ b/docs/php/builtins/math/asin.md @@ -1,8 +1,8 @@ --- title: "asin()" -description: "asin() — math builtin supported by Elephc." +description: "Returns the arcsine of a number in radians." sidebar: - order: 220 + order: 237 --- ## asin() @@ -11,7 +11,7 @@ sidebar: function asin(float $num): float ``` -`asin()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the arcsine of a number in radians. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `asin` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/asin.md). + diff --git a/docs/php/builtins/math/atan.md b/docs/php/builtins/math/atan.md index c41d0b9d64..f93ba54f99 100644 --- a/docs/php/builtins/math/atan.md +++ b/docs/php/builtins/math/atan.md @@ -1,8 +1,8 @@ --- title: "atan()" -description: "atan() — math builtin supported by Elephc." +description: "Returns the arctangent of a number in radians." sidebar: - order: 221 + order: 238 --- ## atan() @@ -11,7 +11,7 @@ sidebar: function atan(float $num): float ``` -`atan()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the arctangent of a number in radians. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `atan` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/atan.md). + diff --git a/docs/php/builtins/math/atan2.md b/docs/php/builtins/math/atan2.md index 190133618b..f52df945f5 100644 --- a/docs/php/builtins/math/atan2.md +++ b/docs/php/builtins/math/atan2.md @@ -1,8 +1,8 @@ --- title: "atan2()" -description: "Lowers `atan2()` using the C ABI argument order `y, x`." +description: "Returns the arc tangent of two variables." sidebar: - order: 222 + order: 239 --- ## atan2() @@ -11,7 +11,7 @@ sidebar: function atan2(float $y, float $x): float ``` -Lowers `atan2()` using the C ABI argument order `y, x`. +Returns the arc tangent of two variables. **Parameters**: - `$y` (`float`) diff --git a/docs/php/builtins/math/ceil.md b/docs/php/builtins/math/ceil.md index 39a8860ae2..0f5564bbd9 100644 --- a/docs/php/builtins/math/ceil.md +++ b/docs/php/builtins/math/ceil.md @@ -1,8 +1,8 @@ --- title: "ceil()" -description: "Lowers `ceil()` for concrete integer-like and floating operands." +description: "Rounds a number up to the nearest integer." sidebar: - order: 223 + order: 240 --- ## ceil() @@ -11,7 +11,7 @@ sidebar: function ceil(float $num): float ``` -Lowers `ceil()` for concrete integer-like and floating operands. +Rounds a number up to the nearest integer. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/clamp.md b/docs/php/builtins/math/clamp.md index 628029bd22..1dc2122491 100644 --- a/docs/php/builtins/math/clamp.md +++ b/docs/php/builtins/math/clamp.md @@ -1,24 +1,24 @@ --- title: "clamp()" -description: "Lowers numeric `clamp(value, min, max)` calls with PHP-compatible bound checks." +description: "Clamps a value to be within a specified range." sidebar: - order: 224 + order: 241 --- ## clamp() ```php -function clamp(int $value, int $min, int $max): string +function clamp(int $value, int $min, int $max): mixed ``` -Lowers numeric `clamp(value, min, max)` calls with PHP-compatible bound checks. +Clamps a value to be within a specified range. **Parameters**: - `$value` (`int`) - `$min` (`int`) - `$max` (`int`) -**Returns**: `string` +**Returns**: `mixed` _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 74f770335f..a07542ec1f 100644 --- a/docs/php/builtins/math/cos.md +++ b/docs/php/builtins/math/cos.md @@ -1,8 +1,8 @@ --- title: "cos()" -description: "cos() — math builtin supported by Elephc." +description: "Returns the cosine of a number (radians)." sidebar: - order: 225 + order: 242 --- ## cos() @@ -11,7 +11,7 @@ sidebar: function cos(float $num): float ``` -`cos()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the cosine of a number (radians). **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `cos` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/cos.md). + diff --git a/docs/php/builtins/math/cosh.md b/docs/php/builtins/math/cosh.md index 224ecd785d..ea9da090fb 100644 --- a/docs/php/builtins/math/cosh.md +++ b/docs/php/builtins/math/cosh.md @@ -1,8 +1,8 @@ --- title: "cosh()" -description: "cosh() — math builtin supported by Elephc." +description: "Returns the hyperbolic cosine of a number." sidebar: - order: 226 + order: 243 --- ## cosh() @@ -11,7 +11,7 @@ sidebar: function cosh(float $num): float ``` -`cosh()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the hyperbolic cosine of a number. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `cosh` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/cosh.md). + diff --git a/docs/php/builtins/math/deg2rad.md b/docs/php/builtins/math/deg2rad.md index 6da814b064..4ad81c1f2f 100644 --- a/docs/php/builtins/math/deg2rad.md +++ b/docs/php/builtins/math/deg2rad.md @@ -1,8 +1,8 @@ --- title: "deg2rad()" -description: "Lowers `deg2rad()` by multiplying with `PI / 180`." +description: "Converts a degree value to radians." sidebar: - order: 227 + order: 244 --- ## deg2rad() @@ -11,7 +11,7 @@ sidebar: function deg2rad(float $num): float ``` -Lowers `deg2rad()` by multiplying with `PI / 180`. +Converts a degree value to radians. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/exp.md b/docs/php/builtins/math/exp.md index 730f7fda22..bb38c14642 100644 --- a/docs/php/builtins/math/exp.md +++ b/docs/php/builtins/math/exp.md @@ -1,8 +1,8 @@ --- title: "exp()" -description: "exp() — math builtin supported by Elephc." +description: "Returns e raised to the power of a number." sidebar: - order: 228 + order: 245 --- ## exp() @@ -11,7 +11,7 @@ sidebar: function exp(float $num): float ``` -`exp()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns e raised to the power of a number. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `exp` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/exp.md). + diff --git a/docs/php/builtins/math/fdiv.md b/docs/php/builtins/math/fdiv.md index 3b8fb9ab04..cb296e066c 100644 --- a/docs/php/builtins/math/fdiv.md +++ b/docs/php/builtins/math/fdiv.md @@ -1,8 +1,8 @@ --- title: "fdiv()" -description: "Lowers `fdiv()` for concrete integer-like and floating operands." +description: "Divides two numbers, according to IEEE 754." sidebar: - order: 229 + order: 246 --- ## fdiv() @@ -11,7 +11,7 @@ sidebar: function fdiv(float $num1, float $num2): float ``` -Lowers `fdiv()` for concrete integer-like and floating operands. +Divides two numbers, according to IEEE 754. **Parameters**: - `$num1` (`float`) diff --git a/docs/php/builtins/math/floor.md b/docs/php/builtins/math/floor.md index 959a86348e..c920dc07b5 100644 --- a/docs/php/builtins/math/floor.md +++ b/docs/php/builtins/math/floor.md @@ -1,8 +1,8 @@ --- title: "floor()" -description: "Lowers `floor()` for concrete integer-like and floating operands." +description: "Rounds a number down to the nearest integer." sidebar: - order: 230 + order: 247 --- ## floor() @@ -11,7 +11,7 @@ sidebar: function floor(float $num): float ``` -Lowers `floor()` for concrete integer-like and floating operands. +Rounds a number down to the nearest integer. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/fmod.md b/docs/php/builtins/math/fmod.md index 518300705e..78a2c94254 100644 --- a/docs/php/builtins/math/fmod.md +++ b/docs/php/builtins/math/fmod.md @@ -1,8 +1,8 @@ --- title: "fmod()" -description: "Lowers `fmod()` for concrete integer-like and floating operands." +description: "Returns the floating point remainder of the division of the arguments." sidebar: - order: 231 + order: 248 --- ## fmod() @@ -11,7 +11,7 @@ sidebar: function fmod(float $num1, float $num2): float ``` -Lowers `fmod()` for concrete integer-like and floating operands. +Returns the floating point remainder of the division of the arguments. **Parameters**: - `$num1` (`float`) diff --git a/docs/php/builtins/math/hypot.md b/docs/php/builtins/math/hypot.md index 389fbc3651..6b97393f53 100644 --- a/docs/php/builtins/math/hypot.md +++ b/docs/php/builtins/math/hypot.md @@ -1,8 +1,8 @@ --- title: "hypot()" -description: "Lowers `hypot()` using the C ABI argument order `x, y`." +description: "Calculates the length of the hypotenuse of a right-angle triangle." sidebar: - order: 232 + order: 249 --- ## hypot() @@ -11,7 +11,7 @@ sidebar: function hypot(float $x, float $y): float ``` -Lowers `hypot()` using the C ABI argument order `x, y`. +Calculates the length of the hypotenuse of a right-angle triangle. **Parameters**: - `$x` (`float`) diff --git a/docs/php/builtins/math/intdiv.md b/docs/php/builtins/math/intdiv.md index e7a91be1b7..06740dddfb 100644 --- a/docs/php/builtins/math/intdiv.md +++ b/docs/php/builtins/math/intdiv.md @@ -1,8 +1,8 @@ --- title: "intdiv()" -description: "Lowers `intdiv()` for concrete integer-like numeric operands." +description: "Integer division." sidebar: - order: 233 + order: 250 --- ## intdiv() @@ -11,7 +11,7 @@ sidebar: function intdiv(int $num1, int $num2): int ``` -Lowers `intdiv()` for concrete integer-like numeric operands. +Integer division. **Parameters**: - `$num1` (`int`) diff --git a/docs/php/builtins/math/is_finite.md b/docs/php/builtins/math/is_finite.md index 8fe56f1b03..eed5a5021e 100644 --- a/docs/php/builtins/math/is_finite.md +++ b/docs/php/builtins/math/is_finite.md @@ -1,8 +1,8 @@ --- title: "is_finite()" -description: "Lowers `is_finite()` by rejecting NaN and both infinities." +description: "Checks whether a float is finite." sidebar: - order: 234 + order: 251 --- ## is_finite() @@ -11,7 +11,7 @@ sidebar: function is_finite(float $num): bool ``` -Lowers `is_finite()` by rejecting NaN and both infinities. +Checks whether a float is finite. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/is_infinite.md b/docs/php/builtins/math/is_infinite.md index dec0ca3a0c..f2927a37a5 100644 --- a/docs/php/builtins/math/is_infinite.md +++ b/docs/php/builtins/math/is_infinite.md @@ -1,8 +1,8 @@ --- title: "is_infinite()" -description: "Lowers `is_infinite()` by comparing the normalized float against +/- infinity." +description: "Checks whether a float is infinite." sidebar: - order: 235 + order: 252 --- ## is_infinite() @@ -11,7 +11,7 @@ sidebar: function is_infinite(float $num): bool ``` -Lowers `is_infinite()` by comparing the normalized float against +/- infinity. +Checks whether a float is infinite. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/is_nan.md b/docs/php/builtins/math/is_nan.md index 659d4250b6..7b5ab79a73 100644 --- a/docs/php/builtins/math/is_nan.md +++ b/docs/php/builtins/math/is_nan.md @@ -1,8 +1,8 @@ --- title: "is_nan()" -description: "Lowers `is_nan()` by checking whether the normalized float is unordered with itself." +description: "Checks whether a float is NAN." sidebar: - order: 236 + order: 253 --- ## is_nan() @@ -11,7 +11,7 @@ sidebar: function is_nan(float $num): bool ``` -Lowers `is_nan()` by checking whether the normalized float is unordered with itself. +Checks whether a float is NAN. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/log.md b/docs/php/builtins/math/log.md index ad2a80ecc4..8973c6a012 100644 --- a/docs/php/builtins/math/log.md +++ b/docs/php/builtins/math/log.md @@ -1,21 +1,21 @@ --- title: "log()" -description: "Lowers `log()` in one-argument and base-changing two-argument forms." +description: "Natural logarithm." sidebar: - order: 237 + order: 254 --- ## log() ```php -function log(float $num, float $base): float +function log(float $num, float $base = 2.718281828459045): float ``` -Lowers `log()` in one-argument and base-changing two-argument forms. +Natural logarithm. **Parameters**: - `$num` (`float`) -- `$base` (`float`), optional +- `$base` (`float`), default `2.718281828459045`, optional **Returns**: `float` diff --git a/docs/php/builtins/math/log10.md b/docs/php/builtins/math/log10.md index e68d22ca77..ac21b08007 100644 --- a/docs/php/builtins/math/log10.md +++ b/docs/php/builtins/math/log10.md @@ -1,8 +1,8 @@ --- title: "log10()" -description: "log10() — math builtin supported by Elephc." +description: "Returns the base-10 logarithm of a number." sidebar: - order: 238 + order: 255 --- ## log10() @@ -11,7 +11,7 @@ sidebar: function log10(float $num): float ``` -`log10()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the base-10 logarithm of a number. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `log10` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/log10.md). + diff --git a/docs/php/builtins/math/log2.md b/docs/php/builtins/math/log2.md index 2b2f20cc61..0401ccc162 100644 --- a/docs/php/builtins/math/log2.md +++ b/docs/php/builtins/math/log2.md @@ -1,8 +1,8 @@ --- title: "log2()" -description: "log2() — math builtin supported by Elephc." +description: "Returns the base-2 logarithm of a number." sidebar: - order: 239 + order: 256 --- ## log2() @@ -11,7 +11,7 @@ sidebar: function log2(float $num): float ``` -`log2()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the base-2 logarithm of a number. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `log2` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/log2.md). + diff --git a/docs/php/builtins/math/max.md b/docs/php/builtins/math/max.md index 94f87f45b3..c92f69734a 100644 --- a/docs/php/builtins/math/max.md +++ b/docs/php/builtins/math/max.md @@ -1,23 +1,23 @@ --- title: "max()" -description: "Lowers numeric `min()` and `max()` over concrete integer-like or float operands." +description: "Find highest value." sidebar: - order: 240 + order: 257 --- ## max() ```php -function max(mixed $value, ...$values): float +function max(mixed $value, ...$values): mixed ``` -Lowers numeric `min()` and `max()` over concrete integer-like or float operands. +Find highest value. **Parameters**: - `$value` (`mixed`) - `...$values` — variadic: collects excess arguments into `$values`. -**Returns**: `float` +**Returns**: `mixed` _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 04dca7f93f..bb959c9892 100644 --- a/docs/php/builtins/math/min.md +++ b/docs/php/builtins/math/min.md @@ -1,23 +1,23 @@ --- title: "min()" -description: "Lowers numeric `min()` and `max()` over concrete integer-like or float operands." +description: "Find lowest value." sidebar: - order: 241 + order: 258 --- ## min() ```php -function min(mixed $value, ...$values): float +function min(mixed $value, ...$values): mixed ``` -Lowers numeric `min()` and `max()` over concrete integer-like or float operands. +Find lowest value. **Parameters**: - `$value` (`mixed`) - `...$values` — variadic: collects excess arguments into `$values`. -**Returns**: `float` +**Returns**: `mixed` _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 d032e64679..83d6dd9791 100644 --- a/docs/php/builtins/math/mt_rand.md +++ b/docs/php/builtins/math/mt_rand.md @@ -1,8 +1,8 @@ --- title: "mt_rand()" -description: "mt_rand() — math builtin supported by Elephc." +description: "Generate a random value via the Mersenne Twister Random Number Generator." sidebar: - order: 242 + order: 259 --- ## mt_rand() @@ -11,7 +11,7 @@ sidebar: function mt_rand(int $min, int $max): int ``` -`mt_rand()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Generate a random value via the Mersenne Twister Random Number Generator. **Parameters**: - `$min` (`int`) @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `mt_rand` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/mt_rand.md). + diff --git a/docs/php/builtins/math/pi.md b/docs/php/builtins/math/pi.md index ad468afc28..231447b7b1 100644 --- a/docs/php/builtins/math/pi.md +++ b/docs/php/builtins/math/pi.md @@ -1,8 +1,8 @@ --- title: "pi()" -description: "Lowers `pi()` as the same data-section float constant used by the legacy backend." +description: "Gets value of pi." sidebar: - order: 243 + order: 260 --- ## pi() @@ -11,7 +11,7 @@ sidebar: function pi(): float ``` -Lowers `pi()` as the same data-section float constant used by the legacy backend. +Gets value of pi. **Parameters**: none. diff --git a/docs/php/builtins/math/pow.md b/docs/php/builtins/math/pow.md index fc0c4b6987..9fd3f5b18c 100644 --- a/docs/php/builtins/math/pow.md +++ b/docs/php/builtins/math/pow.md @@ -1,8 +1,8 @@ --- title: "pow()" -description: "Lowers `pow()` for concrete integer-like and floating operands." +description: "Exponential expression." sidebar: - order: 244 + order: 261 --- ## pow() @@ -11,7 +11,7 @@ sidebar: function pow(float $num, float $exponent): float ``` -Lowers `pow()` for concrete integer-like and floating operands. +Exponential expression. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/rad2deg.md b/docs/php/builtins/math/rad2deg.md index f468e2413e..a0382b92c2 100644 --- a/docs/php/builtins/math/rad2deg.md +++ b/docs/php/builtins/math/rad2deg.md @@ -1,8 +1,8 @@ --- title: "rad2deg()" -description: "Lowers `rad2deg()` by multiplying with `180 / PI`." +description: "Converts a radian value to degrees." sidebar: - order: 245 + order: 262 --- ## rad2deg() @@ -11,7 +11,7 @@ sidebar: function rad2deg(float $num): float ``` -Lowers `rad2deg()` by multiplying with `180 / PI`. +Converts a radian value to degrees. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/rand.md b/docs/php/builtins/math/rand.md index 37d0a49b3a..264d5e63e5 100644 --- a/docs/php/builtins/math/rand.md +++ b/docs/php/builtins/math/rand.md @@ -1,8 +1,8 @@ --- title: "rand()" -description: "rand() — math builtin supported by Elephc." +description: "Generate a random integer." sidebar: - order: 246 + order: 263 --- ## rand() @@ -11,7 +11,7 @@ sidebar: function rand(int $min, int $max): int ``` -`rand()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Generate a random integer. **Parameters**: - `$min` (`int`) @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `rand` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/rand.md). + diff --git a/docs/php/builtins/math/random_bytes.md b/docs/php/builtins/math/random_bytes.md new file mode 100644 index 0000000000..30805ea258 --- /dev/null +++ b/docs/php/builtins/math/random_bytes.md @@ -0,0 +1,32 @@ +--- +title: "random_bytes()" +description: "Get a cryptographically secure random string of the given length." +sidebar: + order: 264 +--- + +## random_bytes() + +```php +function random_bytes(int $length): string +``` + +Get a cryptographically secure random string of the given length. + +**Parameters**: +- `$length` (`int`) + +**Returns**: `string` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `random_bytes` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/random_bytes.md). + diff --git a/docs/php/builtins/math/random_int.md b/docs/php/builtins/math/random_int.md index 1494bfc696..2b0e488423 100644 --- a/docs/php/builtins/math/random_int.md +++ b/docs/php/builtins/math/random_int.md @@ -1,8 +1,8 @@ --- title: "random_int()" -description: "Lowers `random_int()` over an inclusive integer range." +description: "Get a cryptographically secure, uniformly selected integer." sidebar: - order: 247 + order: 265 --- ## random_int() @@ -11,7 +11,7 @@ sidebar: function random_int(int $min, int $max): int ``` -Lowers `random_int()` over an inclusive integer range. +Get a cryptographically secure, uniformly selected integer. **Parameters**: - `$min` (`int`) diff --git a/docs/php/builtins/math/round.md b/docs/php/builtins/math/round.md index 13b37b90f0..38a00a8181 100644 --- a/docs/php/builtins/math/round.md +++ b/docs/php/builtins/math/round.md @@ -1,21 +1,21 @@ --- title: "round()" -description: "Lowers `round()` for concrete integer-like and floating operands." +description: "Rounds a float." sidebar: - order: 248 + order: 266 --- ## round() ```php -function round(float $num, int $precision): float +function round(float $num, int $precision = 0): float ``` -Lowers `round()` for concrete integer-like and floating operands. +Rounds a float. **Parameters**: - `$num` (`float`) -- `$precision` (`int`), optional +- `$precision` (`int`), default `0`, optional **Returns**: `float` diff --git a/docs/php/builtins/math/sin.md b/docs/php/builtins/math/sin.md index 7cf2311189..e72dc77583 100644 --- a/docs/php/builtins/math/sin.md +++ b/docs/php/builtins/math/sin.md @@ -1,8 +1,8 @@ --- title: "sin()" -description: "sin() — math builtin supported by Elephc." +description: "Returns the sine of a number (radians)." sidebar: - order: 249 + order: 267 --- ## sin() @@ -11,7 +11,7 @@ sidebar: function sin(float $num): float ``` -`sin()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the sine of a number (radians). **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `sin` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/sin.md). + diff --git a/docs/php/builtins/math/sinh.md b/docs/php/builtins/math/sinh.md index 50b828fb67..63bf9e1ca5 100644 --- a/docs/php/builtins/math/sinh.md +++ b/docs/php/builtins/math/sinh.md @@ -1,8 +1,8 @@ --- title: "sinh()" -description: "sinh() — math builtin supported by Elephc." +description: "Returns the hyperbolic sine of a number." sidebar: - order: 250 + order: 268 --- ## sinh() @@ -11,7 +11,7 @@ sidebar: function sinh(float $num): float ``` -`sinh()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the hyperbolic sine of a number. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `sinh` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/sinh.md). + diff --git a/docs/php/builtins/math/sqrt.md b/docs/php/builtins/math/sqrt.md index 95c6b46a9d..ed2ed1d3a0 100644 --- a/docs/php/builtins/math/sqrt.md +++ b/docs/php/builtins/math/sqrt.md @@ -1,8 +1,8 @@ --- title: "sqrt()" -description: "Lowers `sqrt()` for concrete integer-like and floating operands." +description: "Returns the square root of a number." sidebar: - order: 251 + order: 269 --- ## sqrt() @@ -11,7 +11,7 @@ sidebar: function sqrt(float $num): float ``` -Lowers `sqrt()` for concrete integer-like and floating operands. +Returns the square root of a number. **Parameters**: - `$num` (`float`) diff --git a/docs/php/builtins/math/tan.md b/docs/php/builtins/math/tan.md index d4c540be13..66bdd9f8c0 100644 --- a/docs/php/builtins/math/tan.md +++ b/docs/php/builtins/math/tan.md @@ -1,8 +1,8 @@ --- title: "tan()" -description: "tan() — math builtin supported by Elephc." +description: "Returns the tangent of a number (radians)." sidebar: - order: 252 + order: 270 --- ## tan() @@ -11,7 +11,7 @@ sidebar: function tan(float $num): float ``` -`tan()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the tangent of a number (radians). **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `tan` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/tan.md). + diff --git a/docs/php/builtins/math/tanh.md b/docs/php/builtins/math/tanh.md index d669dc196e..cb6108a319 100644 --- a/docs/php/builtins/math/tanh.md +++ b/docs/php/builtins/math/tanh.md @@ -1,8 +1,8 @@ --- title: "tanh()" -description: "tanh() — math builtin supported by Elephc." +description: "Returns the hyperbolic tangent of a number." sidebar: - order: 253 + order: 271 --- ## tanh() @@ -11,7 +11,7 @@ sidebar: function tanh(float $num): float ``` -`tanh()` is a math builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Returns the hyperbolic tangent of a number. **Parameters**: - `$num` (`float`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `tanh` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/tanh.md). + diff --git a/docs/php/builtins/misc.md b/docs/php/builtins/misc.md index 02054d9255..ccb08155b6 100644 --- a/docs/php/builtins/misc.md +++ b/docs/php/builtins/misc.md @@ -10,16 +10,16 @@ sidebar: | Function | Signature | Returns | |---|---|---| | [`buffer_new()`](./misc/buffer_new.md) | `(int $length): mixed` | `mixed` | -| [`call_user_func()`](./misc/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | -| [`call_user_func_array()`](./misc/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | -| [`define()`](./misc/define.md) | `(string $constant_name, mixed $value, bool $case_insensitive): bool` | `bool` | +| [`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) | `(mixed $header, mixed $replace, mixed $response_code): void` | `void` | -| [`http_response_code()`](./misc/http_response_code.md) | `(mixed $response_code): int` | `int` | +| [`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): string` | `string` | -| [`phpversion()`](./misc/phpversion.md) | `(string $extension = null): string` | `string` | -| [`print_r()`](./misc/print_r.md) | `(...$values): void` | `void` | +| [`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): void` | `void` | +| [`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) | `(...$values): void` | `void` | +| [`var_dump()`](./misc/var_dump.md) | `(mixed $value): void` | `void` | diff --git a/docs/php/builtins/misc/buffer_new.md b/docs/php/builtins/misc/buffer_new.md index cb0e302967..a0ff226320 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: 254 + order: 272 --- ## buffer_new() diff --git a/docs/php/builtins/misc/call_user_func.md b/docs/php/builtins/misc/call_user_func.md deleted file mode 100644 index 4ae00e69b6..0000000000 --- a/docs/php/builtins/misc/call_user_func.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: "call_user_func()" -description: "call_user_func() — misc builtin supported by Elephc." -sidebar: - order: 255 ---- - -## call_user_func() - -```php -function call_user_func(callable $callback, ...$args): mixed -``` - -`call_user_func()` is a misc builtin supported by Elephc. Behavior matches the PHP manual unless noted below. - -**Parameters**: -- `$callback` (`callable`) -- `...$args` — variadic: collects excess arguments into `$args`. - -**Returns**: `mixed` - -_No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - - - diff --git a/docs/php/builtins/misc/call_user_func_array.md b/docs/php/builtins/misc/call_user_func_array.md deleted file mode 100644 index 44fae2128e..0000000000 --- a/docs/php/builtins/misc/call_user_func_array.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: "call_user_func_array()" -description: "call_user_func_array() — misc builtin supported by Elephc." -sidebar: - order: 256 ---- - -## call_user_func_array() - -```php -function call_user_func_array(callable $callback, array $args): mixed -``` - -`call_user_func_array()` is a misc builtin supported by Elephc. Behavior matches the PHP manual unless noted below. - -**Parameters**: -- `$callback` (`callable`) -- `$args` (`array`) - -**Returns**: `mixed` - -_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 de0ae60e73..9b462938b2 100644 --- a/docs/php/builtins/misc/define.md +++ b/docs/php/builtins/misc/define.md @@ -2,13 +2,13 @@ title: "define()" description: "Defines a named constant at runtime." sidebar: - order: 257 + order: 273 --- ## define() ```php -function define(string $constant_name, mixed $value, bool $case_insensitive): bool +function define(string $constant_name, mixed $value): bool ``` Defines a named constant at runtime. @@ -16,7 +16,6 @@ Defines a named constant at runtime. **Parameters**: - `$constant_name` (`string`) - `$value` (`mixed`) -- `$case_insensitive` (`bool`) **Returns**: `bool` diff --git a/docs/php/builtins/misc/defined.md b/docs/php/builtins/misc/defined.md index 9a9627af0d..484fd739a8 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: 258 + order: 274 --- ## defined() diff --git a/docs/php/builtins/misc/empty.md b/docs/php/builtins/misc/empty.md index 6269e4e389..64bfb47e26 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: 259 + order: 275 --- ## empty() diff --git a/docs/php/builtins/misc/header.md b/docs/php/builtins/misc/header.md index 62b057db7d..fcf940d5d7 100644 --- a/docs/php/builtins/misc/header.md +++ b/docs/php/builtins/misc/header.md @@ -1,22 +1,22 @@ --- title: "header()" -description: "Lowers `header($line[, $replace[, $code]])` to `__rt_header`, materializing the" +description: "Sends a raw HTTP header." sidebar: - order: 260 + order: 276 --- ## header() ```php -function header(mixed $header, mixed $replace, mixed $response_code): void +function header(string $header, bool $replace = true, int $response_code = 0): void ``` -Lowers `header($line[, $replace[, $code]])` to `__rt_header`, materializing the +Sends a raw HTTP header. **Parameters**: -- `$header` (`mixed`) -- `$replace` (`mixed`), optional -- `$response_code` (`mixed`), optional +- `$header` (`string`) +- `$replace` (`bool`), default `true`, optional +- `$response_code` (`int`), default `0`, optional **Returns**: `void` diff --git a/docs/php/builtins/misc/http_response_code.md b/docs/php/builtins/misc/http_response_code.md index 837dd10313..0505e4f048 100644 --- a/docs/php/builtins/misc/http_response_code.md +++ b/docs/php/builtins/misc/http_response_code.md @@ -1,20 +1,20 @@ --- title: "http_response_code()" -description: "Lowers `http_response_code([$code])` to `__rt_http_response_code`. The code (or" +description: "Gets or sets the HTTP response code." sidebar: - order: 261 + order: 277 --- ## http_response_code() ```php -function http_response_code(mixed $response_code): int +function http_response_code(int $response_code = 0): int ``` -Lowers `http_response_code([$code])` to `__rt_http_response_code`. The code (or +Gets or sets the HTTP response code. **Parameters**: -- `$response_code` (`mixed`), optional +- `$response_code` (`int`), default `0`, optional **Returns**: `int` diff --git a/docs/php/builtins/misc/isset.md b/docs/php/builtins/misc/isset.md index de2e6b9e4c..cc1827b2ab 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: 262 + order: 278 --- ## isset() diff --git a/docs/php/builtins/misc/php_uname.md b/docs/php/builtins/misc/php_uname.md index f49718cb1b..aacf6904d9 100644 --- a/docs/php/builtins/misc/php_uname.md +++ b/docs/php/builtins/misc/php_uname.md @@ -2,19 +2,19 @@ title: "php_uname()" description: "Returns information about the operating system PHP is running on." sidebar: - order: 263 + order: 279 --- ## php_uname() ```php -function php_uname(string $mode): string +function php_uname(string $mode = 'a'): string ``` Returns information about the operating system PHP is running on. **Parameters**: -- `$mode` (`string`), optional +- `$mode` (`string`), default `'a'`, optional **Returns**: `string` diff --git a/docs/php/builtins/misc/phpversion.md b/docs/php/builtins/misc/phpversion.md index 206f14b811..488c639214 100644 --- a/docs/php/builtins/misc/phpversion.md +++ b/docs/php/builtins/misc/phpversion.md @@ -2,19 +2,18 @@ title: "phpversion()" description: "Returns the current PHP version information." sidebar: - order: 264 + order: 280 --- ## phpversion() ```php -function phpversion(string $extension = null): string +function phpversion(): string ``` Returns the current PHP version information. -**Parameters**: -- `$extension` (`string`), default `null`, optional +**Parameters**: none. **Returns**: `string` diff --git a/docs/php/builtins/misc/print_r.md b/docs/php/builtins/misc/print_r.md index b8477db5b0..a4c3591433 100644 --- a/docs/php/builtins/misc/print_r.md +++ b/docs/php/builtins/misc/print_r.md @@ -2,19 +2,19 @@ title: "print_r()" description: "Prints human-readable information about a variable." sidebar: - order: 265 + order: 281 --- ## print_r() ```php -function print_r(...$values): void +function print_r(mixed $value): void ``` Prints human-readable information about a variable. **Parameters**: -- `...$values` — variadic: collects excess arguments into `$values`. +- `$value` (`mixed`) **Returns**: `void` diff --git a/docs/php/builtins/misc/serialize.md b/docs/php/builtins/misc/serialize.md new file mode 100644 index 0000000000..1c0f7a5420 --- /dev/null +++ b/docs/php/builtins/misc/serialize.md @@ -0,0 +1,32 @@ +--- +title: "serialize()" +description: "Generates a storable representation of a value." +sidebar: + order: 282 +--- + +## serialize() + +```php +function serialize(mixed $value): string +``` + +Generates a storable representation of a value. + +**Parameters**: +- `$value` (`mixed`) + +**Returns**: `string` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `serialize` is implemented in the compiler, see [the internals page](../../../internals/builtins/misc/serialize.md). + diff --git a/docs/php/builtins/misc/unserialize.md b/docs/php/builtins/misc/unserialize.md new file mode 100644 index 0000000000..b925c3d412 --- /dev/null +++ b/docs/php/builtins/misc/unserialize.md @@ -0,0 +1,33 @@ +--- +title: "unserialize()" +description: "Creates a PHP value from a stored representation." +sidebar: + order: 283 +--- + +## unserialize() + +```php +function unserialize(string $data, mixed $options = []): mixed +``` + +Creates a PHP value from a stored representation. + +**Parameters**: +- `$data` (`string`) +- `$options` (`mixed`), default `[]`, optional + +**Returns**: `mixed` + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `unserialize` is implemented in the compiler, see [the internals page](../../../internals/builtins/misc/unserialize.md). + diff --git a/docs/php/builtins/misc/unset.md b/docs/php/builtins/misc/unset.md index 52ccee2a00..0aeedc0179 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: 266 + order: 284 --- ## unset() diff --git a/docs/php/builtins/misc/var_dump.md b/docs/php/builtins/misc/var_dump.md index 42dbaeca59..a4d7e8d7a5 100644 --- a/docs/php/builtins/misc/var_dump.md +++ b/docs/php/builtins/misc/var_dump.md @@ -2,19 +2,19 @@ title: "var_dump()" description: "Dumps information about a variable, including its type and value." sidebar: - order: 267 + order: 285 --- ## var_dump() ```php -function var_dump(...$values): void +function var_dump(mixed $value): void ``` Dumps information about a variable, including its type and value. **Parameters**: -- `...$values` — variadic: collects excess arguments into `$values`. +- `$value` (`mixed`) **Returns**: `void` diff --git a/docs/php/builtins/pointer.md b/docs/php/builtins/pointer.md index 3951666478..74dcf78742 100644 --- a/docs/php/builtins/pointer.md +++ b/docs/php/builtins/pointer.md @@ -19,7 +19,7 @@ sidebar: | [`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): mixed` | `mixed` | +| [`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` | diff --git a/docs/php/builtins/pointer/ptr.md b/docs/php/builtins/pointer/ptr.md index 3db4658840..c845062ff9 100644 --- a/docs/php/builtins/pointer/ptr.md +++ b/docs/php/builtins/pointer/ptr.md @@ -1,8 +1,8 @@ --- title: "ptr()" -description: "Lowers `ptr(value)` by materializing the address of addressable local/global storage." +description: "Returns a raw pointer to the given variable." sidebar: - order: 268 + order: 286 --- ## ptr() @@ -11,7 +11,7 @@ sidebar: function ptr(mixed $value): mixed ``` -Lowers `ptr(value)` by materializing the address of addressable local/global storage. +Returns a raw pointer to the given variable. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/pointer/ptr_get.md b/docs/php/builtins/pointer/ptr_get.md index 308ff603f1..0100fcbf47 100644 --- a/docs/php/builtins/pointer/ptr_get.md +++ b/docs/php/builtins/pointer/ptr_get.md @@ -1,8 +1,8 @@ --- title: "ptr_get()" -description: "Lowers `ptr_get(pointer)` by reading one machine word through a checked pointer." +description: "Reads one machine word through a raw pointer and returns it as an integer." sidebar: - order: 269 + order: 287 --- ## ptr_get() @@ -11,7 +11,7 @@ sidebar: function ptr_get(pointer $pointer): int ``` -Lowers `ptr_get(pointer)` by reading one machine word through a checked pointer. +Reads one machine word through a raw pointer and returns it as an integer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_is_null.md b/docs/php/builtins/pointer/ptr_is_null.md index 5c8f6700f3..16bca05dc4 100644 --- a/docs/php/builtins/pointer/ptr_is_null.md +++ b/docs/php/builtins/pointer/ptr_is_null.md @@ -1,8 +1,8 @@ --- title: "ptr_is_null()" -description: "Lowers `ptr_is_null(pointer)` by comparing the raw pointer address to zero." +description: "Returns true if the pointer is null." sidebar: - order: 270 + order: 288 --- ## ptr_is_null() @@ -11,7 +11,7 @@ sidebar: function ptr_is_null(pointer $pointer): bool ``` -Lowers `ptr_is_null(pointer)` by comparing the raw pointer address to zero. +Returns true if the pointer is null. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_null.md b/docs/php/builtins/pointer/ptr_null.md index 7914be61f8..dc5dbe5788 100644 --- a/docs/php/builtins/pointer/ptr_null.md +++ b/docs/php/builtins/pointer/ptr_null.md @@ -1,8 +1,8 @@ --- title: "ptr_null()" -description: "Lowers `ptr_null()` by materializing the raw null pointer sentinel." +description: "Returns a null raw pointer." sidebar: - order: 271 + order: 289 --- ## ptr_null() @@ -11,7 +11,7 @@ sidebar: function ptr_null(): mixed ``` -Lowers `ptr_null()` by materializing the raw null pointer sentinel. +Returns a null raw pointer. **Parameters**: none. diff --git a/docs/php/builtins/pointer/ptr_offset.md b/docs/php/builtins/pointer/ptr_offset.md index 6d01db5604..4a846c0844 100644 --- a/docs/php/builtins/pointer/ptr_offset.md +++ b/docs/php/builtins/pointer/ptr_offset.md @@ -1,8 +1,8 @@ --- title: "ptr_offset()" -description: "Lowers `ptr_offset(pointer, offset)` by adding a byte offset to a raw address." +description: "Returns a new pointer offset from the given pointer by the given byte count." sidebar: - order: 272 + order: 290 --- ## ptr_offset() @@ -11,7 +11,7 @@ sidebar: function ptr_offset(pointer $pointer, int $offset): mixed ``` -Lowers `ptr_offset(pointer, offset)` by adding a byte offset to a raw address. +Returns a new pointer offset from the given pointer by the given byte count. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_read16.md b/docs/php/builtins/pointer/ptr_read16.md index 5ce9b8fd14..929ce1b363 100644 --- a/docs/php/builtins/pointer/ptr_read16.md +++ b/docs/php/builtins/pointer/ptr_read16.md @@ -1,8 +1,8 @@ --- title: "ptr_read16()" -description: "Lowers `ptr_read16(pointer)` by reading one unsigned 16-bit word through a checked pointer." +description: "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer." sidebar: - order: 273 + order: 291 --- ## ptr_read16() @@ -11,7 +11,7 @@ sidebar: function ptr_read16(pointer $pointer): int ``` -Lowers `ptr_read16(pointer)` by reading one unsigned 16-bit word through a checked pointer. +Reads one unsigned 16-bit word through a raw pointer and returns it as an integer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_read32.md b/docs/php/builtins/pointer/ptr_read32.md index 4e93335e75..c5c3231ce7 100644 --- a/docs/php/builtins/pointer/ptr_read32.md +++ b/docs/php/builtins/pointer/ptr_read32.md @@ -1,8 +1,8 @@ --- title: "ptr_read32()" -description: "Lowers `ptr_read32(pointer)` by reading one unsigned 32-bit word through a checked pointer." +description: "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer." sidebar: - order: 274 + order: 292 --- ## ptr_read32() @@ -11,7 +11,7 @@ sidebar: function ptr_read32(pointer $pointer): int ``` -Lowers `ptr_read32(pointer)` by reading one unsigned 32-bit word through a checked pointer. +Reads one unsigned 32-bit word through a raw pointer and returns it as an integer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_read8.md b/docs/php/builtins/pointer/ptr_read8.md index 605f453dec..68aa17af17 100644 --- a/docs/php/builtins/pointer/ptr_read8.md +++ b/docs/php/builtins/pointer/ptr_read8.md @@ -1,8 +1,8 @@ --- title: "ptr_read8()" -description: "Lowers `ptr_read8(pointer)` by reading one unsigned byte through a checked pointer." +description: "Reads one unsigned byte through a raw pointer and returns it as an integer." sidebar: - order: 275 + order: 293 --- ## ptr_read8() @@ -11,7 +11,7 @@ sidebar: function ptr_read8(pointer $pointer): int ``` -Lowers `ptr_read8(pointer)` by reading one unsigned byte through a checked pointer. +Reads one unsigned byte through a raw pointer and returns it as an integer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_read_string.md b/docs/php/builtins/pointer/ptr_read_string.md index fca1020c8b..6254238376 100644 --- a/docs/php/builtins/pointer/ptr_read_string.md +++ b/docs/php/builtins/pointer/ptr_read_string.md @@ -1,8 +1,8 @@ --- title: "ptr_read_string()" -description: "Lowers `ptr_read_string(pointer, length)` by copying raw bytes into an owned PHP string." +description: "Copies raw bytes from a pointer into a PHP string of the given length." sidebar: - order: 276 + order: 294 --- ## ptr_read_string() @@ -11,7 +11,7 @@ sidebar: function ptr_read_string(pointer $pointer, int $length): string ``` -Lowers `ptr_read_string(pointer, length)` by copying raw bytes into an owned PHP string. +Copies raw bytes from a pointer into a PHP string of the given length. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_set.md b/docs/php/builtins/pointer/ptr_set.md index 69edde40c8..bf78a1d467 100644 --- a/docs/php/builtins/pointer/ptr_set.md +++ b/docs/php/builtins/pointer/ptr_set.md @@ -1,8 +1,8 @@ --- title: "ptr_set()" -description: "Lowers `ptr_set(pointer, value)` by writing one machine word through a checked pointer." +description: "Writes one machine word through a raw pointer." sidebar: - order: 277 + order: 295 --- ## ptr_set() @@ -11,7 +11,7 @@ sidebar: function ptr_set(pointer $pointer, mixed $value): void ``` -Lowers `ptr_set(pointer, value)` by writing one machine word through a checked pointer. +Writes one machine word through a raw pointer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_sizeof.md b/docs/php/builtins/pointer/ptr_sizeof.md index 0dbdcd96ff..de554e86d7 100644 --- a/docs/php/builtins/pointer/ptr_sizeof.md +++ b/docs/php/builtins/pointer/ptr_sizeof.md @@ -1,22 +1,22 @@ --- title: "ptr_sizeof()" -description: "Lowers `ptr_sizeof(\"type\")` by materializing the checked static byte size." +description: "Returns the byte size of the named pointer target type." sidebar: - order: 278 + order: 296 --- ## ptr_sizeof() ```php -function ptr_sizeof(string $type): mixed +function ptr_sizeof(string $type): int ``` -Lowers `ptr_sizeof("type")` by materializing the checked static byte size. +Returns the byte size of the named pointer target type. **Parameters**: - `$type` (`string`) -**Returns**: `mixed` +**Returns**: `int` _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 6eceb1abe7..3ba6deb729 100644 --- a/docs/php/builtins/pointer/ptr_write16.md +++ b/docs/php/builtins/pointer/ptr_write16.md @@ -1,8 +1,8 @@ --- title: "ptr_write16()" -description: "Lowers `ptr_write16(pointer, value)` by writing one 16-bit word through a checked pointer." +description: "Writes one 16-bit word through a raw pointer." sidebar: - order: 279 + order: 297 --- ## ptr_write16() @@ -11,7 +11,7 @@ sidebar: function ptr_write16(pointer $pointer, int $value): void ``` -Lowers `ptr_write16(pointer, value)` by writing one 16-bit word through a checked pointer. +Writes one 16-bit word through a raw pointer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_write32.md b/docs/php/builtins/pointer/ptr_write32.md index a89852cb66..1d4647c9d4 100644 --- a/docs/php/builtins/pointer/ptr_write32.md +++ b/docs/php/builtins/pointer/ptr_write32.md @@ -1,8 +1,8 @@ --- title: "ptr_write32()" -description: "Lowers `ptr_write32(pointer, value)` by writing one 32-bit word through a checked pointer." +description: "Writes one 32-bit word through a raw pointer." sidebar: - order: 280 + order: 298 --- ## ptr_write32() @@ -11,7 +11,7 @@ sidebar: function ptr_write32(pointer $pointer, int $value): void ``` -Lowers `ptr_write32(pointer, value)` by writing one 32-bit word through a checked pointer. +Writes one 32-bit word through a raw pointer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_write8.md b/docs/php/builtins/pointer/ptr_write8.md index d903fb5297..d4d0c1474d 100644 --- a/docs/php/builtins/pointer/ptr_write8.md +++ b/docs/php/builtins/pointer/ptr_write8.md @@ -1,8 +1,8 @@ --- title: "ptr_write8()" -description: "Lowers `ptr_write8(pointer, value)` by writing one byte through a checked pointer." +description: "Writes one byte through a raw pointer." sidebar: - order: 281 + order: 299 --- ## ptr_write8() @@ -11,7 +11,7 @@ sidebar: function ptr_write8(pointer $pointer, int $value): void ``` -Lowers `ptr_write8(pointer, value)` by writing one byte through a checked pointer. +Writes one byte through a raw pointer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/pointer/ptr_write_string.md b/docs/php/builtins/pointer/ptr_write_string.md index 936c658958..373db36ea0 100644 --- a/docs/php/builtins/pointer/ptr_write_string.md +++ b/docs/php/builtins/pointer/ptr_write_string.md @@ -1,8 +1,8 @@ --- title: "ptr_write_string()" -description: "Lowers `ptr_write_string(pointer, string)` by copying PHP string bytes into raw memory." +description: "Copies PHP string bytes into raw memory at the given pointer." sidebar: - order: 282 + order: 300 --- ## ptr_write_string() @@ -11,7 +11,7 @@ sidebar: function ptr_write_string(pointer $pointer, string $string): int ``` -Lowers `ptr_write_string(pointer, string)` by copying PHP string bytes into raw memory. +Copies PHP string bytes into raw memory at the given pointer. **Parameters**: - `$pointer` (`pointer`) diff --git a/docs/php/builtins/process.md b/docs/php/builtins/process.md index ae2e46e680..dacbecf105 100644 --- a/docs/php/builtins/process.md +++ b/docs/php/builtins/process.md @@ -10,13 +10,13 @@ sidebar: | Function | Signature | Returns | |---|---|---| | [`die()`](./process/die.md) | `(int $status): void` | `void` | -| [`exec()`](./process/exec.md) | `(string $command, array $output, int $result_code): string` | `string` | +| [`exec()`](./process/exec.md) | `(string $command): string` | `string` | | [`exit()`](./process/exit.md) | `(int $status): void` | `void` | -| [`passthru()`](./process/passthru.md) | `(string $command, int $result_code): 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): 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, int $result_code): string` | `string` | +| [`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 e4e9c568f4..00b2780d87 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: 283 + order: 301 --- ## die() diff --git a/docs/php/builtins/process/exec.md b/docs/php/builtins/process/exec.md index d87bc5083d..a4906301cd 100644 --- a/docs/php/builtins/process/exec.md +++ b/docs/php/builtins/process/exec.md @@ -1,22 +1,20 @@ --- title: "exec()" -description: "Lowers `exec(command)` by capturing shell stdout through the shared runtime helper." +description: "Executes an external program and returns the last line of output." sidebar: - order: 284 + order: 302 --- ## exec() ```php -function exec(string $command, array $output, int $result_code): string +function exec(string $command): string ``` -Lowers `exec(command)` by capturing shell stdout through the shared runtime helper. +Executes an external program and returns the last line of output. **Parameters**: - `$command` (`string`) -- `$output` (`array`), passed by reference -- `$result_code` (`int`), passed by reference **Returns**: `string` diff --git a/docs/php/builtins/process/exit.md b/docs/php/builtins/process/exit.md index 72daab3e6a..d74c1d9415 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: 285 + order: 303 --- ## exit() diff --git a/docs/php/builtins/process/passthru.md b/docs/php/builtins/process/passthru.md index c4670d922b..95c25c8e40 100644 --- a/docs/php/builtins/process/passthru.md +++ b/docs/php/builtins/process/passthru.md @@ -1,21 +1,20 @@ --- title: "passthru()" -description: "Lowers `passthru(command)` through libc `system()` for direct stdout passthrough." +description: "Executes an external program and passes its output directly." sidebar: - order: 286 + order: 304 --- ## passthru() ```php -function passthru(string $command, int $result_code): void +function passthru(string $command): void ``` -Lowers `passthru(command)` through libc `system()` for direct stdout passthrough. +Executes an external program and passes its output directly. **Parameters**: - `$command` (`string`) -- `$result_code` (`int`), passed by reference **Returns**: `void` diff --git a/docs/php/builtins/process/pclose.md b/docs/php/builtins/process/pclose.md index 730e522cec..3f8ef82cb6 100644 --- a/docs/php/builtins/process/pclose.md +++ b/docs/php/builtins/process/pclose.md @@ -1,8 +1,8 @@ --- title: "pclose()" -description: "Lowers `pclose(handle)` and returns the child process status." +description: "Closes process file pointer." sidebar: - order: 287 + order: 305 --- ## pclose() @@ -11,7 +11,7 @@ sidebar: function pclose(resource $handle): int ``` -Lowers `pclose(handle)` and returns the child process status. +Closes process file pointer. **Parameters**: - `$handle` (`resource`) diff --git a/docs/php/builtins/process/popen.md b/docs/php/builtins/process/popen.md index 84477f5020..c384e80f68 100644 --- a/docs/php/builtins/process/popen.md +++ b/docs/php/builtins/process/popen.md @@ -1,8 +1,8 @@ --- title: "popen()" -description: "Lowers `popen(command, mode)` and boxes the process pipe as `resource|false`." +description: "Opens process file pointer." sidebar: - order: 288 + order: 306 --- ## popen() @@ -11,7 +11,7 @@ sidebar: function popen(string $command, string $mode): mixed ``` -Lowers `popen(command, mode)` and boxes the process pipe as `resource|false`. +Opens process file pointer. **Parameters**: - `$command` (`string`) diff --git a/docs/php/builtins/process/readline.md b/docs/php/builtins/process/readline.md index 6ab2a5891d..cb0d5de245 100644 --- a/docs/php/builtins/process/readline.md +++ b/docs/php/builtins/process/readline.md @@ -1,20 +1,20 @@ --- title: "readline()" -description: "Lowers `readline(prompt?)` by optionally writing a prompt and reading stdin." +description: "Reads a line from the user's terminal." sidebar: - order: 289 + order: 307 --- ## readline() ```php -function readline(string $prompt): mixed +function readline(string $prompt = null): mixed ``` -Lowers `readline(prompt?)` by optionally writing a prompt and reading stdin. +Reads a line from the user's terminal. **Parameters**: -- `$prompt` (`string`), optional +- `$prompt` (`string`), default `null`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/process/shell_exec.md b/docs/php/builtins/process/shell_exec.md index 50b475645f..68cbc6d625 100644 --- a/docs/php/builtins/process/shell_exec.md +++ b/docs/php/builtins/process/shell_exec.md @@ -1,8 +1,8 @@ --- title: "shell_exec()" -description: "Lowers `shell_exec(command)` by capturing shell stdout through the shared runtime helper." +description: "Executes a command via the shell and returns the complete output as a string." sidebar: - order: 290 + order: 308 --- ## shell_exec() @@ -11,7 +11,7 @@ sidebar: function shell_exec(string $command): string ``` -Lowers `shell_exec(command)` by capturing shell stdout through the shared runtime helper. +Executes a command via the shell and returns the complete output as a string. **Parameters**: - `$command` (`string`) diff --git a/docs/php/builtins/process/sleep.md b/docs/php/builtins/process/sleep.md index 0a303458c0..327b1f7399 100644 --- a/docs/php/builtins/process/sleep.md +++ b/docs/php/builtins/process/sleep.md @@ -1,8 +1,8 @@ --- title: "sleep()" -description: "Lowers `sleep(seconds)` through the target's C library symbol." +description: "Delays execution for a number of seconds." sidebar: - order: 291 + order: 309 --- ## sleep() @@ -11,7 +11,7 @@ sidebar: function sleep(int $seconds): int ``` -Lowers `sleep(seconds)` through the target's C library symbol. +Delays execution for a number of seconds. **Parameters**: - `$seconds` (`int`) diff --git a/docs/php/builtins/process/system.md b/docs/php/builtins/process/system.md index 5e432fe9ca..84df401224 100644 --- a/docs/php/builtins/process/system.md +++ b/docs/php/builtins/process/system.md @@ -1,21 +1,20 @@ --- title: "system()" -description: "Lowers `system(command)` through libc `system()` and returns the legacy empty string result." +description: "Executes an external program and displays the output." sidebar: - order: 292 + order: 310 --- ## system() ```php -function system(string $command, int $result_code): string +function system(string $command): string ``` -Lowers `system(command)` through libc `system()` and returns the legacy empty string result. +Executes an external program and displays the output. **Parameters**: - `$command` (`string`) -- `$result_code` (`int`), passed by reference **Returns**: `string` diff --git a/docs/php/builtins/process/usleep.md b/docs/php/builtins/process/usleep.md index 361e8dcda5..3b790742f7 100644 --- a/docs/php/builtins/process/usleep.md +++ b/docs/php/builtins/process/usleep.md @@ -1,8 +1,8 @@ --- title: "usleep()" -description: "Lowers `usleep(microseconds)` through the target's C library symbol." +description: "Delays execution for a number of microseconds." sidebar: - order: 293 + order: 311 --- ## usleep() @@ -11,7 +11,7 @@ sidebar: function usleep(int $microseconds): void ``` -Lowers `usleep(microseconds)` through the target's C library symbol. +Delays execution for a number of microseconds. **Parameters**: - `$microseconds` (`int`) diff --git a/docs/php/builtins/regex.md b/docs/php/builtins/regex.md index 06d27a7aac..bce803832f 100644 --- a/docs/php/builtins/regex.md +++ b/docs/php/builtins/regex.md @@ -9,8 +9,8 @@ sidebar: | Function | Signature | Returns | |---|---|---| -| [`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, array $matches): int` | `int` | -| [`preg_replace()`](./regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject, int $limit = -1, int $count = null): string` | `string` | -| [`preg_replace_callback()`](./regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject, int $limit = -1, int $count = null, int $flags = 0): array` | `array` | -| [`preg_split()`](./regex/preg_split.md) | `(string $pattern, string $subject, int $limit, int $flags): array` | `array` | +| [`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/preg_match.md b/docs/php/builtins/regex/preg_match.md index ddea72625c..481d9f5ea5 100644 --- a/docs/php/builtins/regex/preg_match.md +++ b/docs/php/builtins/regex/preg_match.md @@ -1,22 +1,22 @@ --- title: "preg_match()" -description: "Lowers `preg_match(pattern, subject)` through the shared regex runtime helper." +description: "Performs a regular expression match." sidebar: - order: 294 + order: 312 --- ## preg_match() ```php -function preg_match(string $pattern, string $subject, array $matches): int +function preg_match(string $pattern, string $subject, array $matches = []): int ``` -Lowers `preg_match(pattern, subject)` through the shared regex runtime helper. +Performs a regular expression match. **Parameters**: - `$pattern` (`string`) - `$subject` (`string`) -- `$matches` (`array`), passed by reference, optional +- `$matches` (`array`), passed by reference, default `[]`, optional **Returns**: `int` diff --git a/docs/php/builtins/regex/preg_match_all.md b/docs/php/builtins/regex/preg_match_all.md index caa03eb805..42fb2cc236 100644 --- a/docs/php/builtins/regex/preg_match_all.md +++ b/docs/php/builtins/regex/preg_match_all.md @@ -1,22 +1,21 @@ --- title: "preg_match_all()" -description: "Lowers `preg_match_all(pattern, subject)` through the shared regex runtime helper." +description: "Performs a global regular expression match and returns the number of matches." sidebar: - order: 295 + order: 313 --- ## preg_match_all() ```php -function preg_match_all(string $pattern, string $subject, array $matches): int +function preg_match_all(string $pattern, string $subject): int ``` -Lowers `preg_match_all(pattern, subject)` through the shared regex runtime helper. +Performs a global regular expression match and returns the number of matches. **Parameters**: - `$pattern` (`string`) - `$subject` (`string`) -- `$matches` (`array`), passed by reference **Returns**: `int` diff --git a/docs/php/builtins/regex/preg_replace.md b/docs/php/builtins/regex/preg_replace.md index fff5f64622..434ee8fcb8 100644 --- a/docs/php/builtins/regex/preg_replace.md +++ b/docs/php/builtins/regex/preg_replace.md @@ -1,24 +1,22 @@ --- title: "preg_replace()" -description: "Lowers `preg_replace(pattern, replacement, subject)` through the regex replacement helper." +description: "Performs a regular expression search and replace." sidebar: - order: 296 + order: 314 --- ## preg_replace() ```php -function preg_replace(string $pattern, string $replacement, string $subject, int $limit = -1, int $count = null): string +function preg_replace(string $pattern, string $replacement, string $subject): string ``` -Lowers `preg_replace(pattern, replacement, subject)` through the regex replacement helper. +Performs a regular expression search and replace. **Parameters**: - `$pattern` (`string`) - `$replacement` (`string`) - `$subject` (`string`) -- `$limit` (`int`), default `-1`, optional -- `$count` (`int`), passed by reference, default `null`, optional **Returns**: `string` diff --git a/docs/php/builtins/regex/preg_replace_callback.md b/docs/php/builtins/regex/preg_replace_callback.md index 5945c461be..a50397cf54 100644 --- a/docs/php/builtins/regex/preg_replace_callback.md +++ b/docs/php/builtins/regex/preg_replace_callback.md @@ -1,27 +1,24 @@ --- title: "preg_replace_callback()" -description: "Lowers `preg_replace_callback(pattern, callback, subject)` through supported direct callbacks." +description: "Performs a regular expression search and replace using a callback." sidebar: - order: 297 + order: 315 --- ## preg_replace_callback() ```php -function preg_replace_callback(string $pattern, callable $callback, string $subject, int $limit = -1, int $count = null, int $flags = 0): array +function preg_replace_callback(string $pattern, callable $callback, string $subject): string ``` -Lowers `preg_replace_callback(pattern, callback, subject)` through supported direct callbacks. +Performs a regular expression search and replace using a callback. **Parameters**: - `$pattern` (`string`) - `$callback` (`callable`) - `$subject` (`string`) -- `$limit` (`int`), default `-1`, optional -- `$count` (`int`), passed by reference, default `null`, optional -- `$flags` (`int`), default `0`, optional -**Returns**: `array` +**Returns**: `string` _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 518d8ec2c0..1e63c8af68 100644 --- a/docs/php/builtins/regex/preg_split.md +++ b/docs/php/builtins/regex/preg_split.md @@ -1,23 +1,23 @@ --- title: "preg_split()" -description: "Lowers `preg_split(pattern, subject, limit?, flags?)` through the regex split helper." +description: "Splits a string by a regular expression." sidebar: - order: 298 + order: 316 --- ## preg_split() ```php -function preg_split(string $pattern, string $subject, int $limit, int $flags): array +function preg_split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array ``` -Lowers `preg_split(pattern, subject, limit?, flags?)` through the regex split helper. +Splits a string by a regular expression. **Parameters**: - `$pattern` (`string`) - `$subject` (`string`) -- `$limit` (`int`), optional -- `$flags` (`int`), optional +- `$limit` (`int`), default `-1`, optional +- `$flags` (`int`), default `0`, optional **Returns**: `array` diff --git a/docs/php/builtins/spl.md b/docs/php/builtins/spl.md index 0041b8e697..c94612f328 100644 --- a/docs/php/builtins/spl.md +++ b/docs/php/builtins/spl.md @@ -9,14 +9,14 @@ sidebar: | Function | Signature | Returns | |---|---|---| -| [`iterator_apply()`](./spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args): int` | `int` | +| [`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): array` | `array` | -| [`spl_autoload()`](./spl/spl_autoload.md) | `(string $class, string $file_extensions): void` | `void` | +| [`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): string` | `string` | +| [`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, bool $throw, bool $prepend): bool` | `bool` | +| [`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` | diff --git a/docs/php/builtins/spl/iterator_apply.md b/docs/php/builtins/spl/iterator_apply.md index 5720a2874f..dc02145650 100644 --- a/docs/php/builtins/spl/iterator_apply.md +++ b/docs/php/builtins/spl/iterator_apply.md @@ -1,22 +1,22 @@ --- title: "iterator_apply()" -description: "Lowers `iterator_apply()` over supported Traversable sources and callback forms." +description: "Call a function for every element in an iterator." sidebar: - order: 299 + order: 317 --- ## iterator_apply() ```php -function iterator_apply(traversable $iterator, callable $callback, array $args): int +function iterator_apply(traversable $iterator, callable $callback, array $args = null): int ``` -Lowers `iterator_apply()` over supported Traversable sources and callback forms. +Call a function for every element in an iterator. **Parameters**: - `$iterator` (`traversable`) - `$callback` (`callable`) -- `$args` (`array`), optional +- `$args` (`array`), default `null`, optional **Returns**: `int` diff --git a/docs/php/builtins/spl/iterator_count.md b/docs/php/builtins/spl/iterator_count.md index 45dc100556..43f4e9c7e4 100644 --- a/docs/php/builtins/spl/iterator_count.md +++ b/docs/php/builtins/spl/iterator_count.md @@ -1,8 +1,8 @@ --- title: "iterator_count()" -description: "Lowers `iterator_count()` over arrays, `iterable`, and Traversable objects." +description: "Count the elements in an iterator." sidebar: - order: 300 + order: 318 --- ## iterator_count() @@ -11,7 +11,7 @@ sidebar: function iterator_count(traversable $iterator): int ``` -Lowers `iterator_count()` over arrays, `iterable`, and Traversable objects. +Count the elements in an iterator. **Parameters**: - `$iterator` (`traversable`) diff --git a/docs/php/builtins/spl/iterator_to_array.md b/docs/php/builtins/spl/iterator_to_array.md index c0e61c92c3..bfe3ff5d63 100644 --- a/docs/php/builtins/spl/iterator_to_array.md +++ b/docs/php/builtins/spl/iterator_to_array.md @@ -1,21 +1,21 @@ --- title: "iterator_to_array()" -description: "Lowers `iterator_to_array()` over arrays, `iterable`, and Traversable objects." +description: "Copy the iterator into an array." sidebar: - order: 301 + order: 319 --- ## iterator_to_array() ```php -function iterator_to_array(traversable $iterator, bool $preserve_keys): array +function iterator_to_array(traversable $iterator, bool $preserve_keys = true): array ``` -Lowers `iterator_to_array()` over arrays, `iterable`, and Traversable objects. +Copy the iterator into an array. **Parameters**: - `$iterator` (`traversable`) -- `$preserve_keys` (`bool`), optional +- `$preserve_keys` (`bool`), default `true`, optional **Returns**: `array` diff --git a/docs/php/builtins/spl/spl_autoload.md b/docs/php/builtins/spl/spl_autoload.md index 865bd54aad..cd32f0639f 100644 --- a/docs/php/builtins/spl/spl_autoload.md +++ b/docs/php/builtins/spl/spl_autoload.md @@ -1,21 +1,21 @@ --- title: "spl_autoload()" -description: "Lowers no-op autoload calls by preserving arg effects and returning PHP null if used." +description: "Default implementation for __autoload()." sidebar: - order: 302 + order: 320 --- ## spl_autoload() ```php -function spl_autoload(string $class, string $file_extensions): void +function spl_autoload(string $class, string $file_extensions = null): void ``` -Lowers no-op autoload calls by preserving arg effects and returning PHP null if used. +Default implementation for __autoload(). **Parameters**: - `$class` (`string`) -- `$file_extensions` (`string`), optional +- `$file_extensions` (`string`), default `null`, optional **Returns**: `void` diff --git a/docs/php/builtins/spl/spl_autoload_call.md b/docs/php/builtins/spl/spl_autoload_call.md index 76ed1936ab..e8fd92ac37 100644 --- a/docs/php/builtins/spl/spl_autoload_call.md +++ b/docs/php/builtins/spl/spl_autoload_call.md @@ -1,8 +1,8 @@ --- title: "spl_autoload_call()" -description: "Lowers no-op autoload calls by preserving arg effects and returning PHP null if used." +description: "Try all registered __autoload() functions to load the requested class." sidebar: - order: 303 + order: 321 --- ## spl_autoload_call() @@ -11,7 +11,7 @@ sidebar: function spl_autoload_call(string $class): void ``` -Lowers no-op autoload calls by preserving arg effects and returning PHP null if used. +Try all registered __autoload() functions to load the requested class. **Parameters**: - `$class` (`string`) diff --git a/docs/php/builtins/spl/spl_autoload_extensions.md b/docs/php/builtins/spl/spl_autoload_extensions.md index 6431f96bb6..2064f3fa38 100644 --- a/docs/php/builtins/spl/spl_autoload_extensions.md +++ b/docs/php/builtins/spl/spl_autoload_extensions.md @@ -1,20 +1,20 @@ --- title: "spl_autoload_extensions()" -description: "Lowers `spl_autoload_extensions()` against the legacy mutable extension globals." +description: "Register and return default file extensions for spl_autoload." sidebar: - order: 304 + order: 322 --- ## spl_autoload_extensions() ```php -function spl_autoload_extensions(string $file_extensions): string +function spl_autoload_extensions(string $file_extensions = null): string ``` -Lowers `spl_autoload_extensions()` against the legacy mutable extension globals. +Register and return default file extensions for spl_autoload. **Parameters**: -- `$file_extensions` (`string`), optional +- `$file_extensions` (`string`), default `null`, optional **Returns**: `string` diff --git a/docs/php/builtins/spl/spl_autoload_functions.md b/docs/php/builtins/spl/spl_autoload_functions.md index ed11b72f73..4923f54bac 100644 --- a/docs/php/builtins/spl/spl_autoload_functions.md +++ b/docs/php/builtins/spl/spl_autoload_functions.md @@ -1,8 +1,8 @@ --- title: "spl_autoload_functions()" -description: "Lowers `spl_autoload_functions()` to an indexed array of AOT rule placeholders." +description: "Return all registered __autoload() functions." sidebar: - order: 305 + order: 323 --- ## spl_autoload_functions() @@ -11,7 +11,7 @@ sidebar: function spl_autoload_functions(): array ``` -Lowers `spl_autoload_functions()` to an indexed array of AOT rule placeholders. +Return all registered __autoload() functions. **Parameters**: none. diff --git a/docs/php/builtins/spl/spl_autoload_register.md b/docs/php/builtins/spl/spl_autoload_register.md index e4166a6d7d..8cfa2dcb01 100644 --- a/docs/php/builtins/spl/spl_autoload_register.md +++ b/docs/php/builtins/spl/spl_autoload_register.md @@ -1,22 +1,22 @@ --- title: "spl_autoload_register()" -description: "Lowers autoload registration stubs by preserving arg effects and returning true." +description: "Register given function as __autoload() implementation." sidebar: - order: 306 + order: 324 --- ## spl_autoload_register() ```php -function spl_autoload_register(callable $callback, bool $throw, bool $prepend): bool +function spl_autoload_register(callable $callback = null, bool $throw = true, bool $prepend = false): bool ``` -Lowers autoload registration stubs by preserving arg effects and returning true. +Register given function as __autoload() implementation. **Parameters**: -- `$callback` (`callable`), optional -- `$throw` (`bool`), optional -- `$prepend` (`bool`), optional +- `$callback` (`callable`), default `null`, optional +- `$throw` (`bool`), default `true`, optional +- `$prepend` (`bool`), default `false`, optional **Returns**: `bool` diff --git a/docs/php/builtins/spl/spl_autoload_unregister.md b/docs/php/builtins/spl/spl_autoload_unregister.md index 132836e70e..76efe13a57 100644 --- a/docs/php/builtins/spl/spl_autoload_unregister.md +++ b/docs/php/builtins/spl/spl_autoload_unregister.md @@ -1,8 +1,8 @@ --- title: "spl_autoload_unregister()" -description: "Lowers autoload registration stubs by preserving arg effects and returning true." +description: "Unregister given function as __autoload() implementation." sidebar: - order: 307 + order: 325 --- ## spl_autoload_unregister() @@ -11,7 +11,7 @@ sidebar: function spl_autoload_unregister(callable $callback): bool ``` -Lowers autoload registration stubs by preserving arg effects and returning true. +Unregister given function as __autoload() implementation. **Parameters**: - `$callback` (`callable`) diff --git a/docs/php/builtins/spl/spl_classes.md b/docs/php/builtins/spl/spl_classes.md index daaa2d3aa8..fe3f1ec019 100644 --- a/docs/php/builtins/spl/spl_classes.md +++ b/docs/php/builtins/spl/spl_classes.md @@ -1,8 +1,8 @@ --- title: "spl_classes()" -description: "Lowers `spl_classes()` to the static compiler-shipped SPL/core type snapshot." +description: "Return available SPL classes." sidebar: - order: 308 + order: 326 --- ## spl_classes() @@ -11,7 +11,7 @@ sidebar: function spl_classes(): array ``` -Lowers `spl_classes()` to the static compiler-shipped SPL/core type snapshot. +Return available SPL classes. **Parameters**: none. diff --git a/docs/php/builtins/spl/spl_object_hash.md b/docs/php/builtins/spl/spl_object_hash.md index ed80db38a9..14c5edb5e7 100644 --- a/docs/php/builtins/spl/spl_object_hash.md +++ b/docs/php/builtins/spl/spl_object_hash.md @@ -1,8 +1,8 @@ --- title: "spl_object_hash()" -description: "Lowers `spl_object_hash(object)` by formatting the loaded object pointer as a string." +description: "Return hash id for given object." sidebar: - order: 309 + order: 327 --- ## spl_object_hash() @@ -11,7 +11,7 @@ sidebar: function spl_object_hash(object $object): string ``` -Lowers `spl_object_hash(object)` by formatting the loaded object pointer as a string. +Return hash id for given object. **Parameters**: - `$object` (`object`) diff --git a/docs/php/builtins/spl/spl_object_id.md b/docs/php/builtins/spl/spl_object_id.md index 3445fc66bc..f2adf84e6a 100644 --- a/docs/php/builtins/spl/spl_object_id.md +++ b/docs/php/builtins/spl/spl_object_id.md @@ -1,8 +1,8 @@ --- title: "spl_object_id()" -description: "Lowers `spl_object_id(object)` by returning the loaded object pointer as an integer." +description: "Return the integer object handle for given object." sidebar: - order: 310 + order: 328 --- ## spl_object_id() @@ -11,7 +11,7 @@ sidebar: function spl_object_id(object $object): int ``` -Lowers `spl_object_id(object)` by returning the loaded object pointer as an integer. +Return the integer object handle for given object. **Parameters**: - `$object` (`object`) diff --git a/docs/php/builtins/streams.md b/docs/php/builtins/streams.md index 5af11fc0cf..a2ab662d99 100644 --- a/docs/php/builtins/streams.md +++ b/docs/php/builtins/streams.md @@ -9,9 +9,9 @@ sidebar: | Function | Signature | Returns | |---|---|---| -| [`fsockopen()`](./streams/fsockopen.md) | `(string $hostname, int $port, int $error_code, string $error_message, float $timeout): mixed` | `mixed` | -| [`pfsockopen()`](./streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code, string $error_message, float $timeout): mixed` | `mixed` | +| [`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 $filter_name, int $mode, mixed $params): mixed` | `mixed` | -| [`stream_filter_prepend()`](./streams/stream_filter_prepend.md) | `(resource $stream, string $filter_name, int $mode, mixed $params): mixed` | `mixed` | +| [`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 667ea54dd9..ed08cb072e 100644 --- a/docs/php/builtins/streams/fsockopen.md +++ b/docs/php/builtins/streams/fsockopen.md @@ -1,24 +1,24 @@ --- title: "fsockopen()" -description: "fsockopen() — streams builtin supported by Elephc." +description: "Open Internet or Unix domain socket connection." sidebar: - order: 311 + order: 329 --- ## fsockopen() ```php -function fsockopen(string $hostname, int $port, int $error_code, string $error_message, float $timeout): mixed +function fsockopen(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed ``` -`fsockopen()` is a streams builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Open Internet or Unix domain socket connection. **Parameters**: - `$hostname` (`string`) - `$port` (`int`) -- `$error_code` (`int`), passed by reference, optional -- `$error_message` (`string`), passed by reference, optional -- `$timeout` (`float`), optional +- `$error_code` (`int`), passed by reference, default `null`, optional +- `$error_message` (`string`), passed by reference, default `null`, optional +- `$timeout` (`float`), default `null`, optional **Returns**: `mixed` @@ -30,3 +30,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `fsockopen` is implemented in the compiler, see [the internals page](../../../internals/builtins/streams/fsockopen.md). + diff --git a/docs/php/builtins/streams/pfsockopen.md b/docs/php/builtins/streams/pfsockopen.md index 7bd8d5a712..747af755ed 100644 --- a/docs/php/builtins/streams/pfsockopen.md +++ b/docs/php/builtins/streams/pfsockopen.md @@ -1,24 +1,24 @@ --- title: "pfsockopen()" -description: "pfsockopen() — streams builtin supported by Elephc." +description: "Open persistent Internet or Unix domain socket connection." sidebar: - order: 312 + order: 330 --- ## pfsockopen() ```php -function pfsockopen(string $hostname, int $port, int $error_code, string $error_message, float $timeout): mixed +function pfsockopen(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed ``` -`pfsockopen()` is a streams builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Open persistent Internet or Unix domain socket connection. **Parameters**: - `$hostname` (`string`) - `$port` (`int`) -- `$error_code` (`int`), passed by reference, optional -- `$error_message` (`string`), passed by reference, optional -- `$timeout` (`float`), optional +- `$error_code` (`int`), passed by reference, default `null`, optional +- `$error_message` (`string`), passed by reference, default `null`, optional +- `$timeout` (`float`), default `null`, optional **Returns**: `mixed` @@ -30,3 +30,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `pfsockopen` is implemented in the compiler, see [the internals page](../../../internals/builtins/streams/pfsockopen.md). + diff --git a/docs/php/builtins/streams/stream_bucket_append.md b/docs/php/builtins/streams/stream_bucket_append.md index 26ce7bfa13..8ba572a055 100644 --- a/docs/php/builtins/streams/stream_bucket_append.md +++ b/docs/php/builtins/streams/stream_bucket_append.md @@ -1,8 +1,8 @@ --- title: "stream_bucket_append()" -description: "stream_bucket_append() — streams builtin supported by Elephc." +description: "Appends a bucket to the brigade." sidebar: - order: 313 + order: 331 --- ## stream_bucket_append() @@ -11,7 +11,7 @@ sidebar: function stream_bucket_append(mixed $brigade, mixed $bucket): void ``` -`stream_bucket_append()` is a streams builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Appends a bucket to the brigade. **Parameters**: - `$brigade` (`mixed`) @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `stream_bucket_append` is implemented in the compiler, see [the internals page](../../../internals/builtins/streams/stream_bucket_append.md). + diff --git a/docs/php/builtins/streams/stream_bucket_prepend.md b/docs/php/builtins/streams/stream_bucket_prepend.md index 0f8a015932..fa6d27bff8 100644 --- a/docs/php/builtins/streams/stream_bucket_prepend.md +++ b/docs/php/builtins/streams/stream_bucket_prepend.md @@ -1,8 +1,8 @@ --- title: "stream_bucket_prepend()" -description: "stream_bucket_prepend() — streams builtin supported by Elephc." +description: "Prepends a bucket to the brigade." sidebar: - order: 314 + order: 332 --- ## stream_bucket_prepend() @@ -11,7 +11,7 @@ sidebar: function stream_bucket_prepend(mixed $brigade, mixed $bucket): void ``` -`stream_bucket_prepend()` is a streams builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Prepends a bucket to the brigade. **Parameters**: - `$brigade` (`mixed`) @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `stream_bucket_prepend` is implemented in the compiler, see [the internals page](../../../internals/builtins/streams/stream_bucket_prepend.md). + diff --git a/docs/php/builtins/streams/stream_filter_append.md b/docs/php/builtins/streams/stream_filter_append.md index 4f7dc54cc8..503f83bc7b 100644 --- a/docs/php/builtins/streams/stream_filter_append.md +++ b/docs/php/builtins/streams/stream_filter_append.md @@ -1,23 +1,23 @@ --- title: "stream_filter_append()" -description: "stream_filter_append() — streams builtin supported by Elephc." +description: "Attaches a filter to a stream." sidebar: - order: 315 + order: 333 --- ## stream_filter_append() ```php -function stream_filter_append(resource $stream, string $filter_name, int $mode, mixed $params): mixed +function stream_filter_append(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed ``` -`stream_filter_append()` is a streams builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Attaches a filter to a stream. **Parameters**: - `$stream` (`resource`) -- `$filter_name` (`string`) -- `$mode` (`int`), optional -- `$params` (`mixed`), optional +- `$filtername` (`string`) +- `$read_write` (`int`), default `3`, optional +- `$params` (`mixed`), default `null`, optional **Returns**: `mixed` @@ -29,3 +29,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `stream_filter_append` is implemented in the compiler, see [the internals page](../../../internals/builtins/streams/stream_filter_append.md). + diff --git a/docs/php/builtins/streams/stream_filter_prepend.md b/docs/php/builtins/streams/stream_filter_prepend.md index 714dcfc4c3..15ee3fcc5c 100644 --- a/docs/php/builtins/streams/stream_filter_prepend.md +++ b/docs/php/builtins/streams/stream_filter_prepend.md @@ -1,23 +1,23 @@ --- title: "stream_filter_prepend()" -description: "stream_filter_prepend() — streams builtin supported by Elephc." +description: "Attaches a filter to a stream (prepend)." sidebar: - order: 316 + order: 334 --- ## stream_filter_prepend() ```php -function stream_filter_prepend(resource $stream, string $filter_name, int $mode, mixed $params): mixed +function stream_filter_prepend(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed ``` -`stream_filter_prepend()` is a streams builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Attaches a filter to a stream (prepend). **Parameters**: - `$stream` (`resource`) -- `$filter_name` (`string`) -- `$mode` (`int`), optional -- `$params` (`mixed`), optional +- `$filtername` (`string`) +- `$read_write` (`int`), default `3`, optional +- `$params` (`mixed`), default `null`, optional **Returns**: `mixed` @@ -29,3 +29,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `stream_filter_prepend` is implemented in the compiler, see [the internals page](../../../internals/builtins/streams/stream_filter_prepend.md). + diff --git a/docs/php/builtins/string.md b/docs/php/builtins/string.md index bf2eda7a71..2ec01ad8d9 100644 --- a/docs/php/builtins/string.md +++ b/docs/php/builtins/string.md @@ -10,73 +10,73 @@ sidebar: | Function | Signature | Returns | |---|---|---| | [`addslashes()`](./string/addslashes.md) | `(string $string): string` | `string` | -| [`base64_decode()`](./string/base64_decode.md) | `(string $string, bool $strict): 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): 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): array` | `array` | +| [`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, int $encoding): string` | `string` | -| [`gzdeflate()`](./string/gzdeflate.md) | `(string $data, int $level, int $encoding): string` | `string` | -| [`gzinflate()`](./string/gzinflate.md) | `(string $data, int $max_length): string` | `string` | -| [`gzuncompress()`](./string/gzuncompress.md) | `(string $data, int $max_length): string` | `string` | -| [`hash()`](./string/hash.md) | `(string $algo, string $data, bool $binary = false, array $options = []): string` | `string` | +| [`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): string` | `string` | -| [`hash_hmac()`](./string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary): string` | `string` | -| [`hash_init()`](./string/hash_init.md) | `(string $algo, int $flags = 0, string $key = '', array $options = []): mixed` | `mixed` | +| [`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, int $flags, string $encoding): string` | `string` | -| [`htmlentities()`](./string/htmlentities.md) | `(string $string, int $flags, string $encoding, bool $double_encode): string` | `string` | -| [`htmlspecialchars()`](./string/htmlspecialchars.md) | `(string $string, int $flags, string $encoding, bool $double_encode): string` | `string` | -| [`implode()`](./string/implode.md) | `(string $separator, array $array): string` | `string` | +| [`html_entity_decode()`](./string/html_entity_decode.md) | `(string $string): string` | `string` | +| [`htmlentities()`](./string/htmlentities.md) | `(string $string): string` | `string` | +| [`htmlspecialchars()`](./string/htmlspecialchars.md) | `(string $string): 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): string` | `string` | -| [`md5()`](./string/md5.md) | `(string $string, bool $binary): string` | `string` | -| [`nl2br()`](./string/nl2br.md) | `(string $string, bool $use_xhtml): string` | `string` | -| [`number_format()`](./string/number_format.md) | `(float $num, int $decimals, string $decimal_separator, string $thousands_separator): 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): string` | `string` | -| [`sha1()`](./string/sha1.md) | `(string $string, bool $binary): 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) | `(mixed $search, mixed $replace, mixed $subject, int $count): mixed` | `mixed` | -| [`str_pad()`](./string/str_pad.md) | `(string $string, int $length, string $pad_string, int $pad_type): string` | `string` | +| [`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): mixed` | `mixed` | -| [`str_split()`](./string/str_split.md) | `(string $string, int $length): array` | `array` | +| [`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): mixed` | `mixed` | +| [`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): mixed` | `mixed` | -| [`strstr()`](./string/strstr.md) | `(string $haystack, string $needle, bool $before_needle): 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): string` | `string` | -| [`substr_replace()`](./string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length): string` | `string` | -| [`trim()`](./string/trim.md) | `(string $string, string $characters): 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): 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, string $break, bool $cut_long_words): 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 11cfaf7eab..7a455bc8b3 100644 --- a/docs/php/builtins/string/addslashes.md +++ b/docs/php/builtins/string/addslashes.md @@ -1,8 +1,8 @@ --- title: "addslashes()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Adds backslashes before characters that need to be escaped." sidebar: - order: 317 + order: 335 --- ## addslashes() @@ -11,7 +11,7 @@ sidebar: function addslashes(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Adds backslashes before characters that need to be escaped. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/base64_decode.md b/docs/php/builtins/string/base64_decode.md index 6a49a55922..15df010bf0 100644 --- a/docs/php/builtins/string/base64_decode.md +++ b/docs/php/builtins/string/base64_decode.md @@ -1,21 +1,20 @@ --- title: "base64_decode()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Decodes a Base64-encoded string back into its original data." sidebar: - order: 318 + order: 336 --- ## base64_decode() ```php -function base64_decode(string $string, bool $strict): string +function base64_decode(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Decodes a Base64-encoded string back into its original data. **Parameters**: - `$string` (`string`) -- `$strict` (`bool`) **Returns**: `string` diff --git a/docs/php/builtins/string/base64_encode.md b/docs/php/builtins/string/base64_encode.md index 11e10dd396..65004319bc 100644 --- a/docs/php/builtins/string/base64_encode.md +++ b/docs/php/builtins/string/base64_encode.md @@ -1,8 +1,8 @@ --- title: "base64_encode()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Encodes binary data into a Base64 string." sidebar: - order: 319 + order: 337 --- ## base64_encode() @@ -11,7 +11,7 @@ sidebar: function base64_encode(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Encodes binary data into a Base64 string. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/bin2hex.md b/docs/php/builtins/string/bin2hex.md index afb3330e29..50bdf61de2 100644 --- a/docs/php/builtins/string/bin2hex.md +++ b/docs/php/builtins/string/bin2hex.md @@ -1,8 +1,8 @@ --- title: "bin2hex()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Converts binary data into its hexadecimal string representation." sidebar: - order: 320 + order: 338 --- ## bin2hex() @@ -11,7 +11,7 @@ sidebar: function bin2hex(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Converts binary data into its hexadecimal string representation. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/chop.md b/docs/php/builtins/string/chop.md index 2f6ed8c194..5788c4d61b 100644 --- a/docs/php/builtins/string/chop.md +++ b/docs/php/builtins/string/chop.md @@ -1,21 +1,21 @@ --- title: "chop()" -description: "chop() — string builtin supported by Elephc." +description: "Alias of rtrim: strips whitespace (or other characters) from the end of a string." sidebar: - order: 321 + order: 339 --- ## chop() ```php -function chop(string $string, string $characters): string +function chop(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string ``` -`chop()` is a string builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Alias of rtrim: strips whitespace (or other characters) from the end of a string. **Parameters**: - `$string` (`string`) -- `$characters` (`string`), optional +- `$characters` (`string`), default `' \n\r\t\x0b\x0c\x00'`, optional **Returns**: `string` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `chop` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/chop.md). + diff --git a/docs/php/builtins/string/chr.md b/docs/php/builtins/string/chr.md index 5a188707af..2d564533cc 100644 --- a/docs/php/builtins/string/chr.md +++ b/docs/php/builtins/string/chr.md @@ -1,8 +1,8 @@ --- title: "chr()" -description: "Lowers `chr()` by converting an integer code point into a one-byte string." +description: "Returns a one-character string from the given byte code point." sidebar: - order: 322 + order: 340 --- ## chr() @@ -11,7 +11,7 @@ sidebar: function chr(int $codepoint): string ``` -Lowers `chr()` by converting an integer code point into a one-byte string. +Returns a one-character string from the given byte code point. **Parameters**: - `$codepoint` (`int`) diff --git a/docs/php/builtins/string/crc32.md b/docs/php/builtins/string/crc32.md index 4243ccd1c2..776a487ad0 100644 --- a/docs/php/builtins/string/crc32.md +++ b/docs/php/builtins/string/crc32.md @@ -1,8 +1,8 @@ --- title: "crc32()" -description: "Lowers `crc32(string)` through the shared checksum runtime helper." +description: "Calculates the CRC32 polynomial of a string." sidebar: - order: 323 + order: 341 --- ## crc32() @@ -11,7 +11,7 @@ sidebar: function crc32(string $string): int ``` -Lowers `crc32(string)` through the shared checksum runtime helper. +Calculates the CRC32 polynomial of a string. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/explode.md b/docs/php/builtins/string/explode.md index e770c731b8..e0fa0f540b 100644 --- a/docs/php/builtins/string/explode.md +++ b/docs/php/builtins/string/explode.md @@ -1,22 +1,22 @@ --- title: "explode()" -description: "Lowers `explode(delimiter, string)` into the shared string-array splitter helper." +description: "Splits a string by a separator into an array of substrings." sidebar: - order: 324 + order: 342 --- ## explode() ```php -function explode(string $separator, string $string, int $limit): array +function explode(string $separator, string $string, int $limit = PHP_INT_MAX): array ``` -Lowers `explode(delimiter, string)` into the shared string-array splitter helper. +Splits a string by a separator into an array of substrings. **Parameters**: - `$separator` (`string`) - `$string` (`string`) -- `$limit` (`int`), optional +- `$limit` (`int`), default `PHP_INT_MAX`, optional **Returns**: `array` diff --git a/docs/php/builtins/string/grapheme_strrev.md b/docs/php/builtins/string/grapheme_strrev.md index 60f1b2139d..c50439bd1c 100644 --- a/docs/php/builtins/string/grapheme_strrev.md +++ b/docs/php/builtins/string/grapheme_strrev.md @@ -1,8 +1,8 @@ --- title: "grapheme_strrev()" -description: "Lowers `grapheme_strrev()` and boxes its `string|false` result as `Mixed`." +description: "Reverses a string by grapheme cluster, returning false on failure." sidebar: - order: 325 + order: 343 --- ## grapheme_strrev() @@ -11,7 +11,7 @@ sidebar: function grapheme_strrev(string $string): mixed ``` -Lowers `grapheme_strrev()` and boxes its `string|false` result as `Mixed`. +Reverses a string by grapheme cluster, returning false on failure. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/gzcompress.md b/docs/php/builtins/string/gzcompress.md index 1eec8ff52b..5bdf0c8ff3 100644 --- a/docs/php/builtins/string/gzcompress.md +++ b/docs/php/builtins/string/gzcompress.md @@ -1,22 +1,21 @@ --- title: "gzcompress()" -description: "Lowers `gzcompress(data, level?)` through inline zlib `compress2` calls." +description: "Compress a string using the ZLIB data format." sidebar: - order: 326 + order: 344 --- ## gzcompress() ```php -function gzcompress(string $data, int $level, int $encoding): string +function gzcompress(string $data, int $level = -1): string ``` -Lowers `gzcompress(data, level?)` through inline zlib `compress2` calls. +Compress a string using the ZLIB data format. **Parameters**: - `$data` (`string`) -- `$level` (`int`), optional -- `$encoding` (`int`) +- `$level` (`int`), default `-1`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/gzdeflate.md b/docs/php/builtins/string/gzdeflate.md index 8b40df02d6..c97a3f7200 100644 --- a/docs/php/builtins/string/gzdeflate.md +++ b/docs/php/builtins/string/gzdeflate.md @@ -1,22 +1,21 @@ --- title: "gzdeflate()" -description: "Lowers `gzdeflate(data, level?)` through inline raw-DEFLATE zlib calls." +description: "Deflate a string using the DEFLATE data format." sidebar: - order: 327 + order: 345 --- ## gzdeflate() ```php -function gzdeflate(string $data, int $level, int $encoding): string +function gzdeflate(string $data, int $level = -1): string ``` -Lowers `gzdeflate(data, level?)` through inline raw-DEFLATE zlib calls. +Deflate a string using the DEFLATE data format. **Parameters**: - `$data` (`string`) -- `$level` (`int`), optional -- `$encoding` (`int`) +- `$level` (`int`), default `-1`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/gzinflate.md b/docs/php/builtins/string/gzinflate.md index 45a5023334..693151fef7 100644 --- a/docs/php/builtins/string/gzinflate.md +++ b/docs/php/builtins/string/gzinflate.md @@ -1,23 +1,23 @@ --- title: "gzinflate()" -description: "Lowers `gzinflate(data, max_length?)` and boxes zlib failures as PHP false." +description: "Inflate a deflated string." sidebar: - order: 328 + order: 346 --- ## gzinflate() ```php -function gzinflate(string $data, int $max_length): string +function gzinflate(string $data, int $max_length = 0): mixed ``` -Lowers `gzinflate(data, max_length?)` and boxes zlib failures as PHP false. +Inflate a deflated string. **Parameters**: - `$data` (`string`) -- `$max_length` (`int`), optional +- `$max_length` (`int`), default `0`, optional -**Returns**: `string` +**Returns**: `mixed` _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 4f93607318..794d1535c1 100644 --- a/docs/php/builtins/string/gzuncompress.md +++ b/docs/php/builtins/string/gzuncompress.md @@ -1,23 +1,23 @@ --- title: "gzuncompress()" -description: "Lowers `gzuncompress(data, max_length?)` and boxes zlib failures as PHP false." +description: "Uncompress a compressed string." sidebar: - order: 329 + order: 347 --- ## gzuncompress() ```php -function gzuncompress(string $data, int $max_length): string +function gzuncompress(string $data, int $max_length = 0): mixed ``` -Lowers `gzuncompress(data, max_length?)` and boxes zlib failures as PHP false. +Uncompress a compressed string. **Parameters**: - `$data` (`string`) -- `$max_length` (`int`), optional +- `$max_length` (`int`), default `0`, optional -**Returns**: `string` +**Returns**: `mixed` _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 83bae1664a..4cd47569f3 100644 --- a/docs/php/builtins/string/hash.md +++ b/docs/php/builtins/string/hash.md @@ -1,23 +1,22 @@ --- title: "hash()" -description: "Lowers `hash(algo, data, binary?)` through the shared runtime digest dispatcher." +description: "Generates a hash value using the given algorithm." sidebar: - order: 330 + order: 348 --- ## hash() ```php -function hash(string $algo, string $data, bool $binary = false, array $options = []): string +function hash(string $algo, string $data, bool $binary = false): string ``` -Lowers `hash(algo, data, binary?)` through the shared runtime digest dispatcher. +Generates a hash value using the given algorithm. **Parameters**: - `$algo` (`string`) - `$data` (`string`) - `$binary` (`bool`), default `false`, optional -- `$options` (`array`), default `[]`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/hash_algos.md b/docs/php/builtins/string/hash_algos.md index 0f171d9e98..efeeae8015 100644 --- a/docs/php/builtins/string/hash_algos.md +++ b/docs/php/builtins/string/hash_algos.md @@ -1,8 +1,8 @@ --- title: "hash_algos()" -description: "Lowers `hash_algos()` through the runtime algorithm-list builder." +description: "Returns an array of supported hashing algorithm names." sidebar: - order: 331 + order: 349 --- ## hash_algos() @@ -11,7 +11,7 @@ sidebar: function hash_algos(): array ``` -Lowers `hash_algos()` through the runtime algorithm-list builder. +Returns an array of supported hashing algorithm names. **Parameters**: none. diff --git a/docs/php/builtins/string/hash_copy.md b/docs/php/builtins/string/hash_copy.md index 45c969c6d6..b998ac4f0c 100644 --- a/docs/php/builtins/string/hash_copy.md +++ b/docs/php/builtins/string/hash_copy.md @@ -1,8 +1,8 @@ --- title: "hash_copy()" -description: "Lowers `hash_copy(context)` through the incremental hash clone helper." +description: "Copies the state of an incremental hashing context." sidebar: - order: 332 + order: 350 --- ## hash_copy() @@ -11,7 +11,7 @@ sidebar: function hash_copy(resource $context): mixed ``` -Lowers `hash_copy(context)` through the incremental hash clone helper. +Copies the state of an incremental hashing context. **Parameters**: - `$context` (`resource`) diff --git a/docs/php/builtins/string/hash_equals.md b/docs/php/builtins/string/hash_equals.md index d2612215f9..369d4118ad 100644 --- a/docs/php/builtins/string/hash_equals.md +++ b/docs/php/builtins/string/hash_equals.md @@ -1,8 +1,8 @@ --- title: "hash_equals()" -description: "Lowers `hash_equals(known, user)` through the timing-safe runtime compare helper." +description: "Compares two strings using a constant-time algorithm." sidebar: - order: 333 + order: 351 --- ## hash_equals() @@ -11,7 +11,7 @@ sidebar: function hash_equals(string $known_string, string $user_string): bool ``` -Lowers `hash_equals(known, user)` through the timing-safe runtime compare helper. +Compares two strings using a constant-time algorithm. **Parameters**: - `$known_string` (`string`) diff --git a/docs/php/builtins/string/hash_final.md b/docs/php/builtins/string/hash_final.md index bdb2bee3d4..a1a62e766b 100644 --- a/docs/php/builtins/string/hash_final.md +++ b/docs/php/builtins/string/hash_final.md @@ -1,21 +1,21 @@ --- title: "hash_final()" -description: "Lowers `hash_final(context, binary?)` through the incremental hash finalizer." +description: "Finalizes an incremental hash and returns the digest string." sidebar: - order: 334 + order: 352 --- ## hash_final() ```php -function hash_final(resource $context, bool $binary): string +function hash_final(resource $context, bool $binary = false): string ``` -Lowers `hash_final(context, binary?)` through the incremental hash finalizer. +Finalizes an incremental hash and returns the digest string. **Parameters**: - `$context` (`resource`) -- `$binary` (`bool`), optional +- `$binary` (`bool`), default `false`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/hash_hmac.md b/docs/php/builtins/string/hash_hmac.md index 4f2593023b..33302cdb4a 100644 --- a/docs/php/builtins/string/hash_hmac.md +++ b/docs/php/builtins/string/hash_hmac.md @@ -1,23 +1,23 @@ --- title: "hash_hmac()" -description: "Lowers `hash_hmac(algo, data, key, binary?)` through the shared HMAC runtime dispatcher." +description: "Generates a keyed hash value using the HMAC method." sidebar: - order: 335 + order: 353 --- ## hash_hmac() ```php -function hash_hmac(string $algo, string $data, string $key, bool $binary): string +function hash_hmac(string $algo, string $data, string $key, bool $binary = false): string ``` -Lowers `hash_hmac(algo, data, key, binary?)` through the shared HMAC runtime dispatcher. +Generates a keyed hash value using the HMAC method. **Parameters**: - `$algo` (`string`) - `$data` (`string`) - `$key` (`string`) -- `$binary` (`bool`), optional +- `$binary` (`bool`), default `false`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/hash_init.md b/docs/php/builtins/string/hash_init.md index 4bfb9b6c9b..43d3203fd0 100644 --- a/docs/php/builtins/string/hash_init.md +++ b/docs/php/builtins/string/hash_init.md @@ -1,23 +1,22 @@ --- title: "hash_init()" -description: "Lowers `hash_init(algo)` and returns a boxed HashContext resource." +description: "Initialize an incremental hashing context." sidebar: - order: 336 + order: 354 --- ## hash_init() ```php -function hash_init(string $algo, int $flags = 0, string $key = '', array $options = []): mixed +function hash_init(string $algo, int $flags = 0, string $key = ''): mixed ``` -Lowers `hash_init(algo)` and returns a boxed HashContext resource. +Initialize an incremental hashing context. **Parameters**: - `$algo` (`string`) - `$flags` (`int`), default `0`, optional - `$key` (`string`), default `''`, optional -- `$options` (`array`), default `[]`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/string/hash_update.md b/docs/php/builtins/string/hash_update.md index 0ada2580f2..ebfdb6678c 100644 --- a/docs/php/builtins/string/hash_update.md +++ b/docs/php/builtins/string/hash_update.md @@ -1,8 +1,8 @@ --- title: "hash_update()" -description: "Lowers `hash_update(context, data)` through the incremental hash runtime helper." +description: "Pumps data into an active incremental hashing context." sidebar: - order: 337 + order: 355 --- ## hash_update() @@ -11,7 +11,7 @@ sidebar: function hash_update(resource $context, string $data): bool ``` -Lowers `hash_update(context, data)` through the incremental hash runtime helper. +Pumps data into an active incremental hashing context. **Parameters**: - `$context` (`resource`) diff --git a/docs/php/builtins/string/hex2bin.md b/docs/php/builtins/string/hex2bin.md index 7e476789c4..9014f9f4dc 100644 --- a/docs/php/builtins/string/hex2bin.md +++ b/docs/php/builtins/string/hex2bin.md @@ -1,8 +1,8 @@ --- title: "hex2bin()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Decodes a hexadecimal string back into its binary representation." sidebar: - order: 338 + order: 356 --- ## hex2bin() @@ -11,7 +11,7 @@ sidebar: function hex2bin(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Decodes a hexadecimal string back into its binary representation. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/html_entity_decode.md b/docs/php/builtins/string/html_entity_decode.md index 0bd75c38cb..73b4e900d3 100644 --- a/docs/php/builtins/string/html_entity_decode.md +++ b/docs/php/builtins/string/html_entity_decode.md @@ -1,22 +1,20 @@ --- title: "html_entity_decode()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Converts HTML entities in a string back into their corresponding characters." sidebar: - order: 339 + order: 357 --- ## html_entity_decode() ```php -function html_entity_decode(string $string, int $flags, string $encoding): string +function html_entity_decode(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Converts HTML entities in a string back into their corresponding characters. **Parameters**: - `$string` (`string`) -- `$flags` (`int`) -- `$encoding` (`string`) **Returns**: `string` diff --git a/docs/php/builtins/string/htmlentities.md b/docs/php/builtins/string/htmlentities.md index 2996bc5b18..27ea5de4a9 100644 --- a/docs/php/builtins/string/htmlentities.md +++ b/docs/php/builtins/string/htmlentities.md @@ -1,23 +1,20 @@ --- title: "htmlentities()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Converts all applicable characters in a string into their HTML entities." sidebar: - order: 340 + order: 358 --- ## htmlentities() ```php -function htmlentities(string $string, int $flags, string $encoding, bool $double_encode): string +function htmlentities(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Converts all applicable characters in a string into their HTML entities. **Parameters**: - `$string` (`string`) -- `$flags` (`int`) -- `$encoding` (`string`) -- `$double_encode` (`bool`) **Returns**: `string` diff --git a/docs/php/builtins/string/htmlspecialchars.md b/docs/php/builtins/string/htmlspecialchars.md index 2cadff365e..2d79e70a84 100644 --- a/docs/php/builtins/string/htmlspecialchars.md +++ b/docs/php/builtins/string/htmlspecialchars.md @@ -1,23 +1,20 @@ --- title: "htmlspecialchars()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Converts the HTML special characters in a string into their entities." sidebar: - order: 341 + order: 359 --- ## htmlspecialchars() ```php -function htmlspecialchars(string $string, int $flags, string $encoding, bool $double_encode): string +function htmlspecialchars(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Converts the HTML special characters in a string into their entities. **Parameters**: - `$string` (`string`) -- `$flags` (`int`) -- `$encoding` (`string`) -- `$double_encode` (`bool`) **Returns**: `string` diff --git a/docs/php/builtins/string/implode.md b/docs/php/builtins/string/implode.md index 47f792991b..c7f672b688 100644 --- a/docs/php/builtins/string/implode.md +++ b/docs/php/builtins/string/implode.md @@ -1,21 +1,21 @@ --- title: "implode()" -description: "Lowers `implode(glue, array)` by selecting the string or integer array helper." +description: "Joins array elements into a single string using a separator." sidebar: - order: 342 + order: 360 --- ## implode() ```php -function implode(string $separator, array $array): string +function implode(string $separator, array $array = null): string ``` -Lowers `implode(glue, array)` by selecting the string or integer array helper. +Joins array elements into a single string using a separator. **Parameters**: - `$separator` (`string`) -- `$array` (`array`), optional +- `$array` (`array`), default `null`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/inet_ntop.md b/docs/php/builtins/string/inet_ntop.md index ac4e5e4adf..04a2dd3401 100644 --- a/docs/php/builtins/string/inet_ntop.md +++ b/docs/php/builtins/string/inet_ntop.md @@ -1,8 +1,8 @@ --- title: "inet_ntop()" -description: "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." +description: "Converts a packed internet address to a human-readable representation." sidebar: - order: 343 + order: 361 --- ## inet_ntop() @@ -11,7 +11,7 @@ sidebar: function inet_ntop(string $ip): mixed ``` -Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false. +Converts a packed internet address to a human-readable representation. **Parameters**: - `$ip` (`string`) diff --git a/docs/php/builtins/string/inet_pton.md b/docs/php/builtins/string/inet_pton.md index 042789730c..805889b4a2 100644 --- a/docs/php/builtins/string/inet_pton.md +++ b/docs/php/builtins/string/inet_pton.md @@ -1,8 +1,8 @@ --- title: "inet_pton()" -description: "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." +description: "Converts a human-readable IP address to its packed in_addr representation." sidebar: - order: 344 + order: 362 --- ## inet_pton() @@ -11,7 +11,7 @@ sidebar: function inet_pton(string $ip): mixed ``` -Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false. +Converts a human-readable IP address to its packed in_addr representation. **Parameters**: - `$ip` (`string`) diff --git a/docs/php/builtins/string/ip2long.md b/docs/php/builtins/string/ip2long.md index d8afe6ab5c..472ce6aa06 100644 --- a/docs/php/builtins/string/ip2long.md +++ b/docs/php/builtins/string/ip2long.md @@ -1,8 +1,8 @@ --- title: "ip2long()" -description: "Lowers `ip2long(string)` and boxes invalid-address results as PHP false." +description: "Converts a string containing an IPv4 address into a long integer." sidebar: - order: 345 + order: 363 --- ## ip2long() @@ -11,7 +11,7 @@ sidebar: function ip2long(string $ip): mixed ``` -Lowers `ip2long(string)` and boxes invalid-address results as PHP false. +Converts a string containing an IPv4 address into a long integer. **Parameters**: - `$ip` (`string`) diff --git a/docs/php/builtins/string/lcfirst.md b/docs/php/builtins/string/lcfirst.md index 0a5660e02e..59a435a662 100644 --- a/docs/php/builtins/string/lcfirst.md +++ b/docs/php/builtins/string/lcfirst.md @@ -1,8 +1,8 @@ --- title: "lcfirst()" -description: "Lowers `lcfirst()` by copying the string and lowercasing the first ASCII byte." +description: "Lowercases the first character of a string." sidebar: - order: 346 + order: 364 --- ## lcfirst() @@ -11,7 +11,7 @@ sidebar: function lcfirst(string $string): string ``` -Lowers `lcfirst()` by copying the string and lowercasing the first ASCII byte. +Lowercases the first character of a string. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/long2ip.md b/docs/php/builtins/string/long2ip.md index 9b7e998ad9..7650ec0318 100644 --- a/docs/php/builtins/string/long2ip.md +++ b/docs/php/builtins/string/long2ip.md @@ -1,8 +1,8 @@ --- title: "long2ip()" -description: "Lowers `long2ip(value)` through the IPv4 formatting runtime helper." +description: "Converts an IPv4 address from long integer to dotted string notation." sidebar: - order: 347 + order: 365 --- ## long2ip() @@ -11,7 +11,7 @@ sidebar: function long2ip(int $ip): string ``` -Lowers `long2ip(value)` through the IPv4 formatting runtime helper. +Converts an IPv4 address from long integer to dotted string notation. **Parameters**: - `$ip` (`int`) diff --git a/docs/php/builtins/string/ltrim.md b/docs/php/builtins/string/ltrim.md index d1a2575129..59d998f293 100644 --- a/docs/php/builtins/string/ltrim.md +++ b/docs/php/builtins/string/ltrim.md @@ -1,21 +1,21 @@ --- title: "ltrim()" -description: "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." +description: "Strips whitespace (or other characters) from the beginning of a string." sidebar: - order: 348 + order: 366 --- ## ltrim() ```php -function ltrim(string $string, string $characters): string +function ltrim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string ``` -Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks. +Strips whitespace (or other characters) from the beginning of a string. **Parameters**: - `$string` (`string`) -- `$characters` (`string`), optional +- `$characters` (`string`), default `' \n\r\t\x0b\x0c\x00'`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/md5.md b/docs/php/builtins/string/md5.md index 59a0f54e7a..4ce3cc3ddf 100644 --- a/docs/php/builtins/string/md5.md +++ b/docs/php/builtins/string/md5.md @@ -1,21 +1,21 @@ --- title: "md5()" -description: "Lowers `md5(data, binary?)` through the shared crypto-backed runtime helper." +description: "Calculates the MD5 hash of a string." sidebar: - order: 349 + order: 367 --- ## md5() ```php -function md5(string $string, bool $binary): string +function md5(string $string, bool $binary = false): string ``` -Lowers `md5(data, binary?)` through the shared crypto-backed runtime helper. +Calculates the MD5 hash of a string. **Parameters**: - `$string` (`string`) -- `$binary` (`bool`), optional +- `$binary` (`bool`), default `false`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/nl2br.md b/docs/php/builtins/string/nl2br.md index 6ca426af56..2bbcb0c902 100644 --- a/docs/php/builtins/string/nl2br.md +++ b/docs/php/builtins/string/nl2br.md @@ -1,21 +1,20 @@ --- title: "nl2br()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Inserts HTML line breaks before newlines in a string." sidebar: - order: 350 + order: 368 --- ## nl2br() ```php -function nl2br(string $string, bool $use_xhtml): string +function nl2br(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Inserts HTML line breaks before newlines in a string. **Parameters**: - `$string` (`string`) -- `$use_xhtml` (`bool`) **Returns**: `string` diff --git a/docs/php/builtins/string/number_format.md b/docs/php/builtins/string/number_format.md index 612140ce08..d5bf58cdf7 100644 --- a/docs/php/builtins/string/number_format.md +++ b/docs/php/builtins/string/number_format.md @@ -1,23 +1,23 @@ --- title: "number_format()" -description: "Lowers `number_format()` by arranging its runtime helper arguments." +description: "Formats a number with grouped thousands." sidebar: - order: 351 + order: 369 --- ## number_format() ```php -function number_format(float $num, int $decimals, string $decimal_separator, string $thousands_separator): string +function number_format(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string ``` -Lowers `number_format()` by arranging its runtime helper arguments. +Formats a number with grouped thousands. **Parameters**: - `$num` (`float`) -- `$decimals` (`int`), optional -- `$decimal_separator` (`string`), optional -- `$thousands_separator` (`string`), optional +- `$decimals` (`int`), default `0`, optional +- `$decimal_separator` (`string`), default `'.'`, optional +- `$thousands_separator` (`string`), default `','`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/ord.md b/docs/php/builtins/string/ord.md index 4144807112..6b5322b846 100644 --- a/docs/php/builtins/string/ord.md +++ b/docs/php/builtins/string/ord.md @@ -1,8 +1,8 @@ --- title: "ord()" -description: "Lowers `ord()` by returning the first byte of a string or zero for empty input." +description: "Returns the ASCII value of the first character of a string." sidebar: - order: 352 + order: 370 --- ## ord() @@ -11,7 +11,7 @@ sidebar: function ord(string $character): int ``` -Lowers `ord()` by returning the first byte of a string or zero for empty input. +Returns the ASCII value of the first character of a string. **Parameters**: - `$character` (`string`) diff --git a/docs/php/builtins/string/printf.md b/docs/php/builtins/string/printf.md index 019abf015a..edf4bd4eb1 100644 --- a/docs/php/builtins/string/printf.md +++ b/docs/php/builtins/string/printf.md @@ -1,8 +1,8 @@ --- title: "printf()" -description: "Lowers `printf(format, values...)` as `sprintf()` followed by stdout emission." +description: "Outputs a formatted string." sidebar: - order: 353 + order: 371 --- ## printf() @@ -11,7 +11,7 @@ sidebar: function printf(string $format, ...$values): int ``` -Lowers `printf(format, values...)` as `sprintf()` followed by stdout emission. +Outputs a formatted string. **Parameters**: - `$format` (`string`) diff --git a/docs/php/builtins/string/rawurldecode.md b/docs/php/builtins/string/rawurldecode.md index 38b48a96ca..3cd1fafc2d 100644 --- a/docs/php/builtins/string/rawurldecode.md +++ b/docs/php/builtins/string/rawurldecode.md @@ -1,8 +1,8 @@ --- title: "rawurldecode()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Decodes an RFC 3986 percent-encoded string without treating '+' as a space." sidebar: - order: 354 + order: 372 --- ## rawurldecode() @@ -11,7 +11,7 @@ sidebar: function rawurldecode(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Decodes an RFC 3986 percent-encoded string without treating '+' as a space. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/rawurlencode.md b/docs/php/builtins/string/rawurlencode.md index 6a56cc3e8a..cca0db1b5b 100644 --- a/docs/php/builtins/string/rawurlencode.md +++ b/docs/php/builtins/string/rawurlencode.md @@ -1,8 +1,8 @@ --- title: "rawurlencode()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces)." sidebar: - order: 355 + order: 373 --- ## rawurlencode() @@ -11,7 +11,7 @@ sidebar: function rawurlencode(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces). **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/rtrim.md b/docs/php/builtins/string/rtrim.md index 851b9b8cd8..cfea4e4644 100644 --- a/docs/php/builtins/string/rtrim.md +++ b/docs/php/builtins/string/rtrim.md @@ -1,21 +1,21 @@ --- title: "rtrim()" -description: "rtrim() — string builtin supported by Elephc." +description: "Strips whitespace (or other characters) from the end of a string." sidebar: - order: 356 + order: 374 --- ## rtrim() ```php -function rtrim(string $string, string $characters): string +function rtrim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string ``` -`rtrim()` is a string builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Strips whitespace (or other characters) from the end of a string. **Parameters**: - `$string` (`string`) -- `$characters` (`string`), optional +- `$characters` (`string`), default `' \n\r\t\x0b\x0c\x00'`, optional **Returns**: `string` @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `rtrim` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/rtrim.md). + diff --git a/docs/php/builtins/string/sha1.md b/docs/php/builtins/string/sha1.md index 0b9699436c..c35a89513a 100644 --- a/docs/php/builtins/string/sha1.md +++ b/docs/php/builtins/string/sha1.md @@ -1,21 +1,21 @@ --- title: "sha1()" -description: "Lowers `sha1(data, binary?)` through the shared crypto-backed runtime helper." +description: "Calculates the SHA-1 hash of a string." sidebar: - order: 357 + order: 375 --- ## sha1() ```php -function sha1(string $string, bool $binary): string +function sha1(string $string, bool $binary = false): string ``` -Lowers `sha1(data, binary?)` through the shared crypto-backed runtime helper. +Calculates the SHA-1 hash of a string. **Parameters**: - `$string` (`string`) -- `$binary` (`bool`), optional +- `$binary` (`bool`), default `false`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/sprintf.md b/docs/php/builtins/string/sprintf.md index a5bef57c82..4e15f37a7d 100644 --- a/docs/php/builtins/string/sprintf.md +++ b/docs/php/builtins/string/sprintf.md @@ -1,8 +1,8 @@ --- title: "sprintf()" -description: "Lowers `sprintf(format, values...)` by packing variadic records for `__rt_sprintf`." +description: "Returns a formatted string." sidebar: - order: 358 + order: 376 --- ## sprintf() @@ -11,7 +11,7 @@ sidebar: function sprintf(string $format, ...$values): string ``` -Lowers `sprintf(format, values...)` by packing variadic records for `__rt_sprintf`. +Returns a formatted string. **Parameters**: - `$format` (`string`) diff --git a/docs/php/builtins/string/sscanf.md b/docs/php/builtins/string/sscanf.md index 01384673bc..e7e85c2df9 100644 --- a/docs/php/builtins/string/sscanf.md +++ b/docs/php/builtins/string/sscanf.md @@ -1,8 +1,8 @@ --- title: "sscanf()" -description: "Lowers `sscanf(string, format)` into the shared scanner helper." +description: "Parses a string according to a format." sidebar: - order: 359 + order: 377 --- ## sscanf() @@ -11,7 +11,7 @@ sidebar: function sscanf(string $string, string $format, ...$vars): array ``` -Lowers `sscanf(string, format)` into the shared scanner helper. +Parses a string according to a format. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/str_contains.md b/docs/php/builtins/string/str_contains.md index 5156daab8a..62b03be0bf 100644 --- a/docs/php/builtins/string/str_contains.md +++ b/docs/php/builtins/string/str_contains.md @@ -1,8 +1,8 @@ --- title: "str_contains()" -description: "Lowers `str_contains()` through `strpos()` and converts found positions to bool." +description: "Determines if a string contains a given substring." sidebar: - order: 360 + order: 378 --- ## str_contains() @@ -11,7 +11,7 @@ sidebar: function str_contains(string $haystack, string $needle): bool ``` -Lowers `str_contains()` through `strpos()` and converts found positions to bool. +Determines if a string contains a given substring. **Parameters**: - `$haystack` (`string`) diff --git a/docs/php/builtins/string/str_ends_with.md b/docs/php/builtins/string/str_ends_with.md index a9cad96688..f61c89befc 100644 --- a/docs/php/builtins/string/str_ends_with.md +++ b/docs/php/builtins/string/str_ends_with.md @@ -1,8 +1,8 @@ --- title: "str_ends_with()" -description: "Lowers a two-argument string builtin that directly delegates to a runtime helper." +description: "Checks if a string ends with a given substring." sidebar: - order: 361 + order: 379 --- ## str_ends_with() @@ -11,7 +11,7 @@ sidebar: function str_ends_with(string $haystack, string $needle): bool ``` -Lowers a two-argument string builtin that directly delegates to a runtime helper. +Checks if a string ends with a given substring. **Parameters**: - `$haystack` (`string`) diff --git a/docs/php/builtins/string/str_ireplace.md b/docs/php/builtins/string/str_ireplace.md index 0fd77754f6..74155ff2a0 100644 --- a/docs/php/builtins/string/str_ireplace.md +++ b/docs/php/builtins/string/str_ireplace.md @@ -1,25 +1,25 @@ --- title: "str_ireplace()" -description: "str_ireplace() — string builtin supported by Elephc." +description: "Case-insensitive version of str_replace()." sidebar: - order: 362 + order: 380 --- ## str_ireplace() ```php -function str_ireplace(mixed $search, mixed $replace, mixed $subject, int $count): mixed +function str_ireplace(string $search, string $replace, string $subject, int $count = null): string ``` -`str_ireplace()` is a string builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Case-insensitive version of str_replace(). **Parameters**: -- `$search` (`mixed`) -- `$replace` (`mixed`) -- `$subject` (`mixed`) -- `$count` (`int`), passed by reference, optional +- `$search` (`string`) +- `$replace` (`string`) +- `$subject` (`string`) +- `$count` (`int`), default `null`, optional -**Returns**: `mixed` +**Returns**: `string` _No examples yet — check `examples/` and `showcases/` for usage patterns._ @@ -29,3 +29,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `str_ireplace` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/str_ireplace.md). + diff --git a/docs/php/builtins/string/str_pad.md b/docs/php/builtins/string/str_pad.md index 88d433b44c..c045d64553 100644 --- a/docs/php/builtins/string/str_pad.md +++ b/docs/php/builtins/string/str_pad.md @@ -1,23 +1,23 @@ --- title: "str_pad()" -description: "Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper." +description: "Pads a string to a certain length with another string." sidebar: - order: 363 + order: 381 --- ## str_pad() ```php -function str_pad(string $string, int $length, string $pad_string, int $pad_type): string +function str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string ``` -Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper. +Pads a string to a certain length with another string. **Parameters**: - `$string` (`string`) - `$length` (`int`) -- `$pad_string` (`string`), optional -- `$pad_type` (`int`), optional +- `$pad_string` (`string`), default `' '`, optional +- `$pad_type` (`int`), default `1`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/str_repeat.md b/docs/php/builtins/string/str_repeat.md index ca2d1755b6..deed273045 100644 --- a/docs/php/builtins/string/str_repeat.md +++ b/docs/php/builtins/string/str_repeat.md @@ -1,8 +1,8 @@ --- title: "str_repeat()" -description: "Lowers `str_repeat(string, times)` through the shared runtime helper." +description: "Repeats a string a given number of times." sidebar: - order: 364 + order: 382 --- ## str_repeat() @@ -11,7 +11,7 @@ sidebar: function str_repeat(string $string, int $times): string ``` -Lowers `str_repeat(string, times)` through the shared runtime helper. +Repeats a string a given number of times. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/str_replace.md b/docs/php/builtins/string/str_replace.md index 563d0bf09d..a99ccd9442 100644 --- a/docs/php/builtins/string/str_replace.md +++ b/docs/php/builtins/string/str_replace.md @@ -1,25 +1,25 @@ --- title: "str_replace()" -description: "Lowers `str_replace()`/`str_ireplace()` with three string operands." +description: "Replaces all occurrences of a search string with a replacement string." sidebar: - order: 365 + order: 383 --- ## str_replace() ```php -function str_replace(string $search, string $replace, string $subject, int $count): mixed +function str_replace(string $search, string $replace, string $subject, int $count = null): string ``` -Lowers `str_replace()`/`str_ireplace()` with three string operands. +Replaces all occurrences of a search string with a replacement string. **Parameters**: - `$search` (`string`) - `$replace` (`string`) - `$subject` (`string`) -- `$count` (`int`), passed by reference, optional +- `$count` (`int`), default `null`, optional -**Returns**: `mixed` +**Returns**: `string` _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 0b9a625bde..dd04dddc38 100644 --- a/docs/php/builtins/string/str_split.md +++ b/docs/php/builtins/string/str_split.md @@ -1,21 +1,21 @@ --- title: "str_split()" -description: "Lowers `str_split(string, length?)` into the fixed-width string-array splitter." +description: "Converts a string into an array of chunks of the given length." sidebar: - order: 366 + order: 384 --- ## str_split() ```php -function str_split(string $string, int $length): array +function str_split(string $string, int $length = 1): array ``` -Lowers `str_split(string, length?)` into the fixed-width string-array splitter. +Converts a string into an array of chunks of the given length. **Parameters**: - `$string` (`string`) -- `$length` (`int`), optional +- `$length` (`int`), default `1`, optional **Returns**: `array` diff --git a/docs/php/builtins/string/str_starts_with.md b/docs/php/builtins/string/str_starts_with.md index 19cde9337a..46734f6bb1 100644 --- a/docs/php/builtins/string/str_starts_with.md +++ b/docs/php/builtins/string/str_starts_with.md @@ -1,8 +1,8 @@ --- title: "str_starts_with()" -description: "Lowers a two-argument string builtin that directly delegates to a runtime helper." +description: "Checks if a string starts with a given substring." sidebar: - order: 367 + order: 385 --- ## str_starts_with() @@ -11,7 +11,7 @@ sidebar: function str_starts_with(string $haystack, string $needle): bool ``` -Lowers a two-argument string builtin that directly delegates to a runtime helper. +Checks if a string starts with a given substring. **Parameters**: - `$haystack` (`string`) diff --git a/docs/php/builtins/string/strcasecmp.md b/docs/php/builtins/string/strcasecmp.md index b19ef09403..91fa638ab0 100644 --- a/docs/php/builtins/string/strcasecmp.md +++ b/docs/php/builtins/string/strcasecmp.md @@ -1,8 +1,8 @@ --- title: "strcasecmp()" -description: "strcasecmp() — string builtin supported by Elephc." +description: "Binary safe case-insensitive string comparison. Returns negative, zero, or positive." sidebar: - order: 368 + order: 386 --- ## strcasecmp() @@ -11,7 +11,7 @@ sidebar: function strcasecmp(string $string1, string $string2): int ``` -`strcasecmp()` is a string builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Binary safe case-insensitive string comparison. Returns negative, zero, or positive. **Parameters**: - `$string1` (`string`) @@ -27,3 +27,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `strcasecmp` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/strcasecmp.md). + diff --git a/docs/php/builtins/string/strcmp.md b/docs/php/builtins/string/strcmp.md index e946fa74f2..7b969d9a02 100644 --- a/docs/php/builtins/string/strcmp.md +++ b/docs/php/builtins/string/strcmp.md @@ -1,8 +1,8 @@ --- title: "strcmp()" -description: "Lowers a two-argument string builtin that directly delegates to a runtime helper." +description: "Binary safe string comparison. Returns negative, zero, or positive." sidebar: - order: 369 + order: 387 --- ## strcmp() @@ -11,7 +11,7 @@ sidebar: function strcmp(string $string1, string $string2): int ``` -Lowers a two-argument string builtin that directly delegates to a runtime helper. +Binary safe string comparison. Returns negative, zero, or positive. **Parameters**: - `$string1` (`string`) diff --git a/docs/php/builtins/string/stripslashes.md b/docs/php/builtins/string/stripslashes.md index c56f3555ba..2b7107d02f 100644 --- a/docs/php/builtins/string/stripslashes.md +++ b/docs/php/builtins/string/stripslashes.md @@ -1,8 +1,8 @@ --- title: "stripslashes()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Removes backslashes from a string previously escaped by addslashes." sidebar: - order: 370 + order: 388 --- ## stripslashes() @@ -11,7 +11,7 @@ sidebar: function stripslashes(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Removes backslashes from a string previously escaped by addslashes. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/strlen.md b/docs/php/builtins/string/strlen.md index 556d62beff..b62478578c 100644 --- a/docs/php/builtins/string/strlen.md +++ b/docs/php/builtins/string/strlen.md @@ -1,8 +1,8 @@ --- title: "strlen()" -description: "Lowers `strlen()` by coercing string-like values and returning the byte length." +description: "Returns the length of a string." sidebar: - order: 371 + order: 389 --- ## strlen() @@ -11,7 +11,7 @@ sidebar: function strlen(string $string): int ``` -Lowers `strlen()` by coercing string-like values and returning the byte length. +Returns the length of a string. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/strpos.md b/docs/php/builtins/string/strpos.md index e3f726bd34..bfda184641 100644 --- a/docs/php/builtins/string/strpos.md +++ b/docs/php/builtins/string/strpos.md @@ -1,22 +1,22 @@ --- title: "strpos()" -description: "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." +description: "Finds the numeric position of the first occurrence of a substring." sidebar: - order: 372 + order: 390 --- ## strpos() ```php -function strpos(string $haystack, string $needle, int $offset): mixed +function strpos(string $haystack, string $needle, int $offset = 0): mixed ``` -Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed. +Finds the numeric position of the first occurrence of a substring. **Parameters**: - `$haystack` (`string`) - `$needle` (`string`) -- `$offset` (`int`), optional +- `$offset` (`int`), default `0`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/string/strrev.md b/docs/php/builtins/string/strrev.md index a3590c906a..c4339ed1bb 100644 --- a/docs/php/builtins/string/strrev.md +++ b/docs/php/builtins/string/strrev.md @@ -1,8 +1,8 @@ --- title: "strrev()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Reverses a string." sidebar: - order: 373 + order: 391 --- ## strrev() @@ -11,7 +11,7 @@ sidebar: function strrev(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Reverses a string. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/strrpos.md b/docs/php/builtins/string/strrpos.md index dc682f0886..a588437387 100644 --- a/docs/php/builtins/string/strrpos.md +++ b/docs/php/builtins/string/strrpos.md @@ -1,22 +1,22 @@ --- title: "strrpos()" -description: "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." +description: "Finds the numeric position of the last occurrence of a substring." sidebar: - order: 374 + order: 392 --- ## strrpos() ```php -function strrpos(string $haystack, string $needle, int $offset): mixed +function strrpos(string $haystack, string $needle, int $offset = 0): mixed ``` -Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed. +Finds the numeric position of the last occurrence of a substring. **Parameters**: - `$haystack` (`string`) - `$needle` (`string`) -- `$offset` (`int`), optional +- `$offset` (`int`), default `0`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/string/strstr.md b/docs/php/builtins/string/strstr.md index 39017fdffd..62477b8350 100644 --- a/docs/php/builtins/string/strstr.md +++ b/docs/php/builtins/string/strstr.md @@ -1,22 +1,22 @@ --- title: "strstr()" -description: "Lowers `strstr(haystack, needle)` by searching and returning the matching suffix." +description: "Returns the portion of a string starting at the first occurrence of a substring." sidebar: - order: 375 + order: 393 --- ## strstr() ```php -function strstr(string $haystack, string $needle, bool $before_needle): string +function strstr(string $haystack, string $needle, bool $before_needle = false): string ``` -Lowers `strstr(haystack, needle)` by searching and returning the matching suffix. +Returns the portion of a string starting at the first occurrence of a substring. **Parameters**: - `$haystack` (`string`) - `$needle` (`string`) -- `$before_needle` (`bool`), optional +- `$before_needle` (`bool`), default `false`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/strtolower.md b/docs/php/builtins/string/strtolower.md index b0b55b121e..407f9a563f 100644 --- a/docs/php/builtins/string/strtolower.md +++ b/docs/php/builtins/string/strtolower.md @@ -1,8 +1,8 @@ --- title: "strtolower()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Converts a string to lowercase." sidebar: - order: 376 + order: 394 --- ## strtolower() @@ -11,7 +11,7 @@ sidebar: function strtolower(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Converts a string to lowercase. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/strtoupper.md b/docs/php/builtins/string/strtoupper.md index 6a69f98519..14afa212b9 100644 --- a/docs/php/builtins/string/strtoupper.md +++ b/docs/php/builtins/string/strtoupper.md @@ -1,8 +1,8 @@ --- title: "strtoupper()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Converts a string to uppercase." sidebar: - order: 377 + order: 395 --- ## strtoupper() @@ -11,7 +11,7 @@ sidebar: function strtoupper(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Converts a string to uppercase. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/substr.md b/docs/php/builtins/string/substr.md index 93e6cacf98..4297ed688d 100644 --- a/docs/php/builtins/string/substr.md +++ b/docs/php/builtins/string/substr.md @@ -1,22 +1,22 @@ --- title: "substr()" -description: "Lowers `substr(string, offset, length?)` with target-local pointer arithmetic." +description: "Returns a portion of a string specified by the offset and length." sidebar: - order: 378 + order: 396 --- ## substr() ```php -function substr(string $string, int $offset, int $length): string +function substr(string $string, int $offset, int $length = null): string ``` -Lowers `substr(string, offset, length?)` with target-local pointer arithmetic. +Returns a portion of a string specified by the offset and length. **Parameters**: - `$string` (`string`) - `$offset` (`int`) -- `$length` (`int`), optional +- `$length` (`int`), default `null`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/substr_replace.md b/docs/php/builtins/string/substr_replace.md index 6a27e672b5..2653b8715c 100644 --- a/docs/php/builtins/string/substr_replace.md +++ b/docs/php/builtins/string/substr_replace.md @@ -1,23 +1,23 @@ --- title: "substr_replace()" -description: "Lowers `substr_replace(string, replacement, start, length?)`." +description: "Replaces text within a portion of a string." sidebar: - order: 379 + order: 397 --- ## substr_replace() ```php -function substr_replace(string $string, string $replace, int $offset, int $length): string +function substr_replace(string $string, string $replace, int $offset, int $length = null): string ``` -Lowers `substr_replace(string, replacement, start, length?)`. +Replaces text within a portion of a string. **Parameters**: - `$string` (`string`) - `$replace` (`string`) - `$offset` (`int`) -- `$length` (`int`), optional +- `$length` (`int`), default `null`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/trim.md b/docs/php/builtins/string/trim.md index 83b9f69c5f..2ce8500ac5 100644 --- a/docs/php/builtins/string/trim.md +++ b/docs/php/builtins/string/trim.md @@ -1,21 +1,21 @@ --- title: "trim()" -description: "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." +description: "Strips whitespace (or other characters) from the beginning and end of a string." sidebar: - order: 380 + order: 398 --- ## trim() ```php -function trim(string $string, string $characters): string +function trim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string ``` -Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks. +Strips whitespace (or other characters) from the beginning and end of a string. **Parameters**: - `$string` (`string`) -- `$characters` (`string`), optional +- `$characters` (`string`), default `' \n\r\t\x0b\x0c\x00'`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/ucfirst.md b/docs/php/builtins/string/ucfirst.md index be43ff7917..3f4d065622 100644 --- a/docs/php/builtins/string/ucfirst.md +++ b/docs/php/builtins/string/ucfirst.md @@ -1,8 +1,8 @@ --- title: "ucfirst()" -description: "Lowers `ucfirst()` by copying the string and uppercasing the first ASCII byte." +description: "Uppercases the first character of a string." sidebar: - order: 381 + order: 399 --- ## ucfirst() @@ -11,7 +11,7 @@ sidebar: function ucfirst(string $string): string ``` -Lowers `ucfirst()` by copying the string and uppercasing the first ASCII byte. +Uppercases the first character of a string. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/ucwords.md b/docs/php/builtins/string/ucwords.md index 7bf077fb30..fc680a333b 100644 --- a/docs/php/builtins/string/ucwords.md +++ b/docs/php/builtins/string/ucwords.md @@ -1,21 +1,21 @@ --- title: "ucwords()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Uppercases the first character of each word in a string." sidebar: - order: 382 + order: 400 --- ## ucwords() ```php -function ucwords(string $string, string $separators): string +function ucwords(string $string, string $separators = ' \t\r\n\x0c\x0b'): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Uppercases the first character of each word in a string. **Parameters**: - `$string` (`string`) -- `$separators` (`string`), optional +- `$separators` (`string`), default `' \t\r\n\x0c\x0b'`, optional **Returns**: `string` diff --git a/docs/php/builtins/string/urldecode.md b/docs/php/builtins/string/urldecode.md index b8e0f01a04..fa67b82a7b 100644 --- a/docs/php/builtins/string/urldecode.md +++ b/docs/php/builtins/string/urldecode.md @@ -1,8 +1,8 @@ --- title: "urldecode()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "Decodes a URL-encoded string, including '+' as a space." sidebar: - order: 383 + order: 401 --- ## urldecode() @@ -11,7 +11,7 @@ sidebar: function urldecode(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +Decodes a URL-encoded string, including '+' as a space. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/urlencode.md b/docs/php/builtins/string/urlencode.md index 3661efa1ae..2b4f8f3268 100644 --- a/docs/php/builtins/string/urlencode.md +++ b/docs/php/builtins/string/urlencode.md @@ -1,8 +1,8 @@ --- title: "urlencode()" -description: "Lowers a one-argument string builtin that directly delegates to a runtime helper." +description: "URL-encodes a string using application/x-www-form-urlencoded rules." sidebar: - order: 384 + order: 402 --- ## urlencode() @@ -11,7 +11,7 @@ sidebar: function urlencode(string $string): string ``` -Lowers a one-argument string builtin that directly delegates to a runtime helper. +URL-encodes a string using application/x-www-form-urlencoded rules. **Parameters**: - `$string` (`string`) diff --git a/docs/php/builtins/string/vprintf.md b/docs/php/builtins/string/vprintf.md index faae9df645..1971d3f33b 100644 --- a/docs/php/builtins/string/vprintf.md +++ b/docs/php/builtins/string/vprintf.md @@ -1,8 +1,8 @@ --- title: "vprintf()" -description: "Lowers `vprintf(format, values)` as `vsprintf()` followed by stdout emission." +description: "Outputs a formatted string using an array of values." sidebar: - order: 385 + order: 403 --- ## vprintf() @@ -11,7 +11,7 @@ sidebar: function vprintf(string $format, array $values): int ``` -Lowers `vprintf(format, values)` as `vsprintf()` followed by stdout emission. +Outputs a formatted string using an array of values. **Parameters**: - `$format` (`string`) diff --git a/docs/php/builtins/string/vsprintf.md b/docs/php/builtins/string/vsprintf.md index 1a394a5ebd..a4d642052c 100644 --- a/docs/php/builtins/string/vsprintf.md +++ b/docs/php/builtins/string/vsprintf.md @@ -1,8 +1,8 @@ --- title: "vsprintf()" -description: "Lowers `vsprintf(format, values)` through the array-to-sprintf runtime bridge." +description: "Returns a formatted string using an array of values." sidebar: - order: 386 + order: 404 --- ## vsprintf() @@ -11,7 +11,7 @@ sidebar: function vsprintf(string $format, array $values): string ``` -Lowers `vsprintf(format, values)` through the array-to-sprintf runtime bridge. +Returns a formatted string using an array of values. **Parameters**: - `$format` (`string`) diff --git a/docs/php/builtins/string/wordwrap.md b/docs/php/builtins/string/wordwrap.md index 276f0a1616..2189e74a36 100644 --- a/docs/php/builtins/string/wordwrap.md +++ b/docs/php/builtins/string/wordwrap.md @@ -1,23 +1,23 @@ --- title: "wordwrap()" -description: "Lowers `wordwrap(string, width?, break?, cut?)` through the shared runtime helper." +description: "Wraps a string to a given number of characters." sidebar: - order: 387 + order: 405 --- ## wordwrap() ```php -function wordwrap(string $string, int $width, string $break, bool $cut_long_words): string +function wordwrap(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string ``` -Lowers `wordwrap(string, width?, break?, cut?)` through the shared runtime helper. +Wraps a string to a given number of characters. **Parameters**: - `$string` (`string`) -- `$width` (`int`), optional -- `$break` (`string`), optional -- `$cut_long_words` (`bool`), optional +- `$width` (`int`), default `75`, optional +- `$break` (`string`), default `'\n'`, optional +- `$cut_long_words` (`bool`), default `false`, optional **Returns**: `string` diff --git a/docs/php/builtins/type.md b/docs/php/builtins/type.md index 9eea9cf168..75386c75ec 100644 --- a/docs/php/builtins/type.md +++ b/docs/php/builtins/type.md @@ -18,10 +18,10 @@ sidebar: | [`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 $base): int` | `int` | +| [`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 $syntax_only = false, string $callable_name = null): 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` | diff --git a/docs/php/builtins/type/boolval.md b/docs/php/builtins/type/boolval.md index e854d8b571..a8d071bc1c 100644 --- a/docs/php/builtins/type/boolval.md +++ b/docs/php/builtins/type/boolval.md @@ -1,8 +1,8 @@ --- title: "boolval()" -description: "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`." +description: "Returns the boolean value of a variable." sidebar: - order: 388 + order: 406 --- ## boolval() @@ -11,7 +11,7 @@ sidebar: function boolval(mixed $value): bool ``` -Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`. +Returns the boolean value of a variable. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/ctype_alnum.md b/docs/php/builtins/type/ctype_alnum.md index 4c51c2922e..e051fd33a5 100644 --- a/docs/php/builtins/type/ctype_alnum.md +++ b/docs/php/builtins/type/ctype_alnum.md @@ -1,8 +1,8 @@ --- title: "ctype_alnum()" -description: "Lowers `ctype_alnum(string)` by checking every byte against ASCII alpha or digit ranges." +description: "Checks if all characters in the string are alphanumeric." sidebar: - order: 389 + order: 407 --- ## ctype_alnum() @@ -11,7 +11,7 @@ sidebar: function ctype_alnum(string $text): bool ``` -Lowers `ctype_alnum(string)` by checking every byte against ASCII alpha or digit ranges. +Checks if all characters in the string are alphanumeric. **Parameters**: - `$text` (`string`) diff --git a/docs/php/builtins/type/ctype_alpha.md b/docs/php/builtins/type/ctype_alpha.md index 38781f5145..ff8ecaabea 100644 --- a/docs/php/builtins/type/ctype_alpha.md +++ b/docs/php/builtins/type/ctype_alpha.md @@ -1,8 +1,8 @@ --- title: "ctype_alpha()" -description: "Lowers `ctype_alpha(string)` by checking every byte against ASCII alpha ranges." +description: "Checks if all characters in the string are alphabetic." sidebar: - order: 390 + order: 408 --- ## ctype_alpha() @@ -11,7 +11,7 @@ sidebar: function ctype_alpha(string $text): bool ``` -Lowers `ctype_alpha(string)` by checking every byte against ASCII alpha ranges. +Checks if all characters in the string are alphabetic. **Parameters**: - `$text` (`string`) diff --git a/docs/php/builtins/type/ctype_digit.md b/docs/php/builtins/type/ctype_digit.md index ef231ecff6..d80449f902 100644 --- a/docs/php/builtins/type/ctype_digit.md +++ b/docs/php/builtins/type/ctype_digit.md @@ -1,8 +1,8 @@ --- title: "ctype_digit()" -description: "Lowers `ctype_digit(string)` by checking every byte against the ASCII digit range." +description: "Checks if all characters in the string are digits." sidebar: - order: 391 + order: 409 --- ## ctype_digit() @@ -11,7 +11,7 @@ sidebar: function ctype_digit(string $text): bool ``` -Lowers `ctype_digit(string)` by checking every byte against the ASCII digit range. +Checks if all characters in the string are digits. **Parameters**: - `$text` (`string`) diff --git a/docs/php/builtins/type/ctype_space.md b/docs/php/builtins/type/ctype_space.md index 17fae70cfe..2f08643b6c 100644 --- a/docs/php/builtins/type/ctype_space.md +++ b/docs/php/builtins/type/ctype_space.md @@ -1,8 +1,8 @@ --- title: "ctype_space()" -description: "Lowers `ctype_space(string)` by checking every byte against PHP's ASCII whitespace set." +description: "Checks if all characters in the string are whitespace characters." sidebar: - order: 392 + order: 410 --- ## ctype_space() @@ -11,7 +11,7 @@ sidebar: function ctype_space(string $text): bool ``` -Lowers `ctype_space(string)` by checking every byte against PHP's ASCII whitespace set. +Checks if all characters in the string are whitespace characters. **Parameters**: - `$text` (`string`) diff --git a/docs/php/builtins/type/floatval.md b/docs/php/builtins/type/floatval.md index 8d7b7537e5..2a747a687e 100644 --- a/docs/php/builtins/type/floatval.md +++ b/docs/php/builtins/type/floatval.md @@ -1,8 +1,8 @@ --- title: "floatval()" -description: "Lowers `floatval()` for concrete scalar operands." +description: "Returns the float value of a variable." sidebar: - order: 393 + order: 411 --- ## floatval() @@ -11,7 +11,7 @@ sidebar: function floatval(mixed $value): float ``` -Lowers `floatval()` for concrete scalar operands. +Returns the float value of a variable. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/get_resource_id.md b/docs/php/builtins/type/get_resource_id.md index a00a692499..e0d7e455df 100644 --- a/docs/php/builtins/type/get_resource_id.md +++ b/docs/php/builtins/type/get_resource_id.md @@ -1,8 +1,8 @@ --- title: "get_resource_id()" -description: "Lowers `get_resource_id(resource)` by unboxing the native handle and making it one-based." +description: "Returns an integer identifier for the given resource." sidebar: - order: 394 + order: 412 --- ## get_resource_id() @@ -11,7 +11,7 @@ sidebar: function get_resource_id(resource $resource): int ``` -Lowers `get_resource_id(resource)` by unboxing the native handle and making it one-based. +Returns an integer identifier for the given resource. **Parameters**: - `$resource` (`resource`) diff --git a/docs/php/builtins/type/get_resource_type.md b/docs/php/builtins/type/get_resource_type.md index a4bdcab397..58f692113c 100644 --- a/docs/php/builtins/type/get_resource_type.md +++ b/docs/php/builtins/type/get_resource_type.md @@ -1,8 +1,8 @@ --- title: "get_resource_type()" -description: "Lowers `get_resource_type(resource)` to elephc's current resource type label." +description: "Returns the type of a resource." sidebar: - order: 395 + order: 413 --- ## get_resource_type() @@ -11,7 +11,7 @@ sidebar: function get_resource_type(resource $resource): string ``` -Lowers `get_resource_type(resource)` to elephc's current resource type label. +Returns the type of a resource. **Parameters**: - `$resource` (`resource`) diff --git a/docs/php/builtins/type/gettype.md b/docs/php/builtins/type/gettype.md index 35844528bb..99b61a5330 100644 --- a/docs/php/builtins/type/gettype.md +++ b/docs/php/builtins/type/gettype.md @@ -1,8 +1,8 @@ --- title: "gettype()" -description: "Lowers `gettype(value)` for statically concrete PHP types." +description: "Returns the type of a variable as a string." sidebar: - order: 396 + order: 414 --- ## gettype() @@ -11,7 +11,7 @@ sidebar: function gettype(mixed $value): string ``` -Lowers `gettype(value)` for statically concrete PHP types. +Returns the type of a variable as a string. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/intval.md b/docs/php/builtins/type/intval.md index fd99a94fea..73882f61b4 100644 --- a/docs/php/builtins/type/intval.md +++ b/docs/php/builtins/type/intval.md @@ -1,21 +1,20 @@ --- title: "intval()" -description: "Lowers `intval()` for concrete scalar operands." +description: "Returns the integer value of a variable." sidebar: - order: 397 + order: 415 --- ## intval() ```php -function intval(mixed $value, int $base): int +function intval(mixed $value): int ``` -Lowers `intval()` for concrete scalar operands. +Returns the integer value of a variable. **Parameters**: - `$value` (`mixed`) -- `$base` (`int`) **Returns**: `int` diff --git a/docs/php/builtins/type/is_array.md b/docs/php/builtins/type/is_array.md index f5814388de..fd5ce4590f 100644 --- a/docs/php/builtins/type/is_array.md +++ b/docs/php/builtins/type/is_array.md @@ -1,8 +1,8 @@ --- title: "is_array()" -description: "Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value" +description: "Checks whether a variable is an array." sidebar: - order: 398 + order: 416 --- ## is_array() @@ -11,7 +11,7 @@ sidebar: function is_array(mixed $value): bool ``` -Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value +Checks whether a variable is an array. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/is_bool.md b/docs/php/builtins/type/is_bool.md index c075304721..04f70bbb33 100644 --- a/docs/php/builtins/type/is_bool.md +++ b/docs/php/builtins/type/is_bool.md @@ -1,8 +1,8 @@ --- title: "is_bool()" -description: "is_bool() — type builtin supported by Elephc." +description: "Checks whether a variable is a boolean." sidebar: - order: 399 + order: 417 --- ## is_bool() @@ -11,7 +11,7 @@ sidebar: function is_bool(mixed $value): bool ``` -`is_bool()` is a type builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks whether a variable is a boolean. **Parameters**: - `$value` (`mixed`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `is_bool` is implemented in the compiler, see [the internals page](../../../internals/builtins/type/is_bool.md). + diff --git a/docs/php/builtins/type/is_callable.md b/docs/php/builtins/type/is_callable.md index 0e9c05cb6d..a39b02bf2a 100644 --- a/docs/php/builtins/type/is_callable.md +++ b/docs/php/builtins/type/is_callable.md @@ -1,22 +1,20 @@ --- title: "is_callable()" -description: "Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers." +description: "Checks whether a variable can be called as a function." sidebar: - order: 400 + order: 418 --- ## is_callable() ```php -function is_callable(mixed $value, bool $syntax_only = false, string $callable_name = null): bool +function is_callable(mixed $value): bool ``` -Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers. +Checks whether a variable can be called as a function. **Parameters**: - `$value` (`mixed`) -- `$syntax_only` (`bool`), default `false`, optional -- `$callable_name` (`string`), passed by reference, default `null`, optional **Returns**: `bool` diff --git a/docs/php/builtins/type/is_float.md b/docs/php/builtins/type/is_float.md index 13a6d50b0d..844baa0e05 100644 --- a/docs/php/builtins/type/is_float.md +++ b/docs/php/builtins/type/is_float.md @@ -1,8 +1,8 @@ --- title: "is_float()" -description: "is_float() — type builtin supported by Elephc." +description: "Checks whether a variable is a floating-point number." sidebar: - order: 401 + order: 419 --- ## is_float() @@ -11,7 +11,7 @@ sidebar: function is_float(mixed $value): bool ``` -`is_float()` is a type builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks whether a variable is a floating-point number. **Parameters**: - `$value` (`mixed`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `is_float` is implemented in the compiler, see [the internals page](../../../internals/builtins/type/is_float.md). + diff --git a/docs/php/builtins/type/is_int.md b/docs/php/builtins/type/is_int.md index 081c0cf152..12b0081047 100644 --- a/docs/php/builtins/type/is_int.md +++ b/docs/php/builtins/type/is_int.md @@ -1,8 +1,8 @@ --- title: "is_int()" -description: "is_int() — type builtin supported by Elephc." +description: "Checks whether a variable is an integer." sidebar: - order: 402 + order: 420 --- ## is_int() @@ -11,7 +11,7 @@ sidebar: function is_int(mixed $value): bool ``` -`is_int()` is a type builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks whether a variable is an integer. **Parameters**: - `$value` (`mixed`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `is_int` is implemented in the compiler, see [the internals page](../../../internals/builtins/type/is_int.md). + diff --git a/docs/php/builtins/type/is_iterable.md b/docs/php/builtins/type/is_iterable.md index 30b5a086e3..176e0c1470 100644 --- a/docs/php/builtins/type/is_iterable.md +++ b/docs/php/builtins/type/is_iterable.md @@ -1,8 +1,8 @@ --- title: "is_iterable()" -description: "Lowers `is_iterable()` for concrete values and boxed Mixed payloads." +description: "Checks whether a variable is iterable." sidebar: - order: 403 + order: 421 --- ## is_iterable() @@ -11,7 +11,7 @@ sidebar: function is_iterable(mixed $value): bool ``` -Lowers `is_iterable()` for concrete values and boxed Mixed payloads. +Checks whether a variable is iterable. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/is_null.md b/docs/php/builtins/type/is_null.md index 667787976a..673e9c4c4c 100644 --- a/docs/php/builtins/type/is_null.md +++ b/docs/php/builtins/type/is_null.md @@ -1,8 +1,8 @@ --- title: "is_null()" -description: "is_null() — type builtin supported by Elephc." +description: "Checks whether a variable is null." sidebar: - order: 404 + order: 422 --- ## is_null() @@ -11,7 +11,7 @@ sidebar: function is_null(mixed $value): bool ``` -`is_null()` is a type builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks whether a variable is null. **Parameters**: - `$value` (`mixed`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `is_null` is implemented in the compiler, see [the internals page](../../../internals/builtins/type/is_null.md). + diff --git a/docs/php/builtins/type/is_numeric.md b/docs/php/builtins/type/is_numeric.md index 9f9c9efd16..312e1f6fe4 100644 --- a/docs/php/builtins/type/is_numeric.md +++ b/docs/php/builtins/type/is_numeric.md @@ -1,8 +1,8 @@ --- title: "is_numeric()" -description: "Lowers `is_numeric()` for concrete scalar values." +description: "Checks whether a variable is a number or a numeric string." sidebar: - order: 405 + order: 423 --- ## is_numeric() @@ -11,7 +11,7 @@ sidebar: function is_numeric(mixed $value): bool ``` -Lowers `is_numeric()` for concrete scalar values. +Checks whether a variable is a number or a numeric string. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/is_object.md b/docs/php/builtins/type/is_object.md index acf0ef3316..cf63b4b8fd 100644 --- a/docs/php/builtins/type/is_object.md +++ b/docs/php/builtins/type/is_object.md @@ -1,8 +1,8 @@ --- title: "is_object()" -description: "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose" +description: "Checks whether a variable is an object." sidebar: - order: 406 + order: 424 --- ## is_object() @@ -11,7 +11,7 @@ sidebar: function is_object(mixed $value): bool ``` -Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose +Checks whether a variable is an object. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/is_resource.md b/docs/php/builtins/type/is_resource.md index 6d7a8e8019..62e0b8f618 100644 --- a/docs/php/builtins/type/is_resource.md +++ b/docs/php/builtins/type/is_resource.md @@ -1,8 +1,8 @@ --- title: "is_resource()" -description: "Lowers `is_resource(value)` for static resources and boxed Mixed resource cells." +description: "Checks whether a variable is a resource." sidebar: - order: 407 + order: 425 --- ## is_resource() @@ -11,7 +11,7 @@ sidebar: function is_resource(mixed $value): bool ``` -Lowers `is_resource(value)` for static resources and boxed Mixed resource cells. +Checks whether a variable is a resource. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/is_scalar.md b/docs/php/builtins/type/is_scalar.md index 496308d455..f348e8026a 100644 --- a/docs/php/builtins/type/is_scalar.md +++ b/docs/php/builtins/type/is_scalar.md @@ -1,8 +1,8 @@ --- title: "is_scalar()" -description: "Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed" +description: "Checks whether a variable is a scalar." sidebar: - order: 408 + order: 426 --- ## is_scalar() @@ -11,7 +11,7 @@ sidebar: function is_scalar(mixed $value): bool ``` -Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed +Checks whether a variable is a scalar. **Parameters**: - `$value` (`mixed`) diff --git a/docs/php/builtins/type/is_string.md b/docs/php/builtins/type/is_string.md index 44183c2dad..fe632c3b83 100644 --- a/docs/php/builtins/type/is_string.md +++ b/docs/php/builtins/type/is_string.md @@ -1,8 +1,8 @@ --- title: "is_string()" -description: "is_string() — type builtin supported by Elephc." +description: "Checks whether a variable is a string." sidebar: - order: 409 + order: 427 --- ## is_string() @@ -11,7 +11,7 @@ sidebar: function is_string(mixed $value): bool ``` -`is_string()` is a type builtin supported by Elephc. Behavior matches the PHP manual unless noted below. +Checks whether a variable is a string. **Parameters**: - `$value` (`mixed`) @@ -26,3 +26,7 @@ _No examples yet — check `examples/` and `showcases/` for usage patterns._ +## Internals + +For how `is_string` is implemented in the compiler, see [the internals page](../../../internals/builtins/type/is_string.md). + diff --git a/docs/php/builtins/type/settype.md b/docs/php/builtins/type/settype.md index e69829366b..60cad609de 100644 --- a/docs/php/builtins/type/settype.md +++ b/docs/php/builtins/type/settype.md @@ -1,8 +1,8 @@ --- title: "settype()" -description: "Lowers `settype($local, \"type\")` by mutating the resolved local slot and returning true." +description: "Sets the type of a variable." sidebar: - order: 410 + order: 428 --- ## settype() @@ -11,7 +11,7 @@ sidebar: function settype(mixed $var, string $type): bool ``` -Lowers `settype($local, "type")` by mutating the resolved local slot and returning true. +Sets the type of a variable. **Parameters**: - `$var` (`mixed`), passed by reference diff --git a/docs/php/classes.md b/docs/php/classes.md index 3c9f07affa..0eaaa42a18 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -94,8 +94,7 @@ Classes implementing `ArrayAccess` can use PHP subscript syntax: `unset($obj[$key])` dispatches to `offsetUnset()`. `Serializable` is intentionally not provided: it is deprecated since -PHP 8.1. Use `__serialize` / `__unserialize` magic methods instead -(when those land). +PHP 8.1. Use the `__serialize` / `__unserialize` magic methods instead. ### Built-in SPL containers and storage iterators @@ -220,6 +219,8 @@ final class InvoiceNumber { - Static properties with `public static`, `protected static`, or `private static`, including typed static properties - `readonly class` makes all instance properties readonly; static properties stay mutable +Statically-known access violations — calling a `private`/`protected` method from an inaccessible scope, or writing a `readonly` property outside its declaring constructor — raise a catchable `Error` exception at runtime, matching PHP. Without a `try`/`catch` handler the exception is a fatal uncaught exit. + ```php name` property (the case identifier); backed cases also expose `->value`. Plus `::from()`, `::tryFrom()`, `::cases()`. Only `int` and `string` backing types. +Like PHP, an `int`-backed enum's `::from()` / `::tryFrom()` accept a numeric string and coerce it to the integer backing value (`Color::from("2")` returns `Color::Green`). A numeric string with no matching case throws `ValueError`; a non-numeric string (e.g. `"x"`, `"1abc"`, `"0x1"`, `"INF"`, or `"NAN"`) throws `TypeError`, matching PHP's coercive typing. + +A dynamically-typed (`mixed`) argument — such as a `foreach` value or an untyped parameter — is also accepted and coerced on its runtime type: an integer or numeric string resolves (or throws `ValueError`), a float truncates, a bool/null coerces, and an array/object/resource/closure throws `TypeError` naming the given type (objects report `object` rather than the class name). + ### Enum methods, constants, and interfaces Enums may declare instance methods, static methods, constants, and an `implements` clause. Instance methods dispatch on the case singleton, so `$this` is the case: diff --git a/docs/php/streams.md b/docs/php/streams.md index 980dbc0e56..260b899f26 100644 --- a/docs/php/streams.md +++ b/docs/php/streams.md @@ -85,22 +85,66 @@ entry in a SHA1-signed PHAR archive while preserving existing entries. `file_put_contents()` and write-mode `fopen()` also accept runtime-built `phar://` URLs. Native PHAR, tar-based PHAR, and zip-based PHAR containers are writable; ZIP writes preserve stored/deflated entries and compression controls -can rewrite ZIP entries between stored and deflated forms. `Phar` and -`PharData` expose a +can rewrite ZIP entries between stored and deflated forms. ZIP entries written +with a streaming data descriptor are read transparently, and ZIP64 archives +(over 65535 entries, or sizes/offsets over 4 GiB) are both read and written. +Traditional-PKWARE (ZipCrypto) encrypted ZIP entries can be read and written +after calling the `setZipPassword(string $password)` compiler extension on a +`Phar`/`PharData` object: once a password is set, encrypted entries are decrypted +on read and zip entries are encrypted on write (the stub is encrypted too; the +`.phar/signature.bin` entry stays in the clear). ZipCrypto is cryptographically +weak — it is kept for compatibility with legacy archives, not as a real +confidentiality mechanism. `Phar` and `PharData` expose a baseline OOP surface with constructors, format/compression/signature constants, `addFromString()`, `delete()`, `compressFiles()`, `decompressFiles()`, mixed metadata/string stub accessors, path helpers, and ArrayAccess read/write/isset over the same `phar://` paths. ArrayAccess reads return `PharFileInfo` objects -with `getContent()` for payload reads. `foreach` over a `Phar` / `PharData` +with `getContent()` for payload reads and +`setMetadata()`/`getMetadata()`/`hasMetadata()`/`delMetadata()` for per-file +metadata. `foreach` over a `Phar` / `PharData` object visits entries scanned from the archive at construction time plus entries written through that object, yielding `entryName => PharFileInfo`. `unlink("phar://archive/entry")` and `unset($phar["entry"])` remove entries while preserving sibling entries. Native PHAR compression controls support `Phar::GZ`, `Phar::BZ2`, and `Phar::NONE`; ZIP compression controls support -`Phar::GZ` and `Phar::NONE`. Current limits: metadata values and stub strings -are stored on the archive object and are not serialized into the archive file; -tar compression controls and key/private-key signing variants are not -implemented. +`Phar::GZ` and `Phar::NONE`. + +`setMetadata()`/`getMetadata()`/`hasMetadata()`/`delMetadata()` and +`setStub()`/`getStub()` **persist into the archive file** for all three families, +so the global metadata and stub round-trip across fresh `Phar`/`PharData` objects +and across processes (and are interchangeable with the PHP interpreter). Metadata +is stored PHP-`serialize()`d — in the manifest metadata field for native PHAR, in a +`.phar/.metadata.bin` entry for tar, and in the ZIP archive comment for zip; the stub +is stored as the byte prefix for native PHAR and as a `.phar/stub.php` entry for +tar/zip. `setStub()` requires the stub to contain `__HALT_COMPILER();` (matching PHP). +The reserved `.phar/*` control entries are hidden from the entry listing and iteration. + +Per-file metadata persists the same way through the `PharFileInfo` returned by +ArrayAccess: `$phar["entry"]->setMetadata(...)`, `getMetadata()`, `hasMetadata()`, +and `delMetadata()` round-trip across fresh objects and the PHP interpreter. It is +stored in the per-entry manifest field for native PHAR, in a +`.phar/.metadata//.metadata.bin` side entry for tar, and in the per-entry ZIP +central-directory file comment for zip. + +Whole-archive compression is supported on tar-based `PharData`: `compress(Phar::GZ)` +and `compress(Phar::BZ2)` write a sibling `.tar.gz` / `.tar.bz2` and return a fresh +`PharData` for it, while `decompress()` writes the plain `.tar` back; the compressed +archives are read transparently (and are interchangeable with the PHP interpreter). +Per-entry compression for native PHAR / zip stays on `compressFiles()` / +`decompressFiles()`. + +Signatures are supported through `setSignatureAlgorithm()` / `getSignature()` across +native PHAR, tar, and zip phars. `setSignatureAlgorithm(Phar::MD5|Phar::SHA1|Phar::SHA256|Phar::SHA512)` +applies a hash signature, and `setSignatureAlgorithm(Phar::OPENSSL, $privateKey)` signs +with RSA-SHA1 using a PEM private key (PKCS#1 or PKCS#8). Native PHARs store the signature +in their trailer; tar and zip phars store it in a `.phar/signature.bin` control entry. The +resulting signature is verifiable by the PHP interpreter (for OpenSSL, place the matching +public key in `.pubkey`). `getSignature()` returns `['hash' => , +'hash_type' => 'MD5'|'SHA-1'|'SHA-256'|'SHA-512'|'OpenSSL']`. + +Metadata persistence covers the same scalar+array subset as +[`serialize()`/`unserialize()`](system-and-io.md#serialization); object metadata is not +serialized. `file_get_contents($url)` recognizes runtime `http://`, `https://`, `ftp://`, and `ftps://` strings before falling back to `phar://`/filesystem handling. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 9b98b9cdfa..c8c6cb1e04 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -182,6 +182,47 @@ Encoding rules for objects: - Floats encode at PHP's `serialize_precision = -1` — the shortest decimal that round-trips back to the same `double` (so `json_encode(1.0/3.0)` is `0.3333333333333333`, not the 14-digit `echo`/`(string)` form, and `json_encode(0.1 + 0.2)` is `0.30000000000000004`). The JSON number layout differs from `var_export`: integer-valued floats drop the fraction (`json_encode(100.0)` is `100`, not `100.0`) unless `JSON_PRESERVE_ZERO_FRACTION` is set, and exponential magnitudes use a lowercase `e` with a `d.d` mantissa and a no-leading-zero exponent (`1.0e+17`, `1.0e-6`). The decimal/exponential boundary matches PHP (`zend_gcvt`): exponential when `decpt < -3` or `decpt > 17`. The dedicated `__rt_json_ftoa` runtime helper finds the shortest precision by probing `snprintf("%.*e", p, x)` against a `strtod` re-parse, independent of the default `precision` used elsewhere. - JSON helpers are emitted through the shared runtime surface on every supported target. Structural decode into `Mixed`, stdClass dynamic-property helpers, JsonSerializable-aware object encoding, validation, pretty-printing, depth tracking, and JSON error-message lookup are all part of that target-aware runtime path. +## Serialization + +| Function | Signature | Notes | +|---|---|---| +| `serialize()` | `serialize($value): string` | Produces PHP's `serialize()` wire format, byte-for-byte: `N;` (null), `b:0;`/`b:1;` (bool), `i:;` (int), `d:;` (float, shortest round-trip at `serialize_precision = -1`, with `INF`/`-INF`/`NAN` for non-finite values), `s::"";` (string, raw bytes with the exact byte length and no escaping), `a::{...}` (indexed and associative arrays, nested, with int keys as `i:K;` and string keys as `s:N:"...";`, in insertion order), and `O::""::{...}` (objects). | +| `unserialize()` | `unserialize($data, $options = []): mixed` | Parses the `serialize()` wire format back into a boxed `Mixed` value. Scalars, arrays, and objects round-trip exactly. Malformed or unsupported input returns `false`, matching PHP's failure indicator. The `$options` argument is accepted for signature compatibility and currently ignored. | + +`serialize()`/`unserialize()` round-trip the scalar, array, and object subset exactly, +and the produced bytes are interchangeable with the PHP interpreter. They share the same +runtime walker family as `json_encode`/`json_decode` and reuse the shortest-float +formatter, so float output matches `json_encode`'s precision. + +### Objects + +Objects serialize as `O::""::{...}` with PHP's exact property-key +mangling: public properties use the bare name, protected use `\0*\0name`, and private +use `\0Class\0name`. Properties are emitted in declaration order (inherited first). + +Serialization magic methods are honoured: + +- **`__serialize(): array`** — when defined, the object body is the returned array's + `key;value;` pairs instead of the raw properties. +- **`__unserialize(array $data): void`** — when defined, the parsed body is passed to it + to restore the object (instead of injecting properties by name). Its `$data` parameter + is treated as a string/int-keyed array so `$data['key']` works (a bare `array` hint + otherwise resolves to an integer-indexed array). +- **`__sleep(): array`** — serializes only the named properties, in `__sleep()`'s order, + using their mangled keys. +- **`__wakeup(): void`** — runs after properties are injected (when `__unserialize()` is + not defined). + +Repeated objects within a single `serialize()` call are emitted as `r:;` +back-references (PHP's global value counter: every value consumes the next index, array +keys do not), and `unserialize()` rebuilds them as a single shared instance so `===` +identity is preserved. This is the same machinery used to persist `Phar` global metadata +(see [Streams](streams.md)). + +**Limitations:** a cyclic reference *inside an object's own properties* resolves to +`null` on `unserialize()` (serialization itself handles cycles correctly), and the +deprecated `Serializable` interface (`C:` wire form) is not supported. + ## Regex Regex functions and SPL regex iterators are documented in [Regex](regex.md), diff --git a/docs/php/types.md b/docs/php/types.md index 3d79ab31b8..864c6765b8 100644 --- a/docs/php/types.md +++ b/docs/php/types.md @@ -233,14 +233,15 @@ Narrowing applies to function and method parameters. A parameter whose call site ### Known incompatibilities with PHP - `$argv[0]` returns the compiled binary path, not the `.php` file path. -- Integer `+`, `-`, and `*` overflow promotes to `float` only for **constant-folded** arithmetic (compile-time-constant operands), matching PHP. At **runtime**, `int op int` has the static type `int`, so an overflowing operation does **not** promote to `float`: the result is clamped toward the 64-bit integer boundary and `is_float()` stays `false`, whereas PHP returns a `float`. Promoting at runtime would require boxing every arithmetic result, which elephc's unboxed scalar representation avoids. For the same reason, `intval()`/`(int)` of an integer-valued string near the 64-bit boundary (e.g. `intval("9223372036854775807")`) is lossy. +- Integer `+`, `-`, and `*` overflow promotes to `float` for both constant-folded and runtime arithmetic, matching PHP on 64-bit builds. `intval()`/`(int)` of an integer-valued string near the 64-bit boundary (e.g. `intval("9223372036854775807")`) is still lossy at the string-conversion boundary. - Converting an array to a string (via `.` concatenation, `echo`, or string interpolation) yields the literal `"Array"`, matching PHP's value, but elephc does not emit PHP's `E_WARNING` "Array to string conversion". - Scalar loose comparison (`==`, `!=`) follows PHP-style bool truthiness, null-vs-empty-string, numeric-string, non-numeric string byte-comparison, and numeric `int`-vs-`float` rules for constant-folded literals and non-folded runtime scalar operands. One known gap: when an **untyped (`mixed`) operand holds a `float`** at runtime — e.g. `switch ($x)` over an untyped `$x = 1.5`, or `$x == 1` — the value is truncated to `int` before comparing, so `1.5` wrongly compares equal to `1`. Statically-typed `float` operands compare correctly; only untyped float-bearing values are affected. - `??=` is checked against typed assignment storage for variables, object properties, static properties, and non-append array elements. For concrete local variable types, the fallback must keep the same type or be a literal `null`. - 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 legacy `--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. +- 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. +- `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`. ### Filesystem functions not implemented diff --git a/examples/arithmetic/main.php b/examples/arithmetic/main.php index b5fc5a5af2..f8bfce211e 100644 --- a/examples/arithmetic/main.php +++ b/examples/arithmetic/main.php @@ -10,3 +10,6 @@ echo "b % a = " . ($b % $a) . "\n"; echo "2 + 3 * 4 = " . (2 + 3 * 4) . "\n"; echo "(2 + 3) * 4 = " . ((2 + 3) * 4) . "\n"; + +$overflow = PHP_INT_MAX + $argc; +echo "overflow type = " . gettype($overflow) . "\n"; diff --git a/examples/array-parity/.gitignore b/examples/array-parity/.gitignore new file mode 100644 index 0000000000..fc9576aeb9 --- /dev/null +++ b/examples/array-parity/.gitignore @@ -0,0 +1,3 @@ +*.s +*.o +main diff --git a/examples/array-parity/main.php b/examples/array-parity/main.php new file mode 100644 index 0000000000..9df2c643bd --- /dev/null +++ b/examples/array-parity/main.php @@ -0,0 +1,75 @@ + 1, "b" => 2, "c" => 3]; + +echo "is_list(list): " . (array_is_list($list) ? "true" : "false") . "\n"; +echo "is_list(hash): " . (array_is_list($hash) ? "true" : "false") . "\n"; +echo "first key: " . array_key_first($hash) . "\n"; +echo "last key: " . array_key_last($hash) . "\n"; + +// --- hash set operations (right-wins replace, recursive replace) --- +$base = ["host" => "localhost", "port" => 80]; +$over = ["port" => 443, "tls" => 1]; +$merged = array_replace($base, $over); +echo "\nreplace: port=" . $merged["port"] . " tls=" . $merged["tls"] . "\n"; + +$deepA = ["db" => ["host" => "a", "port" => 1]]; +$deepB = ["db" => ["port" => 2]]; +$deep = array_replace_recursive($deepA, $deepB); +echo "replace_rec: host=" . $deep["db"]["host"] . " port=" . $deep["db"]["port"] . "\n"; + +// --- associative diff / intersect (compare key AND value) --- +$left = ["a" => 1, "b" => 2, "c" => 3]; +$right = ["a" => 1, "b" => 99]; +echo "diff_assoc: "; +foreach (array_diff_assoc($left, $right) as $k => $v) { + echo "$k=$v "; +} +echo "\nintersect_assoc:"; +foreach (array_intersect_assoc($left, $right) as $k => $v) { + echo " $k=$v"; +} +echo "\n"; + +// --- recursive merge (scalar collisions combine into lists) --- +$mr = array_merge_recursive(["tag" => "a"], ["tag" => "b"]); +echo "merge_rec: tag has " . count($mr["tag"]) . " values\n"; + +// --- predicate helpers (PHP 8.4): find / any / all --- +function gtTwo($n) { return $n > 2; } +$nums = [1, 2, 3, 4]; +echo "\nfind > 2: " . array_find($nums, "gtTwo") . "\n"; +echo "any > 2: " . (array_any($nums, "gtTwo") ? "true" : "false") . "\n"; +echo "all > 2: " . (array_all($nums, fn($n) => $n > 0) ? "true" : "false") . "\n"; + +// --- user-comparator set operations --- +function cmp($a, $b) { return $a - $b; } +echo "udiff: "; +foreach (array_udiff([1, 2, 3, 4], [2, 4], "cmp") as $v) { + echo $v; +} +echo "\nuintersect: "; +foreach (array_uintersect([1, 2, 3, 4], [2, 4], "cmp") as $v) { + echo $v; +} +echo "\n"; + +// --- recursive walk over nested arrays --- +function visit($leaf) { echo $leaf; echo ","; } +$nested = [[1, 2], [3, 4], [5, 6]]; +echo "walk_recursive: "; +array_walk_recursive($nested, "visit"); +echo "\n"; + +// --- multisort: sort one array, reorder a parallel array in tandem --- +$keys = [3, 1, 2]; +$vals = [30, 10, 20]; +array_multisort($keys, $vals); +echo "multisort keys: "; +foreach ($keys as $v) { echo $v; } +echo "\nmultisort vals: "; +foreach ($vals as $v) { echo $v; } +echo "\n"; diff --git a/examples/catchable-access-errors/.gitignore b/examples/catchable-access-errors/.gitignore new file mode 100644 index 0000000000..2c5756d149 --- /dev/null +++ b/examples/catchable-access-errors/.gitignore @@ -0,0 +1,3 @@ +*.s +*.o +main \ No newline at end of file diff --git a/examples/catchable-access-errors/main.php b/examples/catchable-access-errors/main.php new file mode 100644 index 0000000000..b448ac234c --- /dev/null +++ b/examples/catchable-access-errors/main.php @@ -0,0 +1,51 @@ +value = $value; + } +} + +$vault = new Vault(); + +// Private method access from the global scope throws a catchable Error. +try { + echo $vault->secret(); + echo "no"; +} catch (Error $e) { + echo "private: " . $e->getMessage() . "\n"; +} + +// Protected method access from the global scope throws a catchable Error. +try { + echo $vault->guarded(); + echo "no"; +} catch (Error $e) { + echo "protected: " . $e->getMessage() . "\n"; +} + +// Readonly property write outside the constructor throws a catchable Error. +$container = new Container(1); +try { + $container->value = 99; + echo "no"; +} catch (Error $e) { + echo "readonly: " . $e->getMessage() . "\n"; +} + +echo "done\n"; \ No newline at end of file diff --git a/examples/hashing/main.php b/examples/hashing/main.php index bfe24dad09..9679ee647e 100644 --- a/examples/hashing/main.php +++ b/examples/hashing/main.php @@ -41,6 +41,18 @@ echo "branch A: " . hash_final($base) . "\n"; echo "branch B: " . hash_final($branch) . "\n"; +// A context that is never finalized is auto-freed at scope exit: the runtime +// calls elephc_crypto_free through __rt_mixed_free_deep (tag-9 kind-2) when +// the Mixed box's refcount reaches zero. No leak, no double-free. +function leak_ctx(): void { + $ctx = hash_init("sha256"); + hash_update($ctx, "abandoned"); + // No hash_final() — $ctx is auto-freed when the function returns. +} +leak_ctx(); +echo "\n--- auto-free scope cleanup ---\n"; +echo "unfinalized context freed without crash\n"; + echo "\n--- supported algorithms ---\n"; echo "count: " . count(hash_algos()) . "\n"; diff --git a/examples/phar-write/.gitignore b/examples/phar-write/.gitignore index 0d4bae516f..58e887e7ab 100644 --- a/examples/phar-write/.gitignore +++ b/examples/phar-write/.gitignore @@ -2,3 +2,7 @@ *.o main *.phar +*.tar +*.tar.gz +*.tar.bz2 +*.pubkey diff --git a/examples/phar-write/main.php b/examples/phar-write/main.php index 512de391f8..8279a73d43 100644 --- a/examples/phar-write/main.php +++ b/examples/phar-write/main.php @@ -62,3 +62,29 @@ echo "oop scanned count: " . $scan->count() . "\n"; $oop->delete("array-access.txt"); echo "oop delete removed array-access entry: " . (isset($oop["array-access.txt"]) ? "no\n" : "yes\n"); + +// Per-file metadata persists into the archive on each entry's PharFileInfo and +// round-trips across a fresh Phar object (and the PHP interpreter). +$oop["hello.txt"]->setMetadata(["author" => "elephc", "lines" => 1]); +$reopened = new Phar("oop.phar"); +$fileMeta = $reopened["hello.txt"]->getMetadata(); +echo "per-file metadata author: " . $fileMeta["author"] . "\n"; + +// A native PHAR can be re-signed; getSignature() reports the algorithm and digest. +$oop->setSignatureAlgorithm(Phar::SHA256); +$sig = $oop->getSignature(); +echo "signature type: " . $sig["hash_type"] . "\n"; + +// Tar and zip phars can be signed too: the signature lives in a .phar/signature.bin +// control entry (rather than a trailer), and is verifiable by the PHP interpreter. +$tar = new PharData("bundle.tar"); +$tar->addFromString("doc.txt", "bundled document\n"); +$tar->setSignatureAlgorithm(Phar::SHA1); +$tarSig = $tar->getSignature(); +echo "tar signature type: " . $tarSig["hash_type"] . "\n"; + +// PharData supports whole-archive compression: compress() writes a sibling +// .tar.gz and returns a fresh PharData that reads back transparently. +$gz = $tar->compress(Phar::GZ); +echo "compressed bundle entry count: " . $gz->count() . "\n"; +echo "compressed bundle reads back: " . $gz["doc.txt"]->getContent(); diff --git a/examples/serialize/.gitignore b/examples/serialize/.gitignore new file mode 100644 index 0000000000..0d4bae516f --- /dev/null +++ b/examples/serialize/.gitignore @@ -0,0 +1,4 @@ +*.s +*.o +main +*.phar diff --git a/examples/serialize/main.php b/examples/serialize/main.php new file mode 100644 index 0000000000..94ee000e23 --- /dev/null +++ b/examples/serialize/main.php @@ -0,0 +1,63 @@ + "Ada", "age" => 36]), "\n"; + +// unserialize() is the inverse — round-trips back to the original value. +$blob = serialize(["lang" => "PHP", "stars" => 5, "tags" => ["fast", "native"]]); +$restored = unserialize($blob); +echo $restored["lang"], " has ", $restored["stars"], " stars\n"; +echo "first tag: ", $restored["tags"][0], "\n"; + +// unserialize() returns false on malformed input. +var_dump(unserialize("not valid")); + +// Objects serialize as O::""::{...} with PHP's exact key +// mangling (public bare, protected \0*\0name, private \0Class\0name). +class Point { public int $x = 1; protected int $y = 2; private int $z = 3; } +echo serialize(new Point()), "\n"; + +// __serialize()/__unserialize() customise the wire form: the object body is the +// returned array, and __unserialize() restores it. +class Money { + public int $cents = 0; + public string $currency = "USD"; + public function __serialize(): array { + return ["cents" => $this->cents, "currency" => $this->currency]; + } + public function __unserialize(array $data): void { + $this->cents = (int) $data["cents"]; + $this->currency = (string) $data["currency"]; + } +} +$m = new Money(); +$m->cents = 1299; +$m->currency = "EUR"; +$back = unserialize(serialize($m)); +echo $back->cents, " ", $back->currency, "\n"; // 1299 EUR + +// Repeated objects become r:; back-references and rebuild as one shared +// instance, so identity (===) survives a serialize()/unserialize() round-trip. +$shared = new Point(); +$pair = unserialize(serialize([$shared, $shared])); +echo ($pair[0] === $pair[1] ? "same instance" : "two instances"), "\n"; + +// Phar stores its global metadata as a serialize()d blob, so metadata set on one +// Phar object is read back by another — and by the PHP interpreter. +$path = "build.phar"; +$phar = new Phar($path); +$phar->addFromString("app.php", "setMetadata(["version" => "1.0.0", "author" => "elephc"]); + +$reopened = new Phar($path); +$meta = $reopened->getMetadata(); +echo "phar version: ", $meta["version"], " by ", $meta["author"], "\n"; +echo "has metadata: ", $reopened->hasMetadata() ? "yes" : "no", "\n"; diff --git a/examples/variadic/main.php b/examples/variadic/main.php index e0bfec5720..722117833c 100644 --- a/examples/variadic/main.php +++ b/examples/variadic/main.php @@ -73,3 +73,12 @@ function labeled_pair($left, $right) { echo $v . " "; } echo "\n"; + +// Spread of associative arrays: string keys are preserved, integer keys are reindexed +$config = ['host' => 'localhost', 'port' => 8080]; +$defaults = ['host' => '0.0.0.0', 'timeout' => 30]; +$merged_config = [...$defaults, ...$config]; +foreach ($merged_config as $key => $value) { + echo $key . "=" . $value . " "; +} +echo "\n"; diff --git a/examples/web-framework/main.php b/examples/web-framework/main.php index 7b50812c92..68d79fd543 100644 --- a/examples/web-framework/main.php +++ b/examples/web-framework/main.php @@ -239,7 +239,7 @@ public function handle(Request $request, callable $next): void class Home implements Handler { - public function handle(Request $request): void + public function handle(Request $_request): void { $help = "elephc-web mini-framework\n\n" . "GET / this page\n" @@ -262,7 +262,7 @@ public function handle(Request $request): void class UserList implements Handler { - public function handle(Request $request): void + public function handle(Request $_request): void { $users = [ ['id' => 1, 'name' => 'Ada'], @@ -274,7 +274,7 @@ public function handle(Request $request): void class UserCreate implements Handler { - public function handle(Request $request): void + public function handle(Request $_request): void { $name = $_POST['name'] ?? ''; if ($name === '') { @@ -287,7 +287,7 @@ public function handle(Request $request): void class Secret implements Handler { - public function handle(Request $request): void + public function handle(Request $_request): void { Response::json(json_encode(['secret' => 'the cake is a lie']))->send(); } @@ -295,7 +295,7 @@ public function handle(Request $request): void class Boom implements Handler { - public function handle(Request $request): void + public function handle(Request $_request): void { throw new \Exception('intentional failure'); } diff --git a/rustfmt.toml b/rustfmt.toml index 268c949cf4..5627a72273 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,3 @@ -ignore = [ - "src/codegen/runtime", - "src/codegen/builtins", -] +# Keep `cargo fmt` as a no-op in this repository. Large automatic rustfmt diffs +# are intentionally avoided; use focused manual formatting for touched code. +disable_all_formatting = true diff --git a/scripts/check_asm_comments.py b/scripts/check_asm_comments.py new file mode 100755 index 0000000000..dac0e5aac7 --- /dev/null +++ b/scripts/check_asm_comments.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Check inline assembly-comment alignment in elephc codegen files. + +Every `emitter.instruction(...)` call must carry an inline `//` comment that +starts at column 81 (1-indexed). See "Assembly comment alignment" in +CONTRIBUTING.md for the full policy. + +Usage: + scripts/check_asm_comments.py FILE.rs [FILE.rs ...] + +Reports every `emitter.instruction(...)` whose `//` comment is misaligned, as +`path:line: // at col N (expected 81)`. Exits non-zero if any problem is found, +so it can be used in pre-commit hooks or CI. +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +# 1-indexed column where the `//` of an inline comment must start. +COMMENT_COLUMN = 81 + + +def check_file(path: Path) -> list[str]: + """Return a list of human-readable problems for a single file.""" + try: + text = path.read_text() + except OSError as exc: + return [f"{path}: cannot read file ({exc})"] + + problems: list[str] = [] + for lineno, line in enumerate(text.splitlines(), 1): + stripped = line.rstrip() + if "emitter.instruction" not in stripped or "//" not in stripped: + continue + pos = stripped.index("//") # 0-indexed position of the comment + # The `//` must sit at column 81 (index 80). Lines whose code already + # reaches 80 characters may use a single space before `//` instead, so + # they are exempt from the column check. + if pos != COMMENT_COLUMN - 1 and len(line[:pos].rstrip()) < 80: + problems.append(f"{path}:{lineno}: // at col {pos + 1} (expected {COMMENT_COLUMN})") + return problems + + +def main() -> int: + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("files", nargs="+", type=Path, help="Rust codegen files to check") + args = parser.parse_args() + + problems: list[str] = [] + for path in args.files: + problems.extend(check_file(path)) + + for problem in problems: + print(problem) + + if problems: + print(f"\n{len(problems)} misaligned comment(s) found.", file=sys.stderr) + return 1 + + print("All assembly comments aligned.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/docs/README.md b/scripts/docs/README.md index c62e080e52..b4e77c40d0 100644 --- a/scripts/docs/README.md +++ b/scripts/docs/README.md @@ -10,16 +10,24 @@ 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: it parses three layers of the Rust source to -build a single JSON registry (`scripts/docs/builtin_registry.json`) which -the Markdown renderer consumes. The registry is the canonical source of -truth; everything else is generated. +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 +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. +Everything else is generated. ## Usage -From the repo root: +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`): ```bash +# 0. Build the registry exporter the generator reads from +cargo build --bin gen_builtins + # 1. Parse the source and write the JSON registry python3 scripts/docs/extract_builtins.py @@ -36,18 +44,22 @@ everything. ## What the script reads -| Layer | File | What we extract | +| Layer | Source | What we extract | |---|---|---| -| Catalog | `src/types/checker/builtins/catalog.rs` | The authoritative list of PHP-visible builtins. | -| Signatures | `src/types/signatures.rs` | Per-builtin parameter names, defaults, variadics, by-ref flags, and first-class return types. | -| Lowering | `src/codegen_ir/lower_inst/builtins.rs` + per-area submodules | For each builtin: the lowering function, its location, runtime helpers it calls, and the leading `///` doc comment. | +| Registry | `gen_builtins` binary (reads `src/builtins/`) | The authoritative set of builtins (incl. `internal` helpers) with exact signatures: parameter names, types, defaults, by-ref flags, variadics, and return types. | +| Lowering | Home files `src/builtins//.rs` + `src/codegen/lower_inst/builtins/` | Each home's `lower` hook names the emitter it dispatches to; we resolve that emitter's file, line, `__rt_*` runtime helpers, and leading `///` doc comment. | +| Precision | `elephc_builtins/registry.py` | Presentation refinements the registry represents coarsely as `Mixed`: `PARAM_TYPES` (param display types) and `RETURN_TYPE_OVERRIDES`. Return types are also recovered from a home's `check` hook when possible. | + +The registry represents non-scalar params/returns as `Mixed`; the generator +recovers array/typed returns from the home file's `check` hook and applies +`PARAM_TYPES` for param display types. Builtins whose emitter cannot be +resolved are still emitted, but the internals page notes that no dedicated +lowering was found. -The renderer currently reads the dispatch table in `builtins.rs` (root) -plus the `lower_*` function definitions in the submodule files. When a -builtin has no dedicated dispatch arm (e.g. it is handled by a multi-name -catch-all), the renderer falls back to a `lower_` heuristic on the -root file. Builtins that cannot be mapped to a lowering are still emitted, -but the internals page will note that no dedicated lowering was found. +The 8 PHP language constructs that stay checker-resident +(`isset`/`unset`/`empty`/`exit`/`die`/`buffer_len`/`buffer_free`/`buffer_new`) +are not in the registry; they are added from a hand-curated table +(`LANGUAGE_CONSTRUCTS`) in `extract.py`. ## Layout @@ -57,9 +69,9 @@ scripts/docs/ ├── extract_builtins.py # CLI entry point ├── builtin_registry.json # generated — do not edit by hand └── elephc_builtins/ # Python package - ├── extract.py # parses .rs files + ├── extract.py # reads gen_builtins + resolves lowering ├── render.py # emits Markdown - └── registry.py # data model + └── registry.py # data model + area maps + precision tables ``` ## Output tree @@ -82,24 +94,22 @@ docs/ └── … ``` -Every builtin lives in a subfolder that matches its area. The 3 internal +Every builtin lives in a subfolder that matches its area. The internal `__elephc_*` helpers live under `_internal/`. ## Known limitations -- **Signature precision depends on `src/types/signatures.rs` and several - hand-curated tables in `registry.py`.** Parameter names, types, return - types, by-ref flags, optional defaults, and variadic shape are refined - through `PARAM_TYPES`, `PARAM_NAME_OVERRIDES`, `PARAM_TYPE_OVERRIDES`, - `REF_PARAM_OVERRIDES`, `OPTIONAL_PARAM_OVERRIDES`, `RETURN_TYPE_OVERRIDES`, - and `VARIADIC_OVERRIDES`. The generator also reads - `first_class_callable_builtin_sig()` and `check_builtin()` arms for - additional precision. A few builtins still differ from PHP because Elephc - intentionally supports a smaller surface (e.g. fewer optional parameters). -- **About 48 catalog builtins have no captured lowering.** These are - usually handled by multi-name catch-all dispatchers (e.g. libm unary - functions) or by special compiler paths that the heuristic does not - yet recognize. Their internals page will show `(not lowered)`. +- **Parameter names, defaults, by-ref flags, variadic shape, and arity are + exact** — they come straight from the `builtin!` registry, so they match + Elephc's actual supported surface (which is sometimes smaller than PHP's, + e.g. fewer optional parameters). **Non-scalar types are coarse**: the + registry declares arrays/callables/unions as `Mixed`. The generator + recovers array/typed *return* types from each home's `check` hook, and + refines *param* display types via `PARAM_TYPES` in `registry.py`; where + neither applies, a non-scalar shows as `mixed`. +- **A few builtins have no captured lowering.** When a home's `lower` hook + cannot be resolved to an emitter definition, the internals page notes that + no dedicated lowering was found. - **Areas are inferred from the dispatch module** in `builtins.rs` and the file path of the lowering function. Hand-curated overrides live in `elephc_builtins/registry.py` (`AREA_BY_NAME`, `AREA_BY_LOWERING_FN`, diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 79015963b8..26dd3bb52c 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -2,13 +2,13 @@ { "area": "Misc", "canonical_name": "__elephc_gmmktime_raw", - "description": "Internal helper used by the gmmktime() builtin.", - "in_catalog": true, + "description": "Internal raw gmmktime alias used by the synthetic DateTime body.", + "in_catalog": false, "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_gmmktime", "codegen_line": 151, "notes": [ @@ -21,7 +21,7 @@ "__rt_gmmktime" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/__elephc_gmmktime_raw.rs", "sig_line": null }, "name": "__elephc_gmmktime_raw", @@ -79,13 +79,13 @@ { "area": "Misc", "canonical_name": "__elephc_mktime_raw", - "description": "Internal helper used by the mktime() builtin.", - "in_catalog": true, + "description": "Internal raw mktime alias used by the synthetic DateTime body.", + "in_catalog": false, "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_mktime", "codegen_line": 140, "notes": [ @@ -98,7 +98,7 @@ "__rt_mktime" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/__elephc_mktime_raw.rs", "sig_line": null }, "name": "__elephc_mktime_raw", @@ -154,156 +154,145 @@ "sub_area": "System" }, { - "area": "Misc", - "canonical_name": "__elephc_strtotime_raw", - "description": "Internal helper used by the strtotime() builtin.", - "in_catalog": true, + "area": "IO", + "canonical_name": "__elephc_phar_bzip2_archive", + "description": "Compresses a PHAR archive using bzip2.", + "in_catalog": false, "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_elephc_strtotime_raw", - "codegen_line": 543, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_bzip2_archive", + "codegen_line": 4120, "notes": [ - "Internal helper used by the strtotime() builtin.", - "Provides a raw timestamp parsing path for the runtime strtotime helper." - ], - "runtime_helpers": [ - "__rt_strtotime" + "Lowers `__elephc_phar_bzip2_archive(src)` into the whole-archive bzip2 bridge,", + "returning the written destination path (or an empty string on failure)." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_bzip2_archive.rs", "sig_line": null }, - "name": "__elephc_strtotime_raw", + "name": "__elephc_phar_bzip2_archive", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "datetime", + "name": "src", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "baseTimestamp", - "optional": true, - "type": "int" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "__elephc_strtotime_raw", - "sub_area": "System" + "slug": "__elephc_phar_bzip2_archive", + "sub_area": "IO" }, { - "area": "Math", - "canonical_name": "abs", - "description": "Lowers `abs()` for concrete integer-like and floating operands.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_decompress_archive", + "description": "Decompresses a PHAR archive to a new path.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_abs", - "codegen_line": 43, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_decompress_archive", + "codegen_line": 4135, "notes": [ - "Lowers `abs()` for concrete integer-like and floating operands." - ], - "runtime_helpers": [ - "__rt_abs_mixed" + "Lowers `__elephc_phar_decompress_archive(src)` into the whole-archive decompression", + "bridge, returning the written destination path (or an empty string on failure)." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_decompress_archive.rs", "sig_line": null }, - "name": "abs", + "name": "__elephc_phar_decompress_archive", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "src", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "abs", - "sub_area": "Math" + "slug": "__elephc_phar_decompress_archive", + "sub_area": "IO" }, { - "area": "Math", - "canonical_name": "acos", - "description": "", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_get_file_metadata", + "description": "Reads the serialized per-file metadata blob.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_get_file_metadata", + "codegen_line": 4074, + "notes": [ + "Lowers `__elephc_phar_get_file_metadata()` into the per-file metadata-read bridge." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/__elephc_phar_get_file_metadata.rs", "sig_line": null }, - "name": "acos", + "name": "__elephc_phar_get_file_metadata", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "url", "optional": false, - "type": "float" + "type": "string" } ], - "return_type": "float", + "return_type": "string", "variadic": null }, - "slug": "acos", - "sub_area": "Math" + "slug": "__elephc_phar_get_file_metadata", + "sub_area": "IO" }, { - "area": "String", - "canonical_name": "addslashes", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_get_metadata", + "description": "Reads the serialized PHAR-level metadata blob.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_get_metadata", + "codegen_line": 3859, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." - ], - "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "Lowers `__elephc_phar_get_metadata()` into the metadata-read bridge call." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_get_metadata.rs", "sig_line": null }, - "name": "addslashes", + "name": "__elephc_phar_get_metadata", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "filename", "optional": false, "type": "string" } @@ -311,791 +300,734 @@ "return_type": "string", "variadic": null }, - "slug": "addslashes", - "sub_area": "String" + "slug": "__elephc_phar_get_metadata", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_chunk", - "description": "Lowers `array_chunk()` by splitting an indexed array into nested indexed arrays.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_get_signature_hash", + "description": "Returns the PHAR signature hash bytes.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_chunk", - "codegen_line": 81, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_get_signature_hash", + "codegen_line": 4192, "notes": [ - "Lowers `array_chunk()` by splitting an indexed array into nested indexed arrays." + "Lowers `__elephc_phar_get_signature_hash(path)` into the signature-hash read bridge." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_get_signature_hash.rs", "sig_line": null }, - "name": "array_chunk", + "name": "__elephc_phar_get_signature_hash", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", - "optional": false, - "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "preserve_keys", + "name": "path", "optional": false, - "type": "bool" + "type": "string" } ], - "return_type": "array", + "return_type": "string", "variadic": null }, - "slug": "array_chunk", - "sub_area": "Array" + "slug": "__elephc_phar_get_signature_hash", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_column", - "description": "Lowers `array_column()` by dispatching to the helper matching row value ownership.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_get_signature_type", + "description": "Returns the PHAR signature algorithm name.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays/column.rs", - "codegen_function": "lower_array_column", - "codegen_line": 23, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_get_signature_type", + "codegen_line": 4206, "notes": [ - "Lowers `array_column()` by dispatching to the helper matching row value ownership." + "Lowers `__elephc_phar_get_signature_type(path)` into the signature-type read bridge." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_get_signature_type.rs", "sig_line": null }, - "name": "array_column", + "name": "__elephc_phar_get_signature_type", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", - "optional": false, - "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "column_key", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "index_key", + "name": "path", "optional": false, "type": "string" } ], - "return_type": "array", + "return_type": "string", "variadic": null }, - "slug": "array_column", - "sub_area": "Array" + "slug": "__elephc_phar_get_signature_type", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_combine", - "description": "Lowers `array_combine()` through the legacy hash-building runtime helpers.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_get_stub", + "description": "Reads the PHAR stub script.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_combine", - "codegen_line": 152, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_get_stub", + "codegen_line": 3873, "notes": [ - "Lowers `array_combine()` through the legacy hash-building runtime helpers." + "Lowers `__elephc_phar_get_stub()` into the stub-read bridge call." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_get_stub.rs", "sig_line": null }, - "name": "array_combine", + "name": "__elephc_phar_get_stub", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "keys", - "optional": false, - "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "values", + "name": "filename", "optional": false, - "type": "array" + "type": "string" } ], - "return_type": "array", + "return_type": "string", "variadic": null }, - "slug": "array_combine", - "sub_area": "Array" + "slug": "__elephc_phar_get_stub", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_diff", - "description": "Lowers `array_diff()` for two compatible indexed arrays with pointer-sized payload slots.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_gzip_archive", + "description": "Compresses a PHAR archive using gzip.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_diff", - "codegen_line": 874, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_gzip_archive", + "codegen_line": 4105, "notes": [ - "Lowers `array_diff()` for two compatible indexed arrays with pointer-sized payload slots." - ], - "runtime_helpers": [ - "__rt_array_diff", - "__rt_array_diff_key", - "__rt_array_diff_refcounted", - "__rt_array_intersect", - "__rt_array_intersect_key", - "__rt_array_intersect_refcounted" + "Lowers `__elephc_phar_gzip_archive(src)` into the whole-archive gzip bridge,", + "returning the written destination path (or an empty string on failure)." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_gzip_archive.rs", "sig_line": null }, - "name": "array_diff", + "name": "__elephc_phar_gzip_archive", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", + "name": "src", "optional": false, - "type": "array" + "type": "string" } ], - "return_type": "array", - "variadic": "arrays" + "return_type": "string", + "variadic": null }, - "slug": "array_diff", - "sub_area": "Array" + "slug": "__elephc_phar_gzip_archive", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_diff_key", - "description": "Lowers `array_diff_key()` for two associative arrays by filtering first-operand keys.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_list_entries", + "description": "Lists the file paths within a PHAR archive.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_diff_key", - "codegen_line": 896, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_list_entries", + "codegen_line": 4277, "notes": [ - "Lowers `array_diff_key()` for two associative arrays by filtering first-operand keys." - ], - "runtime_helpers": [ - "__rt_array_diff_key", - "__rt_array_intersect_key" + "Internal helper used by the built-in Phar / PharData support to enumerate archive entries.", + "Calls the native PHAR listing bridge and returns the entries as an array." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_list_entries.rs", "sig_line": null }, - "name": "array_diff_key", + "name": "__elephc_phar_list_entries", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", + "name": "filename", "optional": false, - "type": "array" + "type": "string" } ], "return_type": "array", - "variadic": "arrays" + "variadic": null }, - "slug": "array_diff_key", - "sub_area": "Array" + "slug": "__elephc_phar_list_entries", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_fill", - "description": "Lowers `array_fill()` for pointer-sized scalar and refcounted payloads.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_set_compression", + "description": "Sets the compression algorithm for a PHAR archive.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_fill", - "codegen_line": 115, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_set_compression", + "codegen_line": 3801, "notes": [ - "Lowers `array_fill()` for pointer-sized scalar and refcounted payloads." + "Internal helper used by the built-in Phar / PharData support to change archive compression.", + "Calls the native PHAR compression-control bridge and returns whether the update succeeded." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_set_compression.rs", "sig_line": null }, - "name": "array_fill", + "name": "__elephc_phar_set_compression", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "start_index", + "name": "filename", "optional": false, - "type": "int" + "type": "string" }, { "by_ref": false, "default": null, - "name": "count", + "name": "compression", "optional": false, "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "value", - "optional": false, - "type": "mixed" } ], - "return_type": "array", + "return_type": "bool", "variadic": null }, - "slug": "array_fill", - "sub_area": "Array" + "slug": "__elephc_phar_set_compression", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_fill_keys", - "description": "Lowers `array_fill_keys()` through the legacy hash-building runtime helpers.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_set_file_metadata", + "description": "Writes the serialized per-file metadata blob.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_fill_keys", - "codegen_line": 138, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_set_file_metadata", + "codegen_line": 4090, "notes": [ - "Lowers `array_fill_keys()` through the legacy hash-building runtime helpers." + "Lowers `__elephc_phar_set_file_metadata()` into the per-file metadata-write bridge.", + "The single `phar://archive/entry` URL argument is split by the bridge, so this", + "reuses the same `(url, data) -> bool` shape as the archive-level metadata writer." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_set_file_metadata.rs", "sig_line": null }, - "name": "array_fill_keys", + "name": "__elephc_phar_set_file_metadata", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "keys", + "name": "url", "optional": false, - "type": "array" + "type": "string" }, { "by_ref": false, "default": null, - "name": "value", + "name": "metadata", "optional": false, - "type": "mixed" + "type": "string" } ], - "return_type": "array", + "return_type": "bool", "variadic": null }, - "slug": "array_fill_keys", - "sub_area": "Array" + "slug": "__elephc_phar_set_file_metadata", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_filter", - "description": "Lowers `array_filter()` for static and first-class callbacks through the runtime helper.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_set_metadata", + "description": "Writes the serialized PHAR-level metadata blob.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_filter", - "codegen_line": 211, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_set_metadata", + "codegen_line": 3882, "notes": [ - "Lowers `array_filter()` for static and first-class callbacks through the runtime helper." - ], - "runtime_helpers": [ - "__rt_array_filter", - "__rt_array_filter_refcounted" + "Lowers `__elephc_phar_set_metadata()` into the metadata-write bridge call." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_set_metadata.rs", "sig_line": null }, - "name": "array_filter", + "name": "__elephc_phar_set_metadata", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", + "name": "filename", "optional": false, - "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "callback", - "optional": true, - "type": "callable" + "type": "string" }, { "by_ref": false, "default": null, - "name": "mode", - "optional": true, - "type": "int" + "name": "metadata", + "optional": false, + "type": "string" } ], - "return_type": "array", + "return_type": "bool", "variadic": null }, - "slug": "array_filter", - "sub_area": "Array" + "slug": "__elephc_phar_set_metadata", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_flip", - "description": "Lowers `array_flip()` through the legacy hash-building runtime helpers.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_set_stub", + "description": "Writes the PHAR stub script.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_flip", - "codegen_line": 171, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_set_stub", + "codegen_line": 3896, "notes": [ - "Lowers `array_flip()` through the legacy hash-building runtime helpers." + "Lowers `__elephc_phar_set_stub()` into the stub-write bridge call." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_set_stub.rs", "sig_line": null }, - "name": "array_flip", + "name": "__elephc_phar_set_stub", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", + "name": "filename", "optional": false, - "type": "array" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "stub", + "optional": false, + "type": "string" } ], - "return_type": "float", + "return_type": "bool", "variadic": null }, - "slug": "array_flip", - "sub_area": "Array" + "slug": "__elephc_phar_set_stub", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_intersect", - "description": "Lowers `array_intersect()` for two compatible indexed arrays with pointer-sized payload slots.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_set_zip_password", + "description": "Sets the encryption password for a PHAR ZIP archive.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_intersect", - "codegen_line": 885, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_set_zip_password", + "codegen_line": 4178, "notes": [ - "Lowers `array_intersect()` for two compatible indexed arrays with pointer-sized payload slots." - ], - "runtime_helpers": [ - "__rt_array_diff_key", - "__rt_array_intersect", - "__rt_array_intersect_key", - "__rt_array_intersect_refcounted" + "Lowers `__elephc_phar_set_zip_password(password)` into the ZipCrypto password", + "bridge that lets later reads decrypt encrypted ZIP entries." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_set_zip_password.rs", "sig_line": null }, - "name": "array_intersect", + "name": "__elephc_phar_set_zip_password", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", + "name": "password", "optional": false, - "type": "array" + "type": "string" } ], - "return_type": "array", - "variadic": "arrays" + "return_type": "bool", + "variadic": null }, - "slug": "array_intersect", - "sub_area": "Array" + "slug": "__elephc_phar_set_zip_password", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_intersect_key", - "description": "Lowers `array_intersect_key()` for two associative arrays by keeping shared first-operand keys.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_sign_hash", + "description": "Signs a PHAR archive with the given hash algorithm.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_intersect_key", - "codegen_line": 901, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_sign_hash", + "codegen_line": 4163, "notes": [ - "Lowers `array_intersect_key()` for two associative arrays by keeping shared first-operand keys." - ], - "runtime_helpers": [ - "__rt_array_intersect_key" + "Lowers `__elephc_phar_sign_hash(path, algo)` into the hash-based signing bridge." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_sign_hash.rs", "sig_line": null }, - "name": "array_intersect_key", + "name": "__elephc_phar_sign_hash", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", + "name": "path", "optional": false, - "type": "array" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false, + "type": "string" } ], - "return_type": "array", - "variadic": "arrays" + "return_type": "bool", + "variadic": null }, - "slug": "array_intersect_key", - "sub_area": "Array" + "slug": "__elephc_phar_sign_hash", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_key_exists", - "description": "Lowers `array_key_exists()` for indexed arrays and associative arrays.", - "in_catalog": true, - "is_internal": false, + "area": "IO", + "canonical_name": "__elephc_phar_sign_openssl", + "description": "Signs a PHAR archive using an OpenSSL private key.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs", - "codegen_function": "lower_array_key_exists", - "codegen_line": 22, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_elephc_phar_sign_openssl", + "codegen_line": 4149, "notes": [ - "Lowers `array_key_exists()` for indexed arrays and associative arrays." + "Lowers `__elephc_phar_sign_openssl(path, keyPem)` into the RSA-SHA1 signing bridge." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/__elephc_phar_sign_openssl.rs", "sig_line": null }, - "name": "array_key_exists", + "name": "__elephc_phar_sign_openssl", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "key", + "name": "path", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "array", + "name": "key", "optional": false, - "type": "array" + "type": "string" } ], "return_type": "bool", "variadic": null }, - "slug": "array_key_exists", - "sub_area": "Array" + "slug": "__elephc_phar_sign_openssl", + "sub_area": "IO" }, { - "area": "Array", - "canonical_name": "array_keys", - "description": "Lowers `array_keys()` for indexed arrays and associative arrays.", - "in_catalog": true, - "is_internal": false, + "area": "Misc", + "canonical_name": "__elephc_strtotime_raw", + "description": "Internal raw strtotime alias returning a plain integer.", + "in_catalog": false, + "is_internal": true, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays/keys.rs", - "codegen_function": "lower_array_keys", - "codegen_line": 23, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_elephc_strtotime_raw", + "codegen_line": 543, "notes": [ - "Lowers `array_keys()` for indexed arrays and associative arrays." + "Internal helper used by the strtotime() builtin.", + "Provides a raw timestamp parsing path for the runtime strtotime helper." + ], + "runtime_helpers": [ + "__rt_strtotime" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/__elephc_strtotime_raw.rs", "sig_line": null }, - "name": "array_keys", + "name": "__elephc_strtotime_raw", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", - "optional": false, - "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "filter_value", + "name": "datetime", "optional": false, "type": "string" }, { "by_ref": false, - "default": null, - "name": "strict", - "optional": false, - "type": "bool" + "default": "null", + "name": "baseTimestamp", + "optional": true, + "type": "int" } ], - "return_type": "array", + "return_type": "int", "variadic": null }, - "slug": "array_keys", - "sub_area": "Array" + "slug": "__elephc_strtotime_raw", + "sub_area": "System" }, { - "area": "Array", - "canonical_name": "array_map", - "description": "Lowers `array_map()` through the callback runtime helper matching the callback result type.", + "area": "Math", + "canonical_name": "abs", + "description": "Absolute value.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_map", - "codegen_line": 312, + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_abs", + "codegen_line": 43, "notes": [ - "Lowers `array_map()` through the callback runtime helper matching the callback result type." + "Lowers `abs()` for concrete integer-like and floating operands." ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": null, + "runtime_helpers": [ + "__rt_abs_mixed" + ], + "sig_arm": null, + "sig_file": "src/builtins/math/abs.rs", "sig_line": null }, - "name": "array_map", + "name": "abs", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "callback", - "optional": false, - "type": "callable" - }, - { - "by_ref": false, - "default": null, - "name": "array", + "name": "num", "optional": false, - "type": "array" + "type": "int" } ], - "return_type": "array", - "variadic": "arrays" + "return_type": "mixed", + "variadic": null }, - "slug": "array_map", - "sub_area": "Array" + "slug": "abs", + "sub_area": "Math" }, { - "area": "Array", - "canonical_name": "array_merge", - "description": "Lowers `array_merge()` for two compatible indexed arrays with 8-byte payload slots.", + "area": "Math", + "canonical_name": "acos", + "description": "Returns the arccosine of a number in radians.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_merge", - "codegen_line": 850, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers `array_merge()` for two compatible indexed arrays with 8-byte payload slots." - ], - "runtime_helpers": [ - "__rt_array_diff", - "__rt_array_diff_refcounted" + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/acos.rs", "sig_line": null }, - "name": "array_merge", + "name": "acos", "sig": { - "params": [], - "return_type": "array", - "variadic": "arrays" + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "float" + } + ], + "return_type": "float", + "variadic": null }, - "slug": "array_merge", - "sub_area": "Array" + "slug": "acos", + "sub_area": "Math" }, { - "area": "Array", - "canonical_name": "array_pad", - "description": "Lowers `array_pad()` by copying an indexed array and filling missing slots.", + "area": "String", + "canonical_name": "addslashes", + "description": "Adds backslashes before characters that need to be escaped.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_pad", - "codegen_line": 99, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `array_pad()` by copying an indexed array and filling missing slots." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." + ], + "runtime_helpers": [ + "__rt_grapheme_strrev", + "__rt_strcopy" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/addslashes.rs", "sig_line": null }, - "name": "array_pad", + "name": "addslashes", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", - "optional": false, - "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "value", + "name": "string", "optional": false, - "type": "mixed" + "type": "string" } ], - "return_type": "array", + "return_type": "string", "variadic": null }, - "slug": "array_pad", - "sub_area": "Array" + "slug": "addslashes", + "sub_area": "String" }, { "area": "Array", - "canonical_name": "array_pop", - "description": "Lowers `array_pop()` for indexed arrays by mutating length and boxing `T|null` as Mixed.", + "canonical_name": "array_all", + "description": "Returns true when every array element satisfies the predicate callback.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_pop", - "codegen_line": 1048, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_all", + "codegen_line": 1578, "notes": [ - "Lowers `array_pop()` for indexed arrays by mutating length and boxing `T|null` as Mixed." + "Lowers `array_all()`: returns true when every element satisfies the predicate." ], "runtime_helpers": [ - "__rt_sort_int", - "__rt_sort_str" + "__rt_array_udiff_uintersect", + "__rt_array_walk_recursive" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_all.rs", "sig_line": null }, - "name": "array_pop", + "name": "array_all", "sig": { "params": [ { - "by_ref": true, + "by_ref": false, "default": null, "name": "array", "optional": false, - "type": "array" + "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false, + "type": "mixed" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "array_pop", + "slug": "array_all", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_product", - "description": "Lowers `array_product()` over supported indexed-array payloads.", + "canonical_name": "array_any", + "description": "Returns true when at least one array element satisfies the predicate callback.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_product", - "codegen_line": 56, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_any", + "codegen_line": 1573, "notes": [ - "Lowers `array_product()` over supported indexed-array payloads." + "Lowers `array_any()`: returns true when some element satisfies the predicate." ], "runtime_helpers": [ - "__rt_array_product" + "__rt_array_walk_recursive" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_any.rs", "sig_line": null }, - "name": "array_product", + "name": "array_any", "sig": { "params": [ { @@ -1103,76 +1035,87 @@ "default": null, "name": "array", "optional": false, - "type": "array" + "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false, + "type": "mixed" } ], - "return_type": "float", + "return_type": "bool", "variadic": null }, - "slug": "array_product", + "slug": "array_any", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_push", - "description": "Lowers `array_push()` by appending one value and publishing the mutated array.", + "canonical_name": "array_chunk", + "description": "Splits an array into chunks of the given size.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_push", - "codegen_line": 61, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_chunk", + "codegen_line": 80, "notes": [ - "Lowers `array_push()` by appending one value and publishing the mutated array." + "Lowers `array_chunk()` by splitting an indexed array into nested indexed arrays." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_chunk.rs", "sig_line": null }, - "name": "array_push", + "name": "array_chunk", "sig": { "params": [ { - "by_ref": true, + "by_ref": false, "default": null, "name": "array", "optional": false, "type": "array" + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "int" } ], - "return_type": "void", - "variadic": "values" + "return_type": "array", + "variadic": null }, - "slug": "array_push", + "slug": "array_chunk", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_rand", - "description": "Lowers `array_rand()` for indexed arrays.", + "canonical_name": "array_column", + "description": "Returns the values from a single column of an array of arrays.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_rand", - "codegen_line": 1007, + "codegen_file": "src/codegen/lower_inst/builtins/arrays/column.rs", + "codegen_function": "lower_array_column", + "codegen_line": 23, "notes": [ - "Lowers `array_rand()` for indexed arrays." - ], - "runtime_helpers": [ - "__rt_array_rand", - "__rt_mixed_cast_int" + "Lowers `array_column()` by dispatching to the helper matching row value ownership." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_column.rs", "sig_line": null }, - "name": "array_rand", + "name": "array_column", "sig": { "params": [ { @@ -1185,91 +1128,88 @@ { "by_ref": false, "default": null, - "name": "num", + "name": "column_key", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "int", + "return_type": "array", "variadic": null }, - "slug": "array_rand", + "slug": "array_column", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_reduce", - "description": "Lowers `array_reduce()` through the callback-driven runtime helper.", + "canonical_name": "array_combine", + "description": "Creates an array by using one array for keys and another for values.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_reduce", - "codegen_line": 701, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_combine", + "codegen_line": 160, "notes": [ - "Lowers `array_reduce()` through the callback-driven runtime helper." - ], - "runtime_helpers": [ - "__rt_array_reduce" + "Lowers `array_combine()` through the hash-building runtime helpers." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_combine.rs", "sig_line": null }, - "name": "array_reduce", + "name": "array_combine", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", + "name": "keys", "optional": false, "type": "array" }, { "by_ref": false, "default": null, - "name": "callback", + "name": "values", "optional": false, - "type": "callable" - }, - { - "by_ref": false, - "default": null, - "name": "initial", - "optional": true, - "type": "mixed" + "type": "array" } ], - "return_type": "int", + "return_type": "array", "variadic": null }, - "slug": "array_reduce", + "slug": "array_combine", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_reverse", - "description": "Lowers `array_reverse()` for indexed arrays with 8-byte payload slots.", + "canonical_name": "array_diff", + "description": "Computes the difference of arrays.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_reverse", - "codegen_line": 185, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_diff", + "codegen_line": 870, "notes": [ - "Lowers `array_reverse()` for indexed arrays with 8-byte payload slots." + "Lowers `array_diff()` for two compatible indexed arrays with pointer-sized payload slots." + ], + "runtime_helpers": [ + "__rt_array_diff", + "__rt_array_diff_key", + "__rt_array_diff_refcounted", + "__rt_array_intersect", + "__rt_array_intersect_refcounted" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_diff.rs", "sig_line": null }, - "name": "array_reverse", + "name": "array_diff", "sig": { "params": [ { @@ -1278,249 +1218,212 @@ "name": "array", "optional": false, "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "preserve_keys", - "optional": false, - "type": "bool" } ], "return_type": "array", - "variadic": null + "variadic": "arrays" }, - "slug": "array_reverse", + "slug": "array_diff", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_search", - "description": "Lowers `array_search()` for indexed arrays with integer-like payloads.", + "canonical_name": "array_diff_assoc", + "description": "Computes the difference of arrays with additional index check.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_search", - "codegen_line": 1141, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_diff_assoc", + "codegen_line": 1359, "notes": [ - "Lowers `array_search()` for indexed arrays with integer-like payloads." + "Lowers `array_diff_assoc()` via the shared associative diff/intersect helper (mode 0 = diff)." + ], + "runtime_helpers": [ + "__rt_assoc_diff_intersect" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_diff_assoc.rs", "sig_line": null }, - "name": "array_search", + "name": "array_diff_assoc", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "needle", - "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "haystack", + "name": "array", "optional": false, "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "strict", - "optional": true, - "type": "bool" } ], "return_type": "mixed", - "variadic": null + "variadic": "arrays" }, - "slug": "array_search", + "slug": "array_diff_assoc", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_shift", - "description": "Lowers `array_shift()` for indexed arrays by compacting slots and boxing `T|null` as Mixed.", + "canonical_name": "array_diff_key", + "description": "Computes the difference of arrays using keys for comparison.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays/shift.rs", - "codegen_function": "lower_array_shift", - "codegen_line": 23, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_diff_key", + "codegen_line": 895, "notes": [ - "Lowers `array_shift()` for indexed arrays by compacting slots and boxing `T|null` as Mixed." + "Lowers `array_diff_key()` for two associative arrays by filtering first-operand keys." + ], + "runtime_helpers": [ + "__rt_array_diff_key", + "__rt_array_intersect_key" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_diff_key.rs", "sig_line": null }, - "name": "array_shift", + "name": "array_diff_key", "sig": { "params": [ { - "by_ref": true, + "by_ref": false, "default": null, "name": "array", "optional": false, "type": "array" } ], - "return_type": "mixed", - "variadic": null + "return_type": "array", + "variadic": "arrays" }, - "slug": "array_shift", + "slug": "array_diff_key", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_slice", - "description": "Lowers `array_slice()` for indexed arrays with pointer-sized payload slots.", + "canonical_name": "array_fill", + "description": "Fill an array with values.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_slice", - "codegen_line": 906, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_fill", + "codegen_line": 116, "notes": [ - "Lowers `array_slice()` for indexed arrays with pointer-sized payload slots." + "Lowers `array_fill()` for pointer-sized scalar and refcounted payloads." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_fill.rs", "sig_line": null }, - "name": "array_slice", + "name": "array_fill", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "array", - "optional": false, - "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "offset", + "name": "start_index", "optional": false, "type": "int" }, { "by_ref": false, "default": null, - "name": "length", - "optional": true, + "name": "count", + "optional": false, "type": "int" }, { "by_ref": false, "default": null, - "name": "preserve_keys", + "name": "value", "optional": false, - "type": "bool" + "type": "mixed" } ], "return_type": "array", "variadic": null }, - "slug": "array_slice", + "slug": "array_fill", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_splice", - "description": "Lowers `array_splice()` by mutating an indexed source array and returning removed elements.", + "canonical_name": "array_fill_keys", + "description": "Fill an array with values, specifying keys.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_splice", - "codegen_line": 949, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_fill_keys", + "codegen_line": 139, "notes": [ - "Lowers `array_splice()` by mutating an indexed source array and returning removed elements." + "Lowers `array_fill_keys()` through the hash-building runtime helpers." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_fill_keys.rs", "sig_line": null }, - "name": "array_splice", + "name": "array_fill_keys", "sig": { "params": [ - { - "by_ref": true, - "default": null, - "name": "array", - "optional": false, - "type": "array" - }, { "by_ref": false, "default": null, - "name": "offset", + "name": "keys", "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": true, - "type": "int" + "type": "array" }, { "by_ref": false, "default": null, - "name": "replacement", + "name": "value", "optional": false, - "type": "array" + "type": "mixed" } ], "return_type": "array", "variadic": null }, - "slug": "array_splice", + "slug": "array_fill_keys", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_sum", - "description": "Lowers `array_sum()` over supported indexed-array payloads.", + "canonical_name": "array_filter", + "description": "Filters elements of an array using a callback function.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_sum", - "codegen_line": 51, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_filter", + "codegen_line": 221, "notes": [ - "Lowers `array_sum()` over supported indexed-array payloads." + "Lowers `array_filter()` for static and first-class callbacks through the runtime helper." ], "runtime_helpers": [ - "__rt_array_product", - "__rt_array_sum" + "__rt_array_filter", + "__rt_array_filter_refcounted" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_filter.rs", "sig_line": null }, - "name": "array_sum", + "name": "array_filter", "sig": { "params": [ { @@ -1529,38 +1432,51 @@ "name": "array", "optional": false, "type": "array" + }, + { + "by_ref": false, + "default": "null", + "name": "callback", + "optional": true, + "type": "callable" + }, + { + "by_ref": false, + "default": "0", + "name": "mode", + "optional": true, + "type": "int" } ], - "return_type": "float", + "return_type": "array", "variadic": null }, - "slug": "array_sum", + "slug": "array_filter", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_unique", - "description": "Lowers `array_unique()` for indexed arrays with 8-byte payload slots.", + "canonical_name": "array_find", + "description": "Returns the first element satisfying a predicate callback, or null.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_unique", - "codegen_line": 198, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_find", + "codegen_line": 1568, "notes": [ - "Lowers `array_unique()` for indexed arrays with 8-byte payload slots." + "Lowers `array_find()`: returns the first element satisfying the predicate, boxed as Mixed (or null)." ], "runtime_helpers": [ - "__rt_array_filter", - "__rt_array_filter_refcounted" + "__rt_array_walk_recursive" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_find.rs", "sig_line": null }, - "name": "array_unique", + "name": "array_find", "sig": { "params": [ { @@ -1568,80 +1484,85 @@ "default": null, "name": "array", "optional": false, - "type": "array" + "type": "mixed" }, { "by_ref": false, "default": null, - "name": "flags", + "name": "callback", "optional": false, - "type": "int" + "type": "mixed" } ], - "return_type": "array", + "return_type": "mixed", "variadic": null }, - "slug": "array_unique", + "slug": "array_find", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_unshift", - "description": "Lowers `array_unshift()` by ensuring uniqueness, prepending one scalar value, and returning count.", + "canonical_name": "array_flip", + "description": "Exchanges all keys with their associated values in an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays/unshift.rs", - "codegen_function": "lower_array_unshift", - "codegen_line": 23, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_flip", + "codegen_line": 179, "notes": [ - "Lowers `array_unshift()` by ensuring uniqueness, prepending one scalar value, and returning count." + "Lowers `array_flip()` through the hash-building runtime helpers." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_flip.rs", "sig_line": null }, - "name": "array_unshift", + "name": "array_flip", "sig": { "params": [ { - "by_ref": true, + "by_ref": false, "default": null, "name": "array", "optional": false, "type": "array" } ], - "return_type": "int", - "variadic": "values" + "return_type": "array", + "variadic": null }, - "slug": "array_unshift", + "slug": "array_flip", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_values", - "description": "Lowers `array_values()` for indexed arrays as an alias or associative arrays as a new values array.", + "canonical_name": "array_intersect", + "description": "Computes the intersection of arrays.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays/values.rs", - "codegen_function": "lower_array_values", - "codegen_line": 22, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_intersect", + "codegen_line": 881, "notes": [ - "Lowers `array_values()` for indexed arrays as an alias or associative arrays as a new values array." + "Lowers `array_intersect()` for two compatible indexed arrays with pointer-sized payload slots." + ], + "runtime_helpers": [ + "__rt_array_diff_key", + "__rt_array_intersect", + "__rt_array_intersect_key", + "__rt_array_intersect_refcounted" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_intersect.rs", "sig_line": null }, - "name": "array_values", + "name": "array_intersect", "sig": { "params": [ { @@ -1653,1750 +1574,1737 @@ } ], "return_type": "array", - "variadic": null + "variadic": "arrays" }, - "slug": "array_values", + "slug": "array_intersect", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "array_walk", - "description": "Lowers `array_walk()` through the callback-driven runtime helper.", + "canonical_name": "array_intersect_assoc", + "description": "Computes the intersection of arrays with additional index check.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_array_walk", - "codegen_line": 783, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_intersect_assoc", + "codegen_line": 1373, "notes": [ - "Lowers `array_walk()` through the callback-driven runtime helper." + "Lowers `array_intersect_assoc()` via the shared associative diff/intersect helper (mode 1 = intersect)." ], "runtime_helpers": [ - "__rt_array_walk" + "__rt_array_find_any_all", + "__rt_array_merge_recursive", + "__rt_array_udiff_uintersect", + "__rt_assoc_diff_intersect" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_intersect_assoc.rs", "sig_line": null }, - "name": "array_walk", + "name": "array_intersect_assoc", "sig": { "params": [ { - "by_ref": true, + "by_ref": false, "default": null, "name": "array", "optional": false, "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "callback", - "optional": false, - "type": "callable" - }, - { - "by_ref": false, - "default": null, - "name": "arg", - "optional": false, - "type": "mixed" } ], - "return_type": "void", - "variadic": null + "return_type": "mixed", + "variadic": "arrays" }, - "slug": "array_walk", + "slug": "array_intersect_assoc", "sub_area": "Array" }, { "area": "Array", - "canonical_name": "arsort", - "description": "Lowers `arsort()` for indexed integer arrays through the descending value-sort wrapper.", + "canonical_name": "array_intersect_key", + "description": "Computes the intersection of arrays using keys for comparison.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_arsort", - "codegen_line": 1091, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_intersect_key", + "codegen_line": 903, "notes": [ - "Lowers `arsort()` for indexed integer arrays through the descending value-sort wrapper." + "Lowers `array_intersect_key()` for two associative arrays by keeping shared first-operand keys." ], "runtime_helpers": [ - "__rt_arsort", - "__rt_krsort", - "__rt_ksort", - "__rt_natcasesort", - "__rt_natsort" + "__rt_array_intersect_key" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_intersect_key.rs", "sig_line": null }, - "name": "arsort", + "name": "array_intersect_key", "sig": { "params": [ { - "by_ref": true, + "by_ref": false, "default": null, "name": "array", "optional": false, "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": false, - "type": "int" } ], - "return_type": "bool", - "variadic": null + "return_type": "array", + "variadic": "arrays" }, - "slug": "arsort", + "slug": "array_intersect_key", "sub_area": "Array" }, { - "area": "Math", - "canonical_name": "asin", - "description": "", + "area": "Array", + "canonical_name": "array_is_list", + "description": "Checks whether an array is a list (sequential 0-based integer keys).", "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": "src/types/signatures.rs", - "sig_line": null - }, - "name": "asin", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_is_list", + "codegen_line": 1150, + "notes": [ + "Lowers `array_is_list()` to the `__rt_array_is_list` runtime predicate, returning a bool.", + "The runtime helper accepts any array kind (indexed, associative hash, or boxed mixed cell) and", + "reports `1` when the keys are the sequential integers `0..n-1` in insertion order, `0` otherwise." + ], + "runtime_helpers": [ + "__rt_array_edge_key", + "__rt_array_is_list", + "__rt_mixed_from_value" + ], + "sig_arm": null, + "sig_file": "src/builtins/array/array_is_list.rs", + "sig_line": null + }, + "name": "array_is_list", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "array", "optional": false, - "type": "float" + "type": "mixed" } ], - "return_type": "float", + "return_type": "bool", "variadic": null }, - "slug": "asin", - "sub_area": "Math" + "slug": "array_is_list", + "sub_area": "Array" }, { "area": "Array", - "canonical_name": "asort", - "description": "Lowers `asort()` for indexed integer arrays through the value-sort runtime wrapper.", + "canonical_name": "array_key_exists", + "description": "Checks if the given key or index exists in the array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_asort", - "codegen_line": 1086, + "codegen_file": "src/codegen/lower_inst/builtins/arrays/key_exists.rs", + "codegen_function": "lower_array_key_exists", + "codegen_line": 22, "notes": [ - "Lowers `asort()` for indexed integer arrays through the value-sort runtime wrapper." - ], - "runtime_helpers": [ - "__rt_arsort", - "__rt_asort", - "__rt_krsort", - "__rt_ksort", - "__rt_natcasesort", - "__rt_natsort" + "Lowers `array_key_exists()` for indexed arrays and associative arrays." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_key_exists.rs", "sig_line": null }, - "name": "asort", + "name": "array_key_exists", "sig": { "params": [ { - "by_ref": true, + "by_ref": false, "default": null, - "name": "array", + "name": "key", "optional": false, - "type": "array" + "type": "string" }, { "by_ref": false, "default": null, - "name": "flags", + "name": "array", "optional": false, - "type": "int" + "type": "array" } ], "return_type": "bool", "variadic": null }, - "slug": "asort", + "slug": "array_key_exists", "sub_area": "Array" }, { - "area": "Math", - "canonical_name": "atan", - "description": "", + "area": "Array", + "canonical_name": "array_key_first", + "description": "Gets the first key of an array.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_key_first", + "codegen_line": 1161, + "notes": [ + "Lowers `array_key_first()` through the shared edge-key helper with selector `0`." + ], + "runtime_helpers": [ + "__rt_array_edge_key", + "__rt_mixed_from_value" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/array/array_key_first.rs", "sig_line": null }, - "name": "atan", + "name": "array_key_first", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "array", "optional": false, - "type": "float" + "type": "array" } ], - "return_type": "float", + "return_type": "mixed", "variadic": null }, - "slug": "atan", - "sub_area": "Math" + "slug": "array_key_first", + "sub_area": "Array" }, { - "area": "Math", - "canonical_name": "atan2", - "description": "Lowers `atan2()` using the C ABI argument order `y, x`.", + "area": "Array", + "canonical_name": "array_key_last", + "description": "Gets the last key of an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/libm.rs", - "codegen_function": "lower_atan2", - "codegen_line": 35, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_key_last", + "codegen_line": 1169, "notes": [ - "Lowers `atan2()` using the C ABI argument order `y, x`." + "Lowers `array_key_last()` through the shared edge-key helper with selector `1`." + ], + "runtime_helpers": [ + "__rt_array_edge_key", + "__rt_mixed_from_value" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_key_last.rs", "sig_line": null }, - "name": "atan2", + "name": "array_key_last", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "y", - "optional": false, - "type": "float" - }, - { - "by_ref": false, - "default": null, - "name": "x", + "name": "array", "optional": false, - "type": "float" + "type": "array" } ], - "return_type": "float", + "return_type": "mixed", "variadic": null }, - "slug": "atan2", - "sub_area": "Math" + "slug": "array_key_last", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "base64_decode", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "area": "Array", + "canonical_name": "array_keys", + "description": "Returns all the keys of an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/arrays/keys.rs", + "codegen_function": "lower_array_keys", + "codegen_line": 23, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." - ], - "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "Lowers `array_keys()` for indexed arrays and associative arrays." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_keys.rs", "sig_line": null }, - "name": "base64_decode", + "name": "array_keys", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "strict", + "name": "array", "optional": false, - "type": "bool" + "type": "array" } ], - "return_type": "string", + "return_type": "array", "variadic": null }, - "slug": "base64_decode", - "sub_area": "String" + "slug": "array_keys", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "base64_encode", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "area": "Array", + "canonical_name": "array_map", + "description": "Applies a callback to the elements of an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_map", + "codegen_line": 326, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." - ], - "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "Lowers `array_map()` through the callback runtime helper matching the callback result type." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_map.rs", "sig_line": null }, - "name": "base64_encode", + "name": "array_map", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "callback", "optional": false, - "type": "string" + "type": "callable" + }, + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false, + "type": "array" } ], - "return_type": "string", - "variadic": null + "return_type": "array", + "variadic": "arrays" }, - "slug": "base64_encode", - "sub_area": "String" + "slug": "array_map", + "sub_area": "Array" }, { - "area": "Filesystem", - "canonical_name": "basename", - "description": "Lowers `basename(path, suffix?)` through the target-aware runtime helper.", + "area": "Array", + "canonical_name": "array_merge", + "description": "Merges the elements of two arrays.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_basename", - "codegen_line": 3893, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_merge", + "codegen_line": 846, "notes": [ - "Lowers `basename(path, suffix?)` through the target-aware runtime helper." + "Lowers `array_merge()` for two compatible indexed arrays with 8-byte payload slots." + ], + "runtime_helpers": [ + "__rt_array_diff", + "__rt_array_diff_refcounted" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_merge.rs", "sig_line": null }, - "name": "basename", + "name": "array_merge", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "path", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "suffix", - "optional": true, - "type": "string" - } - ], - "return_type": "string", - "variadic": null + "params": [], + "return_type": "array", + "variadic": "arrays" }, - "slug": "basename", - "sub_area": "Filesystem" + "slug": "array_merge", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "bin2hex", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "area": "Array", + "canonical_name": "array_merge_recursive", + "description": "Recursively merges two arrays, combining scalar collisions into lists.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_merge_recursive", + "codegen_line": 1387, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." + "Lowers `array_merge_recursive()` (recursive merge with scalar collisions combined into lists)." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_array_find_any_all", + "__rt_array_merge_recursive", + "__rt_array_udiff_uintersect" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_merge_recursive.rs", "sig_line": null }, - "name": "bin2hex", + "name": "array_merge_recursive", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "string", - "optional": false, - "type": "string" - } - ], - "return_type": "string", - "variadic": null + "params": [], + "return_type": "array", + "variadic": "arrays" }, - "slug": "bin2hex", - "sub_area": "String" + "slug": "array_merge_recursive", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "gzcompress", - "description": "Lowers `gzcompress(data, level?)` through inline zlib `compress2` calls.", + "area": "Array", + "canonical_name": "array_multisort", + "description": "Sorts multiple arrays or multi-dimensional arrays.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_gzcompress", - "codegen_line": 402, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_multisort", + "codegen_line": 1719, "notes": [ - "Lowers `gzcompress(data, level?)` through inline zlib `compress2` calls." + "Lowers `array_multisort()`: stable-sorts the first indexed array ascending and reorders the second", + "in tandem, both in place. Both arguments are by-reference, so each is copy-on-write split with", + "`ensure_unique_sort_source` and the (possibly relocated) pointer is written back to its local", + "before the runtime mutates the storage. Returns `true`. Supports 8-byte scalar indexed arrays." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_multisort.rs", "sig_line": null }, - "name": "gzcompress", + "name": "array_multisort", "sig": { "params": [ { - "by_ref": false, + "by_ref": true, "default": null, - "name": "data", + "name": "array1", "optional": false, - "type": "string" + "type": "array" }, { - "by_ref": false, + "by_ref": true, "default": null, - "name": "level", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "encoding", + "name": "array2", "optional": false, "type": "int" } ], - "return_type": "string", + "return_type": "bool", "variadic": null }, - "slug": "gzcompress", - "sub_area": "String" + "slug": "array_multisort", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "gzdeflate", - "description": "Lowers `gzdeflate(data, level?)` through inline raw-DEFLATE zlib calls.", + "area": "Array", + "canonical_name": "array_pad", + "description": "Pads an array to the specified length with a value.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_gzdeflate", - "codegen_line": 418, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_pad", + "codegen_line": 99, "notes": [ - "Lowers `gzdeflate(data, level?)` through inline raw-DEFLATE zlib calls." + "Lowers `array_pad()` by copying an indexed array and filling missing slots." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_pad.rs", "sig_line": null }, - "name": "gzdeflate", + "name": "array_pad", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "data", + "name": "array", "optional": false, - "type": "string" + "type": "array" }, { "by_ref": false, "default": null, - "name": "level", - "optional": true, + "name": "length", + "optional": false, "type": "int" }, { "by_ref": false, "default": null, - "name": "encoding", + "name": "value", "optional": false, - "type": "int" + "type": "mixed" } ], - "return_type": "string", + "return_type": "array", "variadic": null }, - "slug": "gzdeflate", - "sub_area": "String" + "slug": "array_pad", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "gzinflate", - "description": "Lowers `gzinflate(data, max_length?)` and boxes zlib failures as PHP false.", + "area": "Array", + "canonical_name": "array_pop", + "description": "Pops the element off the end of array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_gzinflate", - "codegen_line": 436, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_pop", + "codegen_line": 1051, "notes": [ - "Lowers `gzinflate(data, max_length?)` and boxes zlib failures as PHP false." + "Lowers `array_pop()` for indexed arrays by mutating length and boxing `T|null` as Mixed." + ], + "runtime_helpers": [ + "__rt_sort_int", + "__rt_sort_str" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_pop.rs", "sig_line": null }, - "name": "gzinflate", + "name": "array_pop", "sig": { "params": [ { - "by_ref": false, + "by_ref": true, "default": null, - "name": "data", + "name": "array", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "max_length", - "optional": true, - "type": "int" + "type": "array" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "gzinflate", - "sub_area": "String" + "slug": "array_pop", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "gzuncompress", - "description": "Lowers `gzuncompress(data, max_length?)` and boxes zlib failures as PHP false.", + "area": "Array", + "canonical_name": "array_product", + "description": "Calculate the product of values in an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_gzuncompress", - "codegen_line": 457, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_product", + "codegen_line": 55, "notes": [ - "Lowers `gzuncompress(data, max_length?)` and boxes zlib failures as PHP false." + "Lowers `array_product()` over supported indexed-array payloads." ], "runtime_helpers": [ - "__rt_long2ip" + "__rt_array_product" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_product.rs", "sig_line": null }, - "name": "gzuncompress", + "name": "array_product", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "data", + "name": "array", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "max_length", - "optional": true, - "type": "int" + "type": "array" } ], - "return_type": "string", + "return_type": "int", "variadic": null }, - "slug": "gzuncompress", - "sub_area": "String" + "slug": "array_product", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "long2ip", - "description": "Lowers `long2ip(value)` through the IPv4 formatting runtime helper.", + "area": "Array", + "canonical_name": "array_push", + "description": "Pushes one or more elements onto the end of array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_long2ip", - "codegen_line": 476, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_push", + "codegen_line": 60, "notes": [ - "Lowers `long2ip(value)` through the IPv4 formatting runtime helper." - ], - "runtime_helpers": [ - "__rt_ip2long", - "__rt_long2ip" + "Lowers `array_push()` by appending one value and publishing the mutated array." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_push.rs", "sig_line": null }, - "name": "long2ip", + "name": "array_push", "sig": { "params": [ { - "by_ref": false, + "by_ref": true, "default": null, - "name": "ip", + "name": "array", "optional": false, - "type": "int" + "type": "array" } ], - "return_type": "string", - "variadic": null + "return_type": "void", + "variadic": "values" }, - "slug": "long2ip", - "sub_area": "String" + "slug": "array_push", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "ip2long", - "description": "Lowers `ip2long(string)` and boxes invalid-address results as PHP false.", + "area": "Array", + "canonical_name": "array_rand", + "description": "Pick one or more random keys out of an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_ip2long", - "codegen_line": 488, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_rand", + "codegen_line": 1010, "notes": [ - "Lowers `ip2long(string)` and boxes invalid-address results as PHP false." + "Lowers `array_rand()` for indexed arrays." ], "runtime_helpers": [ - "__rt_ip2long", - "__rt_sprintf" + "__rt_array_rand", + "__rt_mixed_cast_int" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_rand.rs", "sig_line": null }, - "name": "ip2long", + "name": "array_rand", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "ip", + "name": "array", "optional": false, - "type": "string" + "type": "array" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "ip2long", - "sub_area": "String" + "slug": "array_rand", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "inet_ntop", - "description": "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false.", + "area": "Array", + "canonical_name": "array_reduce", + "description": "Iteratively reduces an array to a single value using a callback function.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_inet", - "codegen_line": 497, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_reduce", + "codegen_line": 695, "notes": [ - "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." + "Lowers `array_reduce()` through the callback-driven runtime helper." ], "runtime_helpers": [ - "__rt_sprintf" + "__rt_array_reduce" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_reduce.rs", "sig_line": null }, - "name": "inet_ntop", + "name": "array_reduce", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "ip", + "name": "array", "optional": false, - "type": "string" + "type": "array" + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false, + "type": "callable" + }, + { + "by_ref": false, + "default": "null", + "name": "initial", + "optional": true, + "type": "mixed" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "inet_ntop", - "sub_area": "String" + "slug": "array_reduce", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "inet_pton", - "description": "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false.", + "area": "Array", + "canonical_name": "array_replace", + "description": "Replaces elements from passed arrays into the first array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_inet", - "codegen_line": 497, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_replace", + "codegen_line": 1340, "notes": [ - "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." + "Lowers `array_replace()` (right-wins hash merge of two hashes)." ], "runtime_helpers": [ - "__rt_sprintf" + "__rt_array_replace", + "__rt_array_replace_recursive", + "__rt_assoc_diff_intersect" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_replace.rs", "sig_line": null }, - "name": "inet_pton", + "name": "array_replace", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "ip", + "name": "array", "optional": false, - "type": "string" + "type": "array" + }, + { + "by_ref": false, + "default": null, + "name": "replacements", + "optional": false, + "type": "array" } ], "return_type": "mixed", "variadic": null }, - "slug": "inet_pton", - "sub_area": "String" + "slug": "array_replace", + "sub_area": "Array" }, { - "area": "Filesystem", - "canonical_name": "disk_free_space", - "description": "Lowers `disk_free_space(path)` through the shared disk-space runtime helper.", + "area": "Array", + "canonical_name": "array_replace_recursive", + "description": "Replaces elements from passed arrays into the first array recursively.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_disk_free_space", - "codegen_line": 3149, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_replace_recursive", + "codegen_line": 1345, "notes": [ - "Lowers `disk_free_space(path)` through the shared disk-space runtime helper." + "Lowers `array_replace_recursive()` (recursive right-wins hash merge)." ], "runtime_helpers": [ - "__rt_disk_space" + "__rt_array_replace_recursive", + "__rt_assoc_diff_intersect" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_replace_recursive.rs", "sig_line": null }, - "name": "disk_free_space", + "name": "array_replace_recursive", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "directory", + "name": "array", "optional": false, - "type": "string" + "type": "array" + }, + { + "by_ref": false, + "default": null, + "name": "replacements", + "optional": false, + "type": "array" } ], - "return_type": "float", + "return_type": "mixed", "variadic": null }, - "slug": "disk_free_space", - "sub_area": "Filesystem" + "slug": "array_replace_recursive", + "sub_area": "Array" }, { - "area": "Filesystem", - "canonical_name": "disk_total_space", - "description": "Lowers `disk_total_space(path)` through the shared disk-space runtime helper.", + "area": "Array", + "canonical_name": "array_reverse", + "description": "Returns an array with the elements in reverse order.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_disk_total_space", - "codegen_line": 3157, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_reverse", + "codegen_line": 193, "notes": [ - "Lowers `disk_total_space(path)` through the shared disk-space runtime helper." - ], - "runtime_helpers": [ - "__rt_disk_space" + "Lowers `array_reverse()` for indexed arrays with 8-byte payload slots." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_reverse.rs", "sig_line": null }, - "name": "disk_total_space", + "name": "array_reverse", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "directory", + "name": "array", "optional": false, - "type": "string" + "type": "array" } ], - "return_type": "float", + "return_type": "array", "variadic": null }, - "slug": "disk_total_space", - "sub_area": "Filesystem" + "slug": "array_reverse", + "sub_area": "Array" }, { - "area": "Type", - "canonical_name": "boolval", - "description": "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`.", + "area": "Array", + "canonical_name": "array_search", + "description": "Searches the array for a given value and returns the first corresponding key if successful.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_boolval", - "codegen_line": 1064, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_search", + "codegen_line": 1761, "notes": [ - "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`." + "Lowers `array_search()` for indexed arrays with integer-like payloads." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_search.rs", "sig_line": null }, - "name": "boolval", + "name": "array_search", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "needle", "optional": false, "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false, + "type": "array" + }, + { + "by_ref": false, + "default": "false", + "name": "strict", + "optional": true, + "type": "bool" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "boolval", - "sub_area": "Casts" + "slug": "array_search", + "sub_area": "Array" }, { - "area": "Buffer", - "canonical_name": "buffer_free", - "description": "Lowers `buffer_free()` through the direct buffer opcode helper.", + "area": "Array", + "canonical_name": "array_shift", + "description": "Shifts an element off the beginning of array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/buffers.rs", - "codegen_function": "lower_buffer_free", - "codegen_line": 24, + "codegen_file": "src/codegen/lower_inst/builtins/arrays/shift.rs", + "codegen_function": "lower_array_shift", + "codegen_line": 23, "notes": [ - "Lowers `buffer_free()` through the direct buffer opcode helper." + "Lowers `array_shift()` for indexed arrays by compacting slots and boxing `T|null` as Mixed." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_shift.rs", "sig_line": null }, - "name": "buffer_free", + "name": "array_shift", "sig": { "params": [ { - "by_ref": false, + "by_ref": true, "default": null, - "name": "buffer", + "name": "array", "optional": false, - "type": "buffer" + "type": "array" } ], "return_type": "mixed", "variadic": null }, - "slug": "buffer_free", - "sub_area": "Buffer" + "slug": "array_shift", + "sub_area": "Array" }, { - "area": "Buffer", - "canonical_name": "buffer_len", - "description": "Lowers `buffer_len()` through the direct buffer opcode helper.", + "area": "Array", + "canonical_name": "array_slice", + "description": "Extracts a slice of an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/buffers.rs", - "codegen_function": "lower_buffer_len", - "codegen_line": 19, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_slice", + "codegen_line": 911, "notes": [ - "Lowers `buffer_len()` through the direct buffer opcode helper." + "Lowers `array_slice()` for indexed arrays with pointer-sized payload slots." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_slice.rs", "sig_line": null }, - "name": "buffer_len", + "name": "array_slice", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "buffer", + "name": "array", "optional": false, - "type": "buffer" + "type": "array" + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true, + "type": "int" } ], - "return_type": "int", + "return_type": "array", "variadic": null }, - "slug": "buffer_len", - "sub_area": "Buffer" + "slug": "array_slice", + "sub_area": "Array" }, { - "area": "Misc", - "canonical_name": "buffer_new", - "description": "", + "area": "Array", + "canonical_name": "array_splice", + "description": "Removes a portion of the array and replaces it with something else.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_splice", + "codegen_line": 956, + "notes": [ + "Lowers `array_splice()` by mutating an indexed source array and returning removed elements." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/array/array_splice.rs", "sig_line": null }, - "name": "buffer_new", + "name": "array_splice", "sig": { "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false, + "type": "array" + }, { "by_ref": false, "default": null, - "name": "length", + "name": "offset", "optional": false, "type": "int" + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true, + "type": "int" } ], - "return_type": "mixed", + "return_type": "array", "variadic": null }, - "slug": "buffer_new", - "sub_area": "Misc" + "slug": "array_splice", + "sub_area": "Array" }, { - "area": "Misc", - "canonical_name": "call_user_func", - "description": "", + "area": "Array", + "canonical_name": "array_sum", + "description": "Calculate the sum of values in an array.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_sum", + "codegen_line": 50, + "notes": [ + "Lowers `array_sum()` over supported indexed-array payloads." + ], + "runtime_helpers": [ + "__rt_array_product", + "__rt_array_sum" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/array/array_sum.rs", "sig_line": null }, - "name": "call_user_func", + "name": "array_sum", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "callback", + "name": "array", "optional": false, - "type": "callable" + "type": "array" } ], - "return_type": "mixed", - "variadic": "args" + "return_type": "int", + "variadic": null }, - "slug": "call_user_func", - "sub_area": "Misc" + "slug": "array_sum", + "sub_area": "Array" }, { - "area": "Misc", - "canonical_name": "call_user_func_array", - "description": "", + "area": "Array", + "canonical_name": "array_udiff", + "description": "Computes the difference of arrays using a callback comparator.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_udiff", + "codegen_line": 1703, + "notes": [ + "Lowers `array_udiff()`: keeps first-array elements not equal (per comparator) to any second-array element." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/array/array_udiff.rs", "sig_line": null }, - "name": "call_user_func_array", + "name": "array_udiff", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "callback", + "name": "array1", "optional": false, - "type": "callable" + "type": "array" }, { "by_ref": false, "default": null, - "name": "args", + "name": "array2", "optional": false, "type": "array" + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false, + "type": "callable" } ], - "return_type": "mixed", + "return_type": "array", "variadic": null }, - "slug": "call_user_func_array", - "sub_area": "Misc" + "slug": "array_udiff", + "sub_area": "Array" }, { - "area": "Math", - "canonical_name": "ceil", - "description": "Lowers `ceil()` for concrete integer-like and floating operands.", + "area": "Array", + "canonical_name": "array_uintersect", + "description": "Computes the intersection of arrays using a callback comparator.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_ceil", - "codegen_line": 75, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_uintersect", + "codegen_line": 1708, "notes": [ - "Lowers `ceil()` for concrete integer-like and floating operands." + "Lowers `array_uintersect()`: keeps first-array elements equal (per comparator) to some second-array element." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_uintersect.rs", "sig_line": null }, - "name": "ceil", + "name": "array_uintersect", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "array1", "optional": false, - "type": "float" + "type": "array" + }, + { + "by_ref": false, + "default": null, + "name": "array2", + "optional": false, + "type": "array" + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false, + "type": "callable" } ], - "return_type": "float", + "return_type": "array", "variadic": null }, - "slug": "ceil", - "sub_area": "Math" + "slug": "array_uintersect", + "sub_area": "Array" }, { - "area": "Filesystem", - "canonical_name": "chgrp", - "description": "Lowers `chgrp(path, group)` for integer GIDs and string group names.", + "area": "Array", + "canonical_name": "array_unique", + "description": "Removes duplicate values from an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_chgrp", - "codegen_line": 3835, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_unique", + "codegen_line": 207, "notes": [ - "Lowers `chgrp(path, group)` for integer GIDs and string group names." + "Lowers `array_unique()` for indexed arrays with 8-byte payload slots." ], "runtime_helpers": [ - "__rt_umask" + "__rt_array_filter", + "__rt_array_filter_refcounted" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_unique.rs", "sig_line": null }, - "name": "chgrp", + "name": "array_unique", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "array", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "group", - "optional": false, - "type": "int" + "type": "array" } ], - "return_type": "bool", + "return_type": "array", "variadic": null }, - "slug": "chgrp", - "sub_area": "Filesystem" + "slug": "array_unique", + "sub_area": "Array" }, { - "area": "Filesystem", - "canonical_name": "chmod", - "description": "Lowers `chmod(path, mode)` through the target-aware runtime helper.", + "area": "Array", + "canonical_name": "array_unshift", + "description": "Prepends one or more elements to the beginning of an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_chmod", - "codegen_line": 3825, + "codegen_file": "src/codegen/lower_inst/builtins/arrays/unshift.rs", + "codegen_function": "lower_array_unshift", + "codegen_line": 23, "notes": [ - "Lowers `chmod(path, mode)` through the target-aware runtime helper." + "Lowers `array_unshift()` by ensuring uniqueness, prepending one scalar value, and returning count." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_unshift.rs", "sig_line": null }, - "name": "chmod", + "name": "array_unshift", "sig": { "params": [ { - "by_ref": false, - "default": null, - "name": "filename", - "optional": false, - "type": "string" - }, - { - "by_ref": false, + "by_ref": true, "default": null, - "name": "permissions", + "name": "array", "optional": false, - "type": "int" + "type": "array" } ], - "return_type": "bool", - "variadic": null + "return_type": "int", + "variadic": "values" }, - "slug": "chmod", - "sub_area": "Filesystem" + "slug": "array_unshift", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "chop", - "description": "", + "area": "Array", + "canonical_name": "array_values", + "description": "Returns all the values of an array, re-indexed numerically.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/arrays/values.rs", + "codegen_function": "lower_array_values", + "codegen_line": 22, + "notes": [ + "Lowers `array_values()` for indexed arrays as an alias or associative arrays as a new values array." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/array/array_values.rs", "sig_line": null }, - "name": "chop", + "name": "array_values", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "array", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "characters", - "optional": true, - "type": "string" + "type": "array" } ], - "return_type": "string", + "return_type": "array", "variadic": null }, - "slug": "chop", - "sub_area": "String" + "slug": "array_values", + "sub_area": "Array" }, { - "area": "Filesystem", - "canonical_name": "chown", - "description": "Lowers `chown(path, owner)` for integer UIDs and string user names.", + "area": "Array", + "canonical_name": "array_walk", + "description": "Applies a user function to every member of an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_chown", - "codegen_line": 3830, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_walk", + "codegen_line": 779, "notes": [ - "Lowers `chown(path, owner)` for integer UIDs and string user names." + "Lowers `array_walk()` through the callback-driven runtime helper." ], "runtime_helpers": [ - "__rt_umask" + "__rt_array_walk" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_walk.rs", "sig_line": null }, - "name": "chown", + "name": "array_walk", "sig": { "params": [ { - "by_ref": false, + "by_ref": true, "default": null, - "name": "filename", + "name": "array", "optional": false, - "type": "string" + "type": "array" }, { "by_ref": false, "default": null, - "name": "user", + "name": "callback", "optional": false, - "type": "int" + "type": "callable" } ], - "return_type": "bool", + "return_type": "void", "variadic": null }, - "slug": "chown", - "sub_area": "Filesystem" + "slug": "array_walk", + "sub_area": "Array" }, { - "area": "Filesystem", - "canonical_name": "chdir", - "description": "Lowers `chdir(path)` through the target-aware runtime helper.", + "area": "Array", + "canonical_name": "array_walk_recursive", + "description": "Applies a user function recursively to every member of an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_chdir", - "codegen_line": 3795, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_array_walk_recursive", + "codegen_line": 1584, "notes": [ - "Lowers `chdir(path)` through the target-aware runtime helper." + "Lowers `array_walk_recursive()`: invokes the callback on each scalar leaf of a (possibly nested)", + "array, descending into array-valued elements. Returns void; leaves are passed as 8-byte scalars." ], "runtime_helpers": [ - "__rt_chdir", - "__rt_copy", - "__rt_glob", - "__rt_scandir", - "__rt_tempnam" + "__rt_array_udiff_uintersect", + "__rt_array_walk_recursive" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/array_walk_recursive.rs", "sig_line": null }, - "name": "chdir", + "name": "array_walk_recursive", "sig": { "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false, + "type": "array" + }, { "by_ref": false, "default": null, - "name": "directory", + "name": "callback", "optional": false, - "type": "string" + "type": "callable" } ], - "return_type": "bool", + "return_type": "void", "variadic": null }, - "slug": "chdir", - "sub_area": "Filesystem" + "slug": "array_walk_recursive", + "sub_area": "Array" }, { - "area": "Date", - "canonical_name": "checkdate", - "description": "Lowers `checkdate(month, day, year)` through the shared Gregorian-validation runtime helper.", + "area": "Array", + "canonical_name": "arsort", + "description": "Sorts an array in descending order and maintains index association.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_checkdate", - "codegen_line": 163, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_arsort", + "codegen_line": 1094, "notes": [ - "Lowers `checkdate(month, day, year)` through the shared Gregorian-validation runtime helper.", - "Marshals the three integers into the leading ABI argument registers (unboxing any boxed", - "`Mixed`/`Union` argument), then calls `__rt_checkdate`, which returns PHP `true`/`false` in the", - "integer result register for a valid/invalid date." + "Lowers `arsort()` for indexed integer arrays through the descending value-sort wrapper." ], "runtime_helpers": [ - "__rt_checkdate", - "__rt_getdate" + "__rt_arsort", + "__rt_krsort", + "__rt_ksort", + "__rt_natcasesort", + "__rt_natsort" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/arsort.rs", "sig_line": null }, - "name": "checkdate", + "name": "arsort", "sig": { "params": [ { - "by_ref": false, - "default": null, - "name": "month", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "day", - "optional": false, - "type": "int" - }, - { - "by_ref": false, + "by_ref": true, "default": null, - "name": "year", + "name": "array", "optional": false, - "type": "int" + "type": "array" } ], "return_type": "bool", "variadic": null }, - "slug": "checkdate", - "sub_area": "Date" + "slug": "arsort", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "chr", - "description": "Lowers `chr()` by converting an integer code point into a one-byte string.", + "area": "Math", + "canonical_name": "asin", + "description": "Returns the arcsine of a number in radians.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_chr", - "codegen_line": 858, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers `chr()` by converting an integer code point into a one-byte string." - ], - "runtime_helpers": [ - "__rt_chr" + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/asin.rs", "sig_line": null }, - "name": "chr", + "name": "asin", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "codepoint", + "name": "num", "optional": false, - "type": "int" + "type": "float" } ], - "return_type": "string", + "return_type": "float", "variadic": null }, - "slug": "chr", - "sub_area": "String" + "slug": "asin", + "sub_area": "Math" }, { - "area": "Filesystem", - "canonical_name": "clearstatcache", - "description": "Lowers `clearstatcache(...)` as an ordered no-op after EIR operand evaluation.", + "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, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_clearstatcache", - "codegen_line": 4935, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_asort", + "codegen_line": 1089, "notes": [ - "Lowers `clearstatcache(...)` as an ordered no-op after EIR operand evaluation." + "Lowers `asort()` for indexed integer arrays through the value-sort runtime wrapper." ], "runtime_helpers": [ - "__rt_is_dir" + "__rt_arsort", + "__rt_asort", + "__rt_krsort", + "__rt_ksort", + "__rt_natcasesort", + "__rt_natsort" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/asort.rs", "sig_line": null }, - "name": "clearstatcache", + "name": "asort", "sig": { "params": [ { - "by_ref": false, - "default": null, - "name": "clear_realpath_cache", - "optional": true, - "type": "bool" - }, - { - "by_ref": false, + "by_ref": true, "default": null, - "name": "filename", - "optional": true, - "type": "string" + "name": "array", + "optional": false, + "type": "array" } ], - "return_type": "void", + "return_type": "bool", "variadic": null }, - "slug": "clearstatcache", - "sub_area": "Filesystem" + "slug": "asort", + "sub_area": "Array" }, { "area": "Math", - "canonical_name": "clamp", - "description": "Lowers numeric `clamp(value, min, max)` calls with PHP-compatible bound checks.", + "canonical_name": "atan", + "description": "Returns the arctangent of a number in radians.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_clamp", - "codegen_line": 80, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers numeric `clamp(value, min, max)` calls with PHP-compatible bound checks." + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/atan.rs", "sig_line": null }, - "name": "clamp", + "name": "atan", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "min", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "max", + "name": "num", "optional": false, - "type": "int" + "type": "float" } ], - "return_type": "string", + "return_type": "float", "variadic": null }, - "slug": "clamp", + "slug": "atan", "sub_area": "Math" }, { - "area": "Filesystem", - "canonical_name": "copy", - "description": "Lowers `copy(source, dest)` through the target-aware runtime helper.", + "area": "Math", + "canonical_name": "atan2", + "description": "Returns the arc tangent of two variables.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_copy", - "codegen_line": 3800, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_atan2", + "codegen_line": 35, "notes": [ - "Lowers `copy(source, dest)` through the target-aware runtime helper." - ], - "runtime_helpers": [ - "__rt_copy", - "__rt_glob", - "__rt_scandir", - "__rt_tempnam" + "Lowers `atan2()` using the C ABI argument order `y, x`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/atan2.rs", "sig_line": null }, - "name": "copy", + "name": "atan2", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "from", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "to", + "name": "y", "optional": false, - "type": "string" + "type": "float" }, { "by_ref": false, "default": null, - "name": "context", + "name": "x", "optional": false, - "type": "mixed" + "type": "float" } ], - "return_type": "bool", + "return_type": "float", "variadic": null }, - "slug": "copy", - "sub_area": "Filesystem" + "slug": "atan2", + "sub_area": "Math" }, { - "area": "Math", - "canonical_name": "cos", - "description": "", + "area": "String", + "canonical_name": "base64_decode", + "description": "Decodes a Base64-encoded string back into its original data.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, + "notes": [ + "Lowers a one-argument string builtin that directly delegates to a runtime helper." + ], + "runtime_helpers": [ + "__rt_grapheme_strrev", + "__rt_strcopy" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/string/base64_decode.rs", "sig_line": null }, - "name": "cos", + "name": "base64_decode", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "string", "optional": false, - "type": "float" + "type": "string" } ], - "return_type": "float", + "return_type": "string", "variadic": null }, - "slug": "cos", - "sub_area": "Math" + "slug": "base64_decode", + "sub_area": "String" }, { - "area": "Math", - "canonical_name": "cosh", - "description": "", + "area": "String", + "canonical_name": "base64_encode", + "description": "Encodes binary data into a Base64 string.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, + "notes": [ + "Lowers a one-argument string builtin that directly delegates to a runtime helper." + ], + "runtime_helpers": [ + "__rt_grapheme_strrev", + "__rt_strcopy" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/string/base64_encode.rs", "sig_line": null }, - "name": "cosh", + "name": "base64_encode", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "string", "optional": false, - "type": "float" + "type": "string" } ], - "return_type": "float", + "return_type": "string", "variadic": null }, - "slug": "cosh", - "sub_area": "Math" + "slug": "base64_encode", + "sub_area": "String" }, { - "area": "Array", - "canonical_name": "count", - "description": "Lowers `count(array)` for concrete array values by reading the runtime length header.", + "area": "Filesystem", + "canonical_name": "basename", + "description": "Returns the trailing name component of a path.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_count", - "codegen_line": 917, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_basename", + "codegen_line": 4536, "notes": [ - "Lowers `count(array)` for concrete array values by reading the runtime length header." - ], - "runtime_helpers": [ - "__rt_mixed_count" + "Lowers `basename(path, suffix?)` through the target-aware runtime helper." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/basename.rs", "sig_line": null }, - "name": "count", + "name": "basename", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "path", "optional": false, - "type": "array" + "type": "string" }, { "by_ref": false, - "default": null, - "name": "mode", + "default": "''", + "name": "suffix", "optional": true, - "type": "int" + "type": "string" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "count", - "sub_area": "Array" + "slug": "basename", + "sub_area": "Filesystem" }, { "area": "String", - "canonical_name": "crc32", - "description": "Lowers `crc32(string)` through the shared checksum runtime helper.", + "canonical_name": "bin2hex", + "description": "Converts binary data into its hexadecimal string representation.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_crc32", - "codegen_line": 348, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `crc32(string)` through the shared checksum runtime helper." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_crc32", - "__rt_hash", - "__rt_md5", - "__rt_sha1" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/bin2hex.rs", "sig_line": null }, - "name": "crc32", + "name": "bin2hex", "sig": { "params": [ { @@ -3407,276 +3315,315 @@ "type": "string" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "crc32", + "slug": "bin2hex", "sub_area": "String" }, { "area": "Type", - "canonical_name": "ctype_alnum", - "description": "Lowers `ctype_alnum(string)` by checking every byte against ASCII alpha or digit ranges.", + "canonical_name": "boolval", + "description": "Returns the boolean value of a variable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/ctype.rs", - "codegen_function": "lower_ctype_alnum", - "codegen_line": 30, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_boolval", + "codegen_line": 586, "notes": [ - "Lowers `ctype_alnum(string)` by checking every byte against ASCII alpha or digit ranges." + "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/boolval.rs", "sig_line": null }, - "name": "ctype_alnum", + "name": "boolval", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "text", + "name": "value", "optional": false, - "type": "string" + "type": "mixed" } ], "return_type": "bool", "variadic": null }, - "slug": "ctype_alnum", - "sub_area": "Ctype" + "slug": "boolval", + "sub_area": "Casts" }, { - "area": "Type", - "canonical_name": "ctype_alpha", - "description": "Lowers `ctype_alpha(string)` by checking every byte against ASCII alpha ranges.", + "area": "Buffer", + "canonical_name": "buffer_free", + "description": "Lowers `buffer_free()` through the direct buffer opcode helper.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/ctype.rs", - "codegen_function": "lower_ctype_alpha", - "codegen_line": 20, + "codegen_file": "src/codegen/lower_inst/builtins/buffers.rs", + "codegen_function": "lower_buffer_free", + "codegen_line": 24, "notes": [ - "Lowers `ctype_alpha(string)` by checking every byte against ASCII alpha ranges." + "Lowers `buffer_free()` through the direct buffer opcode helper." ], "runtime_helpers": [], "sig_arm": null, "sig_file": null, "sig_line": null }, - "name": "ctype_alpha", + "name": "buffer_free", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "text", + "name": "buffer", "optional": false, - "type": "string" + "type": "buffer" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "ctype_alpha", - "sub_area": "Ctype" + "slug": "buffer_free", + "sub_area": "Buffer" }, { - "area": "Type", - "canonical_name": "ctype_digit", - "description": "Lowers `ctype_digit(string)` by checking every byte against the ASCII digit range.", + "area": "Buffer", + "canonical_name": "buffer_len", + "description": "Lowers `buffer_len()` through the direct buffer opcode helper.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/ctype.rs", - "codegen_function": "lower_ctype_digit", - "codegen_line": 25, + "codegen_file": "src/codegen/lower_inst/builtins/buffers.rs", + "codegen_function": "lower_buffer_len", + "codegen_line": 19, "notes": [ - "Lowers `ctype_digit(string)` by checking every byte against the ASCII digit range." + "Lowers `buffer_len()` through the direct buffer opcode helper." ], "runtime_helpers": [], "sig_arm": null, "sig_file": null, "sig_line": null }, - "name": "ctype_digit", + "name": "buffer_len", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "text", + "name": "buffer", "optional": false, - "type": "string" + "type": "buffer" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "ctype_digit", - "sub_area": "Ctype" + "slug": "buffer_len", + "sub_area": "Buffer" }, { - "area": "Type", - "canonical_name": "ctype_space", - "description": "Lowers `ctype_space(string)` by checking every byte against PHP's ASCII whitespace set.", + "area": "Misc", + "canonical_name": "buffer_new", + "description": "", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/ctype.rs", - "codegen_function": "lower_ctype_space", - "codegen_line": 35, - "notes": [ - "Lowers `ctype_space(string)` by checking every byte against PHP's ASCII whitespace set." - ], + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], "runtime_helpers": [], "sig_arm": null, "sig_file": null, "sig_line": null }, - "name": "ctype_space", + "name": "buffer_new", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "text", + "name": "length", "optional": false, - "type": "string" + "type": "int" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "ctype_space", - "sub_area": "Ctype" + "slug": "buffer_new", + "sub_area": "Misc" }, { - "area": "Date", - "canonical_name": "date", - "description": "Lowers `date(format, timestamp?)` through the shared formatter runtime helper.", + "area": "Array", + "canonical_name": "call_user_func", + "description": "Calls a callback with the given arguments.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_date", - "codegen_line": 22, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_call_user_func_builtin_escape", + "codegen_line": 37, "notes": [ - "Lowers `date(format, timestamp?)` through the shared formatter runtime helper." + "Rejects `call_user_func*` calls that escaped the dedicated EIR callback lowering path." ], "runtime_helpers": [ - "__rt_date", - "__rt_gmdate" + "__rt_array_product", + "__rt_array_sum" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/call_user_func.rs", "sig_line": null }, - "name": "date", + "name": "call_user_func", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "format", + "name": "callback", "optional": false, - "type": "string" + "type": "callable" + } + ], + "return_type": "mixed", + "variadic": "args" + }, + "slug": "call_user_func", + "sub_area": "Array" + }, + { + "area": "Array", + "canonical_name": "call_user_func_array", + "description": "Calls a callback with an array of parameters.", + "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_call_user_func_builtin_escape", + "codegen_line": 37, + "notes": [ + "Rejects `call_user_func*` calls that escaped the dedicated EIR callback lowering path." + ], + "runtime_helpers": [ + "__rt_array_product", + "__rt_array_sum" + ], + "sig_arm": null, + "sig_file": "src/builtins/callables/call_user_func_array.rs", + "sig_line": null + }, + "name": "call_user_func_array", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false, + "type": "callable" }, { "by_ref": false, "default": null, - "name": "timestamp", - "optional": true, - "type": "int" + "name": "args", + "optional": false, + "type": "array" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "date", - "sub_area": "Date" + "slug": "call_user_func_array", + "sub_area": "Array" }, { - "area": "Date", - "canonical_name": "date_default_timezone_get", - "description": "Lowers `date_default_timezone_get()` through the shared runtime helper.", + "area": "Math", + "canonical_name": "ceil", + "description": "Rounds a number up to the nearest integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_date_default_timezone_get", - "codegen_line": 70, + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_ceil", + "codegen_line": 75, "notes": [ - "Lowers `date_default_timezone_get()` through the shared runtime helper.", - "Takes no arguments; `__rt_date_default_timezone_get` returns the stored timezone", - "identifier (or the literal `\"UTC\"` when none was set) in the string-result registers." - ], - "runtime_helpers": [ - "__rt_date_default_timezone_get", - "__rt_date_default_timezone_set" + "Lowers `ceil()` for concrete integer-like and floating operands." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/ceil.rs", "sig_line": null }, - "name": "date_default_timezone_get", + "name": "ceil", "sig": { - "params": [], - "return_type": "string", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "float" + } + ], + "return_type": "float", "variadic": null }, - "slug": "date_default_timezone_get", - "sub_area": "Date" + "slug": "ceil", + "sub_area": "Math" }, { - "area": "Date", - "canonical_name": "date_default_timezone_set", - "description": "Lowers `date_default_timezone_set(timezoneId)` through the shared runtime helper.", + "area": "Filesystem", + "canonical_name": "chdir", + "description": "Changes the current directory.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_date_default_timezone_set", - "codegen_line": 84, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_chdir", + "codegen_line": 4438, "notes": [ - "Lowers `date_default_timezone_set(timezoneId)` through the shared runtime helper.", - "Materializes the identifier string into the registers the helper reads (ptr/len in", - "`x1`/`x2` on ARM64, `rax`/`rdx` on x86_64), then `__rt_date_default_timezone_set`", - "applies it via libc `putenv`+`tzset` and returns PHP `true` in the integer-result register." + "Lowers `chdir(path)` through the target-aware runtime helper." ], "runtime_helpers": [ - "__rt_date_default_timezone_set", - "__rt_microtime", - "__rt_microtime_mixed", - "__rt_microtime_str" + "__rt_chdir", + "__rt_copy", + "__rt_glob", + "__rt_scandir", + "__rt_tempnam" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/chdir.rs", "sig_line": null }, - "name": "date_default_timezone_set", + "name": "chdir", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "timezoneId", + "name": "directory", "optional": false, "type": "string" } @@ -3684,87 +3631,102 @@ "return_type": "bool", "variadic": null }, - "slug": "date_default_timezone_set", - "sub_area": "Date" + "slug": "chdir", + "sub_area": "Filesystem" }, { - "area": "Misc", - "canonical_name": "define", - "description": "Defines a named constant at runtime.", + "area": "Date", + "canonical_name": "checkdate", + "description": "Validates a Gregorian date.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_define", - "codegen_line": 549, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_checkdate", + "codegen_line": 163, "notes": [ - "Lowers `define(\"NAME\", value)` with the legacy duplicate-name runtime guard." + "Lowers `checkdate(month, day, year)` through the shared Gregorian-validation runtime helper.", + "Marshals the three integers into the leading ABI argument registers (unboxing any boxed", + "`Mixed`/`Union` argument), then calls `__rt_checkdate`, which returns PHP `true`/`false` in the", + "integer result register for a valid/invalid date." + ], + "runtime_helpers": [ + "__rt_checkdate", + "__rt_getdate" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/checkdate.rs", "sig_line": null }, - "name": "define", + "name": "checkdate", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "constant_name", + "name": "month", "optional": false, - "type": "string" + "type": "int" }, { "by_ref": false, "default": null, - "name": "value", + "name": "day", "optional": false, - "type": "mixed" + "type": "int" }, { "by_ref": false, "default": null, - "name": "case_insensitive", + "name": "year", "optional": false, - "type": "bool" + "type": "int" } ], "return_type": "bool", "variadic": null }, - "slug": "define", - "sub_area": "Constants" + "slug": "checkdate", + "sub_area": "Date" }, { - "area": "Misc", - "canonical_name": "defined", - "description": "Checks whether a given named constant exists.", + "area": "Filesystem", + "canonical_name": "chgrp", + "description": "Changes file group.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_defined", - "codegen_line": 744, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_chgrp", + "codegen_line": 4478, "notes": [ - "Lowers `defined(\"NAME\")` for compile-time string constant names." + "Lowers `chgrp(path, group)` for integer GIDs and string group names." + ], + "runtime_helpers": [ + "__rt_umask" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/chgrp.rs", "sig_line": null }, - "name": "defined", + "name": "chgrp", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "constant_name", + "name": "filename", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "group", "optional": false, "type": "string" } @@ -3772,962 +3734,945 @@ "return_type": "bool", "variadic": null }, - "slug": "defined", - "sub_area": "Constants" + "slug": "chgrp", + "sub_area": "Filesystem" }, { - "area": "Math", - "canonical_name": "deg2rad", - "description": "Lowers `deg2rad()` by multiplying with `PI / 180`.", + "area": "Filesystem", + "canonical_name": "chmod", + "description": "Changes file mode.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/libm.rs", - "codegen_function": "lower_deg2rad", - "codegen_line": 75, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_chmod", + "codegen_line": 4468, "notes": [ - "Lowers `deg2rad()` by multiplying with `PI / 180`." + "Lowers `chmod(path, mode)` through the target-aware runtime helper." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/chmod.rs", "sig_line": null }, - "name": "deg2rad", + "name": "chmod", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "filename", "optional": false, - "type": "float" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "permissions", + "optional": false, + "type": "int" } ], - "return_type": "float", + "return_type": "bool", "variadic": null }, - "slug": "deg2rad", - "sub_area": "Math" + "slug": "chmod", + "sub_area": "Filesystem" }, { - "area": "Process", - "canonical_name": "die", - "description": "", + "area": "String", + "canonical_name": "chop", + "description": "Alias of rtrim: strips whitespace (or other characters) from the end of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_trim_like", + "codegen_line": 112, + "notes": [ + "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/string/chop.rs", "sig_line": null }, - "name": "die", + "name": "chop", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "status", + "name": "string", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "' \\n\\r\\t\\x0b\\x0c\\x00'", + "name": "characters", "optional": true, - "type": "int" + "type": "string" } ], - "return_type": "void", + "return_type": "string", "variadic": null }, - "slug": "die", - "sub_area": "Process" + "slug": "chop", + "sub_area": "String" }, { "area": "Filesystem", - "canonical_name": "dirname", - "description": "Lowers `dirname(path, levels?)` through the target-aware runtime helper.", + "canonical_name": "chown", + "description": "Changes file owner.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_dirname", - "codegen_line": 3932, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_chown", + "codegen_line": 4473, "notes": [ - "Lowers `dirname(path, levels?)` through the target-aware runtime helper." + "Lowers `chown(path, owner)` for integer UIDs and string user names." ], "runtime_helpers": [ - "__rt_dirname", - "__rt_dirname_levels" + "__rt_umask" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/chown.rs", "sig_line": null }, - "name": "dirname", + "name": "chown", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "path", + "name": "filename", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "levels", - "optional": true, - "type": "int" + "name": "user", + "optional": false, + "type": "string" } ], - "return_type": "string", + "return_type": "bool", "variadic": null }, - "slug": "dirname", + "slug": "chown", "sub_area": "Filesystem" }, { - "area": "Misc", - "canonical_name": "empty", - "description": "Determines whether a variable is considered empty.", + "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, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_empty", - "codegen_line": 1096, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_chr", + "codegen_line": 858, "notes": [ - "Lowers `empty()` for concrete scalar and array-like operands." + "Lowers `chr()` by converting an integer code point into a one-byte string." ], "runtime_helpers": [ - "__rt_mixed_is_empty" + "__rt_chr" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/chr.rs", "sig_line": null }, - "name": "empty", + "name": "chr", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "codepoint", "optional": false, - "type": "mixed" + "type": "int" } ], - "return_type": "bool", + "return_type": "string", "variadic": null }, - "slug": "empty", - "sub_area": "Variable" + "slug": "chr", + "sub_area": "String" }, { - "area": "Process", - "canonical_name": "exec", - "description": "Lowers `exec(command)` by capturing shell stdout through the shared runtime helper.", + "area": "Math", + "canonical_name": "clamp", + "description": "Clamps a value to be within a specified range.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_exec", - "codegen_line": 690, + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_clamp", + "codegen_line": 80, "notes": [ - "Lowers `exec(command)` by capturing shell stdout through the shared runtime helper." + "Lowers numeric `clamp(value, min, max)` calls with PHP-compatible bound checks." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/clamp.rs", "sig_line": null }, - "name": "exec", + "name": "clamp", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "command", + "name": "value", "optional": false, - "type": "string" + "type": "int" }, { - "by_ref": true, + "by_ref": false, "default": null, - "name": "output", + "name": "min", "optional": false, - "type": "array" + "type": "int" }, { - "by_ref": true, + "by_ref": false, "default": null, - "name": "result_code", + "name": "max", "optional": false, "type": "int" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "exec", - "sub_area": "Process" + "slug": "clamp", + "sub_area": "Math" }, { - "area": "Process", - "canonical_name": "exit", - "description": "", + "area": "Class", + "canonical_name": "class_alias", + "description": "Creates an alias for a class.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_class_alias", + "codegen_line": 41, + "notes": [ + "Lowers the defensive `class_alias()` fallback that remains after AOT alias extraction." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/callables/class_alias.rs", "sig_line": null }, - "name": "exit", + "name": "class_alias", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "status", + "name": "class", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "alias", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", "optional": true, - "type": "int" + "type": "bool" } ], - "return_type": "void", + "return_type": "bool", "variadic": null }, - "slug": "exit", - "sub_area": "Process" + "slug": "class_alias", + "sub_area": "Class" }, { - "area": "Math", - "canonical_name": "exp", - "description": "", + "area": "Class", + "canonical_name": "class_attribute_args", + "description": "Returns the constructor arguments of a named attribute applied to a class.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", + "codegen_function": "lower_class_attribute_args", + "codegen_line": 52, + "notes": [ + "Lowers `class_attribute_args(class, attr)` into an indexed Mixed array." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/system/class_attribute_args.rs", "sig_line": null }, - "name": "exp", + "name": "class_attribute_args", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "class_name", "optional": false, - "type": "float" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "attribute_name", + "optional": false, + "type": "string" } ], - "return_type": "float", + "return_type": "array", "variadic": null }, - "slug": "exp", - "sub_area": "Math" + "slug": "class_attribute_args", + "sub_area": "Attributes" }, { - "area": "String", - "canonical_name": "explode", - "description": "Lowers `explode(delimiter, string)` into the shared string-array splitter helper.", + "area": "Class", + "canonical_name": "class_attribute_names", + "description": "Returns the list of attribute names applied to a class.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_explode", - "codegen_line": 151, + "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", + "codegen_function": "lower_class_attribute_names", + "codegen_line": 36, "notes": [ - "Lowers `explode(delimiter, string)` into the shared string-array splitter helper." - ], - "runtime_helpers": [ - "__rt_explode", - "__rt_sscanf" + "Lowers `class_attribute_names(class)` into an indexed string array." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/class_attribute_names.rs", "sig_line": null }, - "name": "explode", + "name": "class_attribute_names", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "separator", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "string", + "name": "class_name", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "limit", - "optional": true, - "type": "int" } ], "return_type": "array", "variadic": null }, - "slug": "explode", - "sub_area": "String" + "slug": "class_attribute_names", + "sub_area": "Attributes" }, { - "area": "IO", - "canonical_name": "fclose", - "description": "Lowers `fclose(stream)` after validating and unboxing the stream handle.", + "area": "Class", + "canonical_name": "class_exists", + "description": "Checks whether the given class has been defined.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fclose", - "codegen_line": 2488, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_class_like_exists", + "codegen_line": 293, "notes": [ - "Lowers `fclose(stream)` after validating and unboxing the stream handle." + "Lowers AOT class/interface/enum existence checks for literal names." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/class_exists.rs", "sig_line": null }, - "name": "fclose", + "name": "class_exists", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "class", "optional": false, - "type": "resource" + "type": "string" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" } ], "return_type": "bool", "variadic": null }, - "slug": "fclose", - "sub_area": "IO" + "slug": "class_exists", + "sub_area": "Class" }, { - "area": "IO", - "canonical_name": "fdatasync", - "description": "Lowers `fdatasync(stream)` through the shared fd data-sync runtime helper.", + "area": "Class", + "canonical_name": "class_get_attributes", + "description": "Returns an array of ReflectionAttribute objects for all attributes of a class.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fdatasync", - "codegen_line": 3083, + "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", + "codegen_function": "lower_class_get_attributes", + "codegen_line": 68, "notes": [ - "Lowers `fdatasync(stream)` through the shared fd data-sync runtime helper." - ], - "runtime_helpers": [ - "__rt_fdatasync" + "Lowers `class_get_attributes(class)` into an array of `ReflectionAttribute` objects." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/class_get_attributes.rs", "sig_line": null }, - "name": "fdatasync", + "name": "class_get_attributes", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "class_name", "optional": false, - "type": "resource" + "type": "string" } ], - "return_type": "bool", + "return_type": "array", "variadic": null }, - "slug": "fdatasync", - "sub_area": "IO" + "slug": "class_get_attributes", + "sub_area": "Attributes" }, { - "area": "Math", - "canonical_name": "fdiv", - "description": "Lowers `fdiv()` for concrete integer-like and floating operands.", + "area": "Class", + "canonical_name": "class_implements", + "description": "Returns the interfaces which are implemented by the given class or its parents.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/binary.rs", - "codegen_function": "lower_fdiv", - "codegen_line": 60, + "codegen_file": "src/codegen/lower_inst/builtins/class_relations.rs", + "codegen_function": "lower_class_relation", + "codegen_line": 32, "notes": [ - "Lowers `fdiv()` for concrete integer-like and floating operands." + "Lowers `class_implements()`, `class_parents()`, and `class_uses()` from static metadata." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/class_implements.rs", "sig_line": null }, - "name": "fdiv", + "name": "class_implements", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num1", + "name": "object_or_class", "optional": false, - "type": "float" + "type": "mixed" }, { "by_ref": false, - "default": null, - "name": "num2", - "optional": false, - "type": "float" + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" } ], - "return_type": "float", + "return_type": "mixed", "variadic": null }, - "slug": "fdiv", - "sub_area": "Math" + "slug": "class_implements", + "sub_area": "Class" }, { - "area": "IO", - "canonical_name": "feof", - "description": "Lowers `feof(stream)` through the runtime EOF-flag table helper.", + "area": "Class", + "canonical_name": "class_parents", + "description": "Returns the parent classes of the given class.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_feof", - "codegen_line": 2900, + "codegen_file": "src/codegen/lower_inst/builtins/class_relations.rs", + "codegen_function": "lower_class_relation", + "codegen_line": 32, "notes": [ - "Lowers `feof(stream)` through the runtime EOF-flag table helper." - ], - "runtime_helpers": [ - "__rt_feof", - "__rt_user_wrapper_ftell" + "Lowers `class_implements()`, `class_parents()`, and `class_uses()` from static metadata." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/class_parents.rs", "sig_line": null }, - "name": "feof", + "name": "class_parents", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "object_or_class", "optional": false, - "type": "resource" + "type": "mixed" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "feof", - "sub_area": "IO" + "slug": "class_parents", + "sub_area": "Class" }, { - "area": "IO", - "canonical_name": "fflush", - "description": "Lowers `fflush(stream)` through the shared fd flush runtime helper.", + "area": "Class", + "canonical_name": "class_uses", + "description": "Returns the traits used by the given class.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fflush", - "codegen_line": 3045, + "codegen_file": "src/codegen/lower_inst/builtins/class_relations.rs", + "codegen_function": "lower_class_relation", + "codegen_line": 32, "notes": [ - "Lowers `fflush(stream)` through the shared fd flush runtime helper." - ], - "runtime_helpers": [ - "__rt_fflush" + "Lowers `class_implements()`, `class_parents()`, and `class_uses()` from static metadata." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/class_uses.rs", "sig_line": null }, - "name": "fflush", + "name": "class_uses", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "object_or_class", "optional": false, - "type": "resource" + "type": "mixed" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "fflush", - "sub_area": "IO" + "slug": "class_uses", + "sub_area": "Class" }, { - "area": "IO", - "canonical_name": "fgetc", - "description": "Lowers `fgetc(stream)` and boxes the one-byte string or PHP false result.", + "area": "Filesystem", + "canonical_name": "clearstatcache", + "description": "Clears file status cache.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fgetc", - "codegen_line": 2759, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_clearstatcache", + "codegen_line": 5578, "notes": [ - "Lowers `fgetc(stream)` and boxes the one-byte string or PHP false result." + "Lowers `clearstatcache(...)` as an ordered no-op after EIR operand evaluation." ], "runtime_helpers": [ - "__rt_fgetc", - "__rt_fgetcsv" + "__rt_is_dir" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/clearstatcache.rs", "sig_line": null }, - "name": "fgetc", + "name": "clearstatcache", "sig": { "params": [ { "by_ref": false, - "default": null, - "name": "stream", - "optional": false, - "type": "resource" + "default": "false", + "name": "clear_realpath_cache", + "optional": true, + "type": "bool" + }, + { + "by_ref": false, + "default": "''", + "name": "filename", + "optional": true, + "type": "string" } ], - "return_type": "mixed", + "return_type": "void", "variadic": null }, - "slug": "fgetc", - "sub_area": "IO" + "slug": "clearstatcache", + "sub_area": "Filesystem" }, { "area": "IO", - "canonical_name": "fgetcsv", - "description": "Lowers `fgetcsv(stream, separator?, enclosure?)` through the CSV row runtime helper.", + "canonical_name": "closedir", + "description": "Closes directory handle.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fgetcsv", - "codegen_line": 2772, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_closedir", + "codegen_line": 3572, "notes": [ - "Lowers `fgetcsv(stream, separator?, enclosure?)` through the CSV row runtime helper." + "Lowers `closedir(dir_handle)` for libc, glob, and userspace-wrapper handles." ], "runtime_helpers": [ - "__rt_fgetcsv", - "__rt_fputcsv" + "__rt_closedir", + "__rt_rewinddir", + "__rt_user_wrapper_dir_closedir", + "__rt_user_wrapper_dir_rewinddir" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/closedir.rs", "sig_line": null }, - "name": "fgetcsv", + "name": "closedir", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "dir_handle", "optional": false, "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "separator", - "optional": true, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "enclosure", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "escape", - "optional": false, - "type": "string" } ], - "return_type": "array", + "return_type": "void", "variadic": null }, - "slug": "fgetcsv", + "slug": "closedir", "sub_area": "IO" }, { - "area": "IO", - "canonical_name": "fgets", - "description": "Lowers `fgets(stream)` through the shared line-read runtime helper.", + "area": "Filesystem", + "canonical_name": "copy", + "description": "Copies a file.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fgets", - "codegen_line": 2746, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_copy", + "codegen_line": 4443, "notes": [ - "Lowers `fgets(stream)` through the shared line-read runtime helper." + "Lowers `copy(source, dest)` through the target-aware runtime helper." ], "runtime_helpers": [ - "__rt_fgetc", - "__rt_fgets" + "__rt_copy", + "__rt_glob", + "__rt_scandir", + "__rt_tempnam" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/copy.rs", "sig_line": null }, - "name": "fgets", + "name": "copy", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "from", "optional": false, - "type": "resource" + "type": "string" }, { "by_ref": false, "default": null, - "name": "length", + "name": "to", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "fgets", - "sub_area": "IO" + "slug": "copy", + "sub_area": "Filesystem" }, { - "area": "IO", - "canonical_name": "fscanf", - "description": "Lowers `fscanf(stream, format)` through `__rt_fgets` and `__rt_sscanf`.", + "area": "Math", + "canonical_name": "cos", + "description": "Returns the cosine of a number (radians).", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fscanf", - "codegen_line": 2717, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers `fscanf(stream, format)` through `__rt_fgets` and `__rt_sscanf`." - ], - "runtime_helpers": [ - "__rt_fgets", - "__rt_sscanf" + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/cos.rs", "sig_line": null }, - "name": "fscanf", + "name": "cos", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", - "optional": false, - "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "format", + "name": "num", "optional": false, - "type": "string" + "type": "float" } ], - "return_type": "array", - "variadic": "vars" + "return_type": "float", + "variadic": null }, - "slug": "fscanf", - "sub_area": "IO" + "slug": "cos", + "sub_area": "Math" }, { - "area": "IO", - "canonical_name": "flock", - "description": "Lowers `flock(stream, operation, would_block?)` through the libc flock wrapper.", + "area": "Math", + "canonical_name": "cosh", + "description": "Returns the hyperbolic cosine of a number.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_flock", - "codegen_line": 3088, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers `flock(stream, operation, would_block?)` through the libc flock wrapper." + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/cosh.rs", "sig_line": null }, - "name": "flock", + "name": "cosh", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", - "optional": false, - "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "operation", + "name": "num", "optional": false, - "type": "int" - }, - { - "by_ref": true, - "default": null, - "name": "would_block", - "optional": true, - "type": "bool" + "type": "float" } ], - "return_type": "bool", + "return_type": "float", "variadic": null }, - "slug": "flock", - "sub_area": "IO" + "slug": "cosh", + "sub_area": "Math" }, { - "area": "IO", - "canonical_name": "file", - "description": "Lowers `file(path)` through the target-aware runtime line-array helper.", + "area": "Array", + "canonical_name": "count", + "description": "Counts all elements in an array or Countable object.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_file", - "codegen_line": 3460, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_count", + "codegen_line": 439, "notes": [ - "Lowers `file(path)` through the target-aware runtime line-array helper." + "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.", + "Handles Array/AssocArray (reads length directly from the runtime header), Mixed/Union", + "(delegates to `__rt_mixed_count`), and Countable Object (calls the object's `count`", + "method via intrinsic or dynamic dispatch)." ], "runtime_helpers": [ - "__rt_file", - "__rt_realpath" + "__rt_mixed_count" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/count.rs", "sig_line": null }, - "name": "file", + "name": "count", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "value", "optional": false, - "type": "string" + "type": "array" }, { "by_ref": false, - "default": null, - "name": "flags", - "optional": false, + "default": "0", + "name": "mode", + "optional": true, "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "context", - "optional": false, - "type": "mixed" } ], - "return_type": "array", + "return_type": "int", "variadic": null }, - "slug": "file", - "sub_area": "IO" + "slug": "count", + "sub_area": "Array" }, { - "area": "Filesystem", - "canonical_name": "fileatime", - "description": "Lowers `fileatime(path)` and boxes the runtime integer-or-false result.", + "area": "String", + "canonical_name": "crc32", + "description": "Calculates the CRC32 polynomial of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fileatime", - "codegen_line": 4825, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_crc32", + "codegen_line": 348, "notes": [ - "Lowers `fileatime(path)` and boxes the runtime integer-or-false result." + "Lowers `crc32(string)` through the shared checksum runtime helper." ], "runtime_helpers": [ - "__rt_fileatime", - "__rt_filectime", - "__rt_fileowner", - "__rt_fileperms" + "__rt_crc32", + "__rt_hash", + "__rt_md5", + "__rt_sha1" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/crc32.rs", "sig_line": null }, - "name": "fileatime", + "name": "crc32", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "string", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "fileatime", - "sub_area": "Filesystem" + "slug": "crc32", + "sub_area": "String" }, { - "area": "Filesystem", - "canonical_name": "filectime", - "description": "Lowers `filectime(path)` and boxes the runtime integer-or-false result.", + "area": "Type", + "canonical_name": "ctype_alnum", + "description": "Checks if all characters in the string are alphanumeric.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_filectime", - "codegen_line": 4833, + "codegen_file": "src/codegen/lower_inst/builtins/ctype.rs", + "codegen_function": "lower_ctype_alnum", + "codegen_line": 30, "notes": [ - "Lowers `filectime(path)` and boxes the runtime integer-or-false result." - ], - "runtime_helpers": [ - "__rt_filectime", - "__rt_filegroup", - "__rt_fileowner", - "__rt_fileperms" + "Lowers `ctype_alnum(string)` by checking every byte against ASCII alpha or digit ranges." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ctype_alnum.rs", "sig_line": null }, - "name": "filectime", + "name": "ctype_alnum", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "text", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "filectime", - "sub_area": "Filesystem" + "slug": "ctype_alnum", + "sub_area": "Ctype" }, { - "area": "Filesystem", - "canonical_name": "file_exists", - "description": "Lowers `file_exists(path)` through the target-aware runtime stat helper.", + "area": "Type", + "canonical_name": "ctype_alpha", + "description": "Checks if all characters in the string are alphabetic.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_file_exists", - "codegen_line": 3756, + "codegen_file": "src/codegen/lower_inst/builtins/ctype.rs", + "codegen_function": "lower_ctype_alpha", + "codegen_line": 20, "notes": [ - "Lowers `file_exists(path)` through the target-aware runtime stat helper." - ], - "runtime_helpers": [ - "__rt_mkdir", - "__rt_unlink" + "Lowers `ctype_alpha(string)` by checking every byte against ASCII alpha ranges." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ctype_alpha.rs", "sig_line": null }, - "name": "file_exists", + "name": "ctype_alpha", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "text", "optional": false, "type": "string" } @@ -4735,942 +4680,793 @@ "return_type": "bool", "variadic": null }, - "slug": "file_exists", - "sub_area": "Filesystem" + "slug": "ctype_alpha", + "sub_area": "Ctype" }, { - "area": "Filesystem", - "canonical_name": "filegroup", - "description": "Lowers `filegroup(path)` and boxes the runtime integer-or-false result.", + "area": "Type", + "canonical_name": "ctype_digit", + "description": "Checks if all characters in the string are digits.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_filegroup", - "codegen_line": 4857, + "codegen_file": "src/codegen/lower_inst/builtins/ctype.rs", + "codegen_function": "lower_ctype_digit", + "codegen_line": 25, "notes": [ - "Lowers `filegroup(path)` and boxes the runtime integer-or-false result." - ], - "runtime_helpers": [ - "__rt_filegroup", - "__rt_fileinode", - "__rt_filetype", - "__rt_stat_array" + "Lowers `ctype_digit(string)` by checking every byte against the ASCII digit range." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ctype_digit.rs", "sig_line": null }, - "name": "filegroup", + "name": "ctype_digit", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "text", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "filegroup", - "sub_area": "Filesystem" + "slug": "ctype_digit", + "sub_area": "Ctype" }, { - "area": "IO", - "canonical_name": "file_get_contents", - "description": "Lowers `file_get_contents(path)` and boxes the runtime string-or-false result.", + "area": "Type", + "canonical_name": "ctype_space", + "description": "Checks if all characters in the string are whitespace characters.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_file_get_contents", - "codegen_line": 39, + "codegen_file": "src/codegen/lower_inst/builtins/ctype.rs", + "codegen_function": "lower_ctype_space", + "codegen_line": 35, "notes": [ - "Lowers `file_get_contents(path)` and boxes the runtime string-or-false result." - ], - "runtime_helpers": [ - "__rt_file_get_contents_maybe_url", - "__rt_php_input" + "Lowers `ctype_space(string)` by checking every byte against PHP's ASCII whitespace set." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ctype_space.rs", "sig_line": null }, - "name": "file_get_contents", + "name": "ctype_space", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "text", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "use_include_path", - "optional": false, - "type": "bool" - }, - { - "by_ref": false, - "default": null, - "name": "context", - "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "offset", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": false, - "type": "int" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "file_get_contents", - "sub_area": "IO" + "slug": "ctype_space", + "sub_area": "Ctype" }, { - "area": "Filesystem", - "canonical_name": "fileinode", - "description": "Lowers `fileinode(path)` and boxes the runtime integer-or-false result.", + "area": "Date", + "canonical_name": "date", + "description": "Formats a local time/date.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fileinode", - "codegen_line": 4865, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_date", + "codegen_line": 22, "notes": [ - "Lowers `fileinode(path)` and boxes the runtime integer-or-false result." + "Lowers `date(format, timestamp?)` through the shared formatter runtime helper." ], "runtime_helpers": [ - "__rt_fileinode", - "__rt_filetype", - "__rt_lstat_array", - "__rt_stat_array" + "__rt_date", + "__rt_gmdate" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/date.rs", "sig_line": null }, - "name": "fileinode", + "name": "date", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "format", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true, + "type": "int" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "fileinode", - "sub_area": "Filesystem" + "slug": "date", + "sub_area": "Date" }, { - "area": "IO", - "canonical_name": "file_put_contents", - "description": "Lowers `file_put_contents(path, data)` through the target-aware runtime writer.", + "area": "Date", + "canonical_name": "date_default_timezone_get", + "description": "Gets the default timezone.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_file_put_contents", - "codegen_line": 3502, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_date_default_timezone_get", + "codegen_line": 70, "notes": [ - "Lowers `file_put_contents(path, data)` through the target-aware runtime writer." + "Lowers `date_default_timezone_get()` through the shared runtime helper.", + "Takes no arguments; `__rt_date_default_timezone_get` returns the stored timezone", + "identifier (or the literal `\"UTC\"` when none was set) in the string-result registers." ], "runtime_helpers": [ - "__rt_file_put_contents", - "__rt_file_put_contents_maybe_phar" + "__rt_date_default_timezone_get", + "__rt_date_default_timezone_set" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/date_default_timezone_get.rs", "sig_line": null }, - "name": "file_put_contents", + "name": "date_default_timezone_get", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "filename", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "data", - "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": "0", - "name": "flags", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": "null", - "name": "context", - "optional": true, - "type": "mixed" - } - ], - "return_type": "int", + "params": [], + "return_type": "string", "variadic": null }, - "slug": "file_put_contents", - "sub_area": "IO" + "slug": "date_default_timezone_get", + "sub_area": "Date" }, { - "area": "Filesystem", - "canonical_name": "fileowner", - "description": "Lowers `fileowner(path)` and boxes the runtime integer-or-false result.", + "area": "Date", + "canonical_name": "date_default_timezone_set", + "description": "Sets the default timezone.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fileowner", - "codegen_line": 4849, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_date_default_timezone_set", + "codegen_line": 84, "notes": [ - "Lowers `fileowner(path)` and boxes the runtime integer-or-false result." + "Lowers `date_default_timezone_set(timezoneId)` through the shared runtime helper.", + "Materializes the identifier string into the registers the helper reads (ptr/len in", + "`x1`/`x2` on ARM64, `rax`/`rdx` on x86_64), then `__rt_date_default_timezone_set`", + "applies it via libc `putenv`+`tzset` and returns PHP `true` in the integer-result register." ], "runtime_helpers": [ - "__rt_filegroup", - "__rt_fileinode", - "__rt_fileowner" + "__rt_date_default_timezone_set", + "__rt_microtime", + "__rt_microtime_mixed", + "__rt_microtime_str" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/date_default_timezone_set.rs", "sig_line": null }, - "name": "fileowner", + "name": "date_default_timezone_set", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "timezoneId", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "fileowner", - "sub_area": "Filesystem" + "slug": "date_default_timezone_set", + "sub_area": "Date" }, { - "area": "Filesystem", - "canonical_name": "fileperms", - "description": "Lowers `fileperms(path)` and boxes the runtime integer-or-false result.", + "area": "Misc", + "canonical_name": "define", + "description": "Defines a named constant at runtime.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fileperms", - "codegen_line": 4841, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_define", + "codegen_line": 82, "notes": [ - "Lowers `fileperms(path)` and boxes the runtime integer-or-false result." - ], - "runtime_helpers": [ - "__rt_filegroup", - "__rt_fileinode", - "__rt_fileowner", - "__rt_fileperms" + "Lowers `define(\"NAME\", value)` with the duplicate-name runtime guard." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/define.rs", "sig_line": null }, - "name": "fileperms", + "name": "define", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "constant_name", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "fileperms", - "sub_area": "Filesystem" + "slug": "define", + "sub_area": "Constants" }, { - "area": "Filesystem", - "canonical_name": "filetype", - "description": "Lowers `filetype(path)` and boxes the runtime string-or-false result.", + "area": "Misc", + "canonical_name": "defined", + "description": "Checks whether a given named constant exists.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_filetype", - "codegen_line": 4873, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_defined", + "codegen_line": 261, "notes": [ - "Lowers `filetype(path)` and boxes the runtime string-or-false result." - ], - "runtime_helpers": [ - "__rt_filetype", - "__rt_lstat_array", - "__rt_stat_array" + "Lowers `defined(\"NAME\")` for compile-time string constant names." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/defined.rs", "sig_line": null }, - "name": "filetype", + "name": "defined", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "constant_name", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "filetype", - "sub_area": "Filesystem" + "slug": "defined", + "sub_area": "Constants" }, { - "area": "Filesystem", - "canonical_name": "filemtime", - "description": "Lowers `filemtime(path)` through the target-aware runtime stat helper.", + "area": "Math", + "canonical_name": "deg2rad", + "description": "Converts a degree value to radians.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_filemtime", - "codegen_line": 4789, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_deg2rad", + "codegen_line": 75, "notes": [ - "Lowers `filemtime(path)` through the target-aware runtime stat helper." - ], - "runtime_helpers": [ - "__rt_filemtime", - "__rt_link", - "__rt_linkinfo", - "__rt_readlink", - "__rt_symlink" + "Lowers `deg2rad()` by multiplying with `PI / 180`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/deg2rad.rs", "sig_line": null }, - "name": "filemtime", + "name": "deg2rad", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "num", "optional": false, - "type": "string" + "type": "float" } ], - "return_type": "int", + "return_type": "float", "variadic": null }, - "slug": "filemtime", - "sub_area": "Filesystem" + "slug": "deg2rad", + "sub_area": "Math" }, { - "area": "Filesystem", - "canonical_name": "filesize", - "description": "Lowers `filesize(path)` through the target-aware runtime stat helper.", + "area": "Process", + "canonical_name": "die", + "description": "", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_filesize", - "codegen_line": 4781, - "notes": [ - "Lowers `filesize(path)` through the target-aware runtime stat helper." - ], - "runtime_helpers": [ - "__rt_filemtime", - "__rt_link", - "__rt_linkinfo", - "__rt_symlink" - ], + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], "sig_arm": null, "sig_file": null, "sig_line": null }, - "name": "filesize", + "name": "die", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", - "optional": false, - "type": "string" + "name": "status", + "optional": true, + "type": "int" } ], - "return_type": "int", + "return_type": "void", "variadic": null }, - "slug": "filesize", - "sub_area": "Filesystem" + "slug": "die", + "sub_area": "Process" }, { - "area": "Type", - "canonical_name": "floatval", - "description": "Lowers `floatval()` for concrete scalar operands.", + "area": "Filesystem", + "canonical_name": "dirname", + "description": "Returns a parent directory's path.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_floatval", - "codegen_line": 1034, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_dirname", + "codegen_line": 4575, "notes": [ - "Lowers `floatval()` for concrete scalar operands." + "Lowers `dirname(path, levels?)` through the target-aware runtime helper." ], "runtime_helpers": [ - "__rt_str_to_number" + "__rt_dirname", + "__rt_dirname_levels" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/dirname.rs", "sig_line": null }, - "name": "floatval", + "name": "dirname", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "path", "optional": false, - "type": "mixed" + "type": "string" + }, + { + "by_ref": false, + "default": "1", + "name": "levels", + "optional": true, + "type": "int" } ], - "return_type": "float", + "return_type": "string", "variadic": null }, - "slug": "floatval", - "sub_area": "Casts" + "slug": "dirname", + "sub_area": "Filesystem" }, { - "area": "Math", - "canonical_name": "floor", - "description": "Lowers `floor()` for concrete integer-like and floating operands.", + "area": "Filesystem", + "canonical_name": "disk_free_space", + "description": "Returns available space on filesystem or disk partition.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_floor", - "codegen_line": 70, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_disk_free_space", + "codegen_line": 3370, "notes": [ - "Lowers `floor()` for concrete integer-like and floating operands." + "Lowers `disk_free_space(path)` through the shared disk-space runtime helper." + ], + "runtime_helpers": [ + "__rt_disk_space" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/disk_free_space.rs", "sig_line": null }, - "name": "floor", + "name": "disk_free_space", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "directory", "optional": false, - "type": "float" + "type": "string" } ], "return_type": "float", "variadic": null }, - "slug": "floor", - "sub_area": "Math" + "slug": "disk_free_space", + "sub_area": "Filesystem" }, { "area": "Filesystem", - "canonical_name": "fnmatch", - "description": "Lowers `fnmatch(pattern, filename, flags?)` through the target-aware runtime helper.", + "canonical_name": "disk_total_space", + "description": "Returns the total size of a filesystem or disk partition.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fnmatch", - "codegen_line": 3960, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_disk_total_space", + "codegen_line": 3378, "notes": [ - "Lowers `fnmatch(pattern, filename, flags?)` through the target-aware runtime helper." + "Lowers `disk_total_space(path)` through the shared disk-space runtime helper." + ], + "runtime_helpers": [ + "__rt_disk_space" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/disk_total_space.rs", "sig_line": null }, - "name": "fnmatch", + "name": "disk_total_space", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pattern", + "name": "directory", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "filename", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": true, - "type": "int" } ], - "return_type": "bool", + "return_type": "float", "variadic": null }, - "slug": "fnmatch", + "slug": "disk_total_space", "sub_area": "Filesystem" }, { - "area": "Math", - "canonical_name": "fmod", - "description": "Lowers `fmod()` for concrete integer-like and floating operands.", + "area": "Misc", + "canonical_name": "empty", + "description": "Determines whether a variable is considered empty.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/binary.rs", - "codegen_function": "lower_fmod", - "codegen_line": 85, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_empty", + "codegen_line": 618, "notes": [ - "Lowers `fmod()` for concrete integer-like and floating operands." + "Lowers `empty()` for concrete scalar and array-like operands." + ], + "runtime_helpers": [ + "__rt_mixed_is_empty" ], - "runtime_helpers": [], "sig_arm": null, "sig_file": null, "sig_line": null }, - "name": "fmod", + "name": "empty", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num1", - "optional": false, - "type": "float" - }, - { - "by_ref": false, - "default": null, - "name": "num2", + "name": "value", "optional": false, - "type": "float" + "type": "mixed" } ], - "return_type": "float", + "return_type": "bool", "variadic": null }, - "slug": "fmod", - "sub_area": "Math" + "slug": "empty", + "sub_area": "Variable" }, { - "area": "IO", - "canonical_name": "fopen", - "description": "Lowers `fopen(filename, mode)` and boxes stream resources or PHP false.", + "area": "Class", + "canonical_name": "enum_exists", + "description": "Checks if the enum has been defined.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fopen", - "codegen_line": 233, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_class_like_exists", + "codegen_line": 293, "notes": [ - "Lowers `fopen(filename, mode)` and boxes stream resources or PHP false." - ], - "runtime_helpers": [ - "__rt_tmpfile" + "Lowers AOT class/interface/enum existence checks for literal names." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/enum_exists.rs", "sig_line": null }, - "name": "fopen", + "name": "enum_exists", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "mode", + "name": "enum", "optional": false, "type": "string" }, { "by_ref": false, - "default": null, - "name": "use_include_path", + "default": "true", + "name": "autoload", "optional": true, "type": "bool" - }, - { - "by_ref": false, - "default": null, - "name": "context", - "optional": true, - "type": "mixed" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "fopen", - "sub_area": "IO" + "slug": "enum_exists", + "sub_area": "Class" }, { - "area": "IO", - "canonical_name": "fpassthru", - "description": "Lowers `fpassthru(stream)` through the remaining-bytes stream runtime helper.", + "area": "Process", + "canonical_name": "exec", + "description": "Executes an external program and returns the last line of output.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fpassthru", - "codegen_line": 2806, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_exec", + "codegen_line": 690, "notes": [ - "Lowers `fpassthru(stream)` through the remaining-bytes stream runtime helper." - ], - "runtime_helpers": [ - "__rt_feof", - "__rt_fpassthru" + "Lowers `exec(command)` by capturing shell stdout through the shared runtime helper." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/exec.rs", "sig_line": null }, - "name": "fpassthru", + "name": "exec", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "command", "optional": false, - "type": "resource" + "type": "string" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "fpassthru", - "sub_area": "IO" + "slug": "exec", + "sub_area": "Process" }, { - "area": "IO", - "canonical_name": "fprintf", - "description": "Lowers `fprintf(stream, format, values...)` as `sprintf()` plus stream write.", + "area": "Process", + "canonical_name": "exit", + "description": "", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fprintf", - "codegen_line": 2641, - "notes": [ - "Lowers `fprintf(stream, format, values...)` as `sprintf()` plus stream write." - ], - "runtime_helpers": [ - "__rt_sprintf" - ], + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], "sig_arm": null, "sig_file": null, "sig_line": null }, - "name": "fprintf", + "name": "exit", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", - "optional": false, - "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "format", - "optional": false, - "type": "string" + "name": "status", + "optional": true, + "type": "int" } ], - "return_type": "int", - "variadic": "values" + "return_type": "void", + "variadic": null }, - "slug": "fprintf", - "sub_area": "IO" + "slug": "exit", + "sub_area": "Process" }, { - "area": "IO", - "canonical_name": "fputcsv", - "description": "Lowers `fputcsv(stream, fields, separator?, enclosure?)` for string arrays.", + "area": "Math", + "canonical_name": "exp", + "description": "Returns e raised to the power of a number.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fputcsv", - "codegen_line": 2784, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers `fputcsv(stream, fields, separator?, enclosure?)` for string arrays." - ], - "runtime_helpers": [ - "__rt_fputcsv" + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/exp.rs", "sig_line": null }, - "name": "fputcsv", + "name": "exp", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", - "optional": false, - "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "fields", + "name": "num", "optional": false, - "type": "array" - }, - { - "by_ref": false, - "default": "','", - "name": "separator", - "optional": true, - "type": "string" - }, - { - "by_ref": false, - "default": "'\"'", - "name": "enclosure", - "optional": true, - "type": "string" - }, - { - "by_ref": false, - "default": "'\\\\'", - "name": "escape", - "optional": true, - "type": "string" - }, - { - "by_ref": false, - "default": "'\\n'", - "name": "eol", - "optional": true, - "type": "string" + "type": "float" } ], - "return_type": "int", + "return_type": "float", "variadic": null }, - "slug": "fputcsv", - "sub_area": "IO" + "slug": "exp", + "sub_area": "Math" }, { - "area": "IO", - "canonical_name": "fread", - "description": "Lowers `fread(stream, length)` using the shared runtime file-read helper.", + "area": "String", + "canonical_name": "explode", + "description": "Splits a string by a separator into an array of substrings.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fread", - "codegen_line": 2595, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_explode", + "codegen_line": 151, "notes": [ - "Lowers `fread(stream, length)` using the shared runtime file-read helper." + "Lowers `explode(delimiter, string)` into the shared string-array splitter helper." ], "runtime_helpers": [ - "__rt_fread" + "__rt_explode", + "__rt_sscanf" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/explode.rs", "sig_line": null }, - "name": "fread", + "name": "explode", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "separator", "optional": false, - "type": "resource" + "type": "string" }, { "by_ref": false, "default": null, - "name": "length", + "name": "string", "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "PHP_INT_MAX", + "name": "limit", + "optional": true, "type": "int" } ], - "return_type": "string", + "return_type": "array", "variadic": null }, - "slug": "fread", - "sub_area": "IO" + "slug": "explode", + "sub_area": "String" }, { - "area": "Filesystem", - "canonical_name": "readfile", - "description": "Lowers `readfile(path)` and boxes the runtime byte-count-or-false result.", + "area": "IO", + "canonical_name": "fclose", + "description": "Closes an open file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_readfile", - "codegen_line": 193, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fclose", + "codegen_line": 2707, "notes": [ - "Lowers `readfile(path)` and boxes the runtime byte-count-or-false result." + "Lowers `fclose(stream)` after validating and unboxing the stream handle." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fclose.rs", "sig_line": null }, - "name": "readfile", + "name": "fclose", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "stream", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "use_include_path", - "optional": false, - "type": "bool" - }, - { - "by_ref": false, - "default": null, - "name": "context", - "optional": false, - "type": "mixed" + "type": "resource" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "readfile", - "sub_area": "Filesystem" + "slug": "fclose", + "sub_area": "IO" }, { "area": "IO", - "canonical_name": "fstat", - "description": "Lowers `fstat(stream)` and boxes the runtime stat array or PHP false result.", + "canonical_name": "fdatasync", + "description": "Synchronizes data (but not meta-data) to file.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fstat", - "codegen_line": 4896, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fdatasync", + "codegen_line": 3304, "notes": [ - "Lowers `fstat(stream)` and boxes the runtime stat array or PHP false result." + "Lowers `fdatasync(stream)` through the shared fd data-sync runtime helper." ], "runtime_helpers": [ - "__rt_fstat_array" + "__rt_fdatasync" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fdatasync.rs", "sig_line": null }, - "name": "fstat", + "name": "fdatasync", "sig": { "params": [ { @@ -5681,87 +5477,80 @@ "type": "resource" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "fstat", + "slug": "fdatasync", "sub_area": "IO" }, { - "area": "IO", - "canonical_name": "fseek", - "description": "Lowers `fseek(stream, offset, whence?)` and clears EOF state on success.", + "area": "Math", + "canonical_name": "fdiv", + "description": "Divides two numbers, according to IEEE 754.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fseek", - "codegen_line": 2951, + "codegen_file": "src/codegen/lower_inst/builtins/math/binary.rs", + "codegen_function": "lower_fdiv", + "codegen_line": 67, "notes": [ - "Lowers `fseek(stream, offset, whence?)` and clears EOF state on success." + "Lowers `fdiv()` for concrete integer-like and floating operands." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/fdiv.rs", "sig_line": null }, - "name": "fseek", + "name": "fdiv", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "num1", "optional": false, - "type": "resource" + "type": "float" }, { "by_ref": false, "default": null, - "name": "offset", + "name": "num2", "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "whence", - "optional": true, - "type": "int" + "type": "float" } ], - "return_type": "int", + "return_type": "float", "variadic": null }, - "slug": "fseek", - "sub_area": "IO" + "slug": "fdiv", + "sub_area": "Math" }, { "area": "IO", - "canonical_name": "fsync", - "description": "Lowers `fsync(stream)` through the shared fd sync runtime helper.", + "canonical_name": "feof", + "description": "Tests for end-of-file on a file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fsync", - "codegen_line": 3040, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_feof", + "codegen_line": 3121, "notes": [ - "Lowers `fsync(stream)` through the shared fd sync runtime helper." + "Lowers `feof(stream)` through the runtime EOF-flag table helper." ], "runtime_helpers": [ - "__rt_fflush", - "__rt_fsync" + "__rt_feof", + "__rt_user_wrapper_ftell" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/feof.rs", "sig_line": null }, - "name": "fsync", + "name": "feof", "sig": { "params": [ { @@ -5775,32 +5564,32 @@ "return_type": "bool", "variadic": null }, - "slug": "fsync", + "slug": "feof", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ftell", - "description": "Lowers `ftell(stream)` as `lseek(fd, 0, SEEK_CUR)`.", + "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_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_ftell", - "codegen_line": 2912, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fflush", + "codegen_line": 3266, "notes": [ - "Lowers `ftell(stream)` as `lseek(fd, 0, SEEK_CUR)`." + "Lowers `fflush(stream)` through the shared fd flush runtime helper." ], "runtime_helpers": [ - "__rt_user_wrapper_ftell" + "__rt_fflush" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fflush.rs", "sig_line": null }, - "name": "ftell", + "name": "fflush", "sig": { "params": [ { @@ -5811,33 +5600,36 @@ "type": "resource" } ], - "return_type": "int", + "return_type": "bool", "variadic": null }, - "slug": "ftell", + "slug": "fflush", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ftruncate", - "description": "Lowers `ftruncate(stream, size)` through the shared fd truncate runtime helper.", + "canonical_name": "fgetc", + "description": "Gets a character from the given file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_ftruncate", - "codegen_line": 2989, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fgetc", + "codegen_line": 2980, "notes": [ - "Lowers `ftruncate(stream, size)` through the shared fd truncate runtime helper." + "Lowers `fgetc(stream)` and boxes the one-byte string or PHP false result." + ], + "runtime_helpers": [ + "__rt_fgetc", + "__rt_fgetcsv" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fgetc.rs", "sig_line": null }, - "name": "ftruncate", + "name": "fgetc", "sig": { "params": [ { @@ -5846,143 +5638,138 @@ "name": "stream", "optional": false, "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "size", - "optional": false, - "type": "int" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "ftruncate", + "slug": "fgetc", "sub_area": "IO" }, { - "area": "Class", - "canonical_name": "class_alias", - "description": "Lowers the defensive `class_alias()` fallback that remains after AOT alias extraction.", + "area": "IO", + "canonical_name": "fgetcsv", + "description": "Gets line from file pointer and parse for CSV fields.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", - "codegen_function": "lower_class_alias", - "codegen_line": 41, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fgetcsv", + "codegen_line": 2993, "notes": [ - "Lowers the defensive `class_alias()` fallback that remains after AOT alias extraction." + "Lowers `fgetcsv(stream, separator?, enclosure?)` through the CSV row runtime helper." + ], + "runtime_helpers": [ + "__rt_fgetcsv", + "__rt_fputcsv" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fgetcsv.rs", "sig_line": null }, - "name": "class_alias", + "name": "fgetcsv", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "class", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" }, { "by_ref": false, - "default": null, - "name": "alias", - "optional": false, - "type": "string" + "default": "null", + "name": "length", + "optional": true, + "type": "int" }, { "by_ref": false, - "default": null, - "name": "autoload", + "default": "','", + "name": "separator", "optional": true, - "type": "bool" + "type": "string" } ], - "return_type": "bool", + "return_type": "array", "variadic": null }, - "slug": "class_alias", - "sub_area": "Class" + "slug": "fgetcsv", + "sub_area": "IO" }, { - "area": "Class", - "canonical_name": "class_attribute_args", - "description": "Lowers `class_attribute_args(class, attr)` into an indexed Mixed array.", + "area": "IO", + "canonical_name": "fgets", + "description": "Gets line from file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/attributes.rs", - "codegen_function": "lower_class_attribute_args", - "codegen_line": 52, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fgets", + "codegen_line": 2967, "notes": [ - "Lowers `class_attribute_args(class, attr)` into an indexed Mixed array." + "Lowers `fgets(stream)` through the shared line-read runtime helper." + ], + "runtime_helpers": [ + "__rt_fgetc", + "__rt_fgets" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fgets.rs", "sig_line": null }, - "name": "class_attribute_args", + "name": "fgets", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "class_name", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "attribute_name", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" } ], - "return_type": "array", + "return_type": "mixed", "variadic": null }, - "slug": "class_attribute_args", - "sub_area": "Attributes" + "slug": "fgets", + "sub_area": "IO" }, { - "area": "Class", - "canonical_name": "class_attribute_names", - "description": "Lowers `class_attribute_names(class)` into an indexed string array.", + "area": "IO", + "canonical_name": "file", + "description": "Reads an entire file into an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/attributes.rs", - "codegen_function": "lower_class_attribute_names", - "codegen_line": 36, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_file", + "codegen_line": 3685, "notes": [ - "Lowers `class_attribute_names(class)` into an indexed string array." + "Lowers `file(path)` through the target-aware runtime line-array helper." + ], + "runtime_helpers": [ + "__rt_file", + "__rt_realpath" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/file.rs", "sig_line": null }, - "name": "class_attribute_names", + "name": "file", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "class_name", + "name": "filename", "optional": false, "type": "string" } @@ -5990,78 +5777,79 @@ "return_type": "array", "variadic": null }, - "slug": "class_attribute_names", - "sub_area": "Attributes" + "slug": "file", + "sub_area": "IO" }, { - "area": "Class", - "canonical_name": "class_exists", - "description": "Checks whether the given class has been defined.", + "area": "Filesystem", + "canonical_name": "file_exists", + "description": "Checks whether a file or directory exists.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_file_exists", + "codegen_line": 4399, + "notes": [ + "Lowers `file_exists(path)` through the target-aware runtime stat helper." + ], + "runtime_helpers": [ + "__rt_mkdir", + "__rt_unlink" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/file_exists.rs", "sig_line": null }, - "name": "class_exists", + "name": "file_exists", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "class", + "name": "filename", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "autoload", - "optional": true, - "type": "bool" } ], "return_type": "bool", "variadic": null }, - "slug": "class_exists", - "sub_area": "Class" + "slug": "file_exists", + "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "class_get_attributes", - "description": "Lowers `class_get_attributes(class)` into an array of `ReflectionAttribute` objects.", + "area": "IO", + "canonical_name": "file_get_contents", + "description": "Reads an entire file into a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/attributes.rs", - "codegen_function": "lower_class_get_attributes", - "codegen_line": 68, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_file_get_contents", + "codegen_line": 39, "notes": [ - "Lowers `class_get_attributes(class)` into an array of `ReflectionAttribute` objects." + "Lowers `file_get_contents(path)` and boxes the runtime string-or-false result." + ], + "runtime_helpers": [ + "__rt_file_get_contents_maybe_url", + "__rt_php_input" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/file_get_contents.rs", "sig_line": null }, - "name": "class_get_attributes", + "name": "file_get_contents", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "class_name", + "name": "filename", "optional": false, "type": "string" } @@ -6069,476 +5857,513 @@ "return_type": "mixed", "variadic": null }, - "slug": "class_get_attributes", - "sub_area": "Attributes" + "slug": "file_get_contents", + "sub_area": "IO" }, { - "area": "Class", - "canonical_name": "class_implements", - "description": "", + "area": "IO", + "canonical_name": "file_put_contents", + "description": "Writes data to a file.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_file_put_contents", + "codegen_line": 3727, + "notes": [ + "Lowers `file_put_contents(path, data)` through the target-aware runtime writer." + ], + "runtime_helpers": [ + "__rt_file_put_contents", + "__rt_file_put_contents_maybe_phar" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/file_put_contents.rs", "sig_line": null }, - "name": "class_implements", + "name": "file_put_contents", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "filename", "optional": false, - "type": "mixed" + "type": "string" }, { "by_ref": false, "default": null, - "name": "autoload", - "optional": true, - "type": "bool" + "name": "data", + "optional": false, + "type": "string" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "class_implements", - "sub_area": "Class" + "slug": "file_put_contents", + "sub_area": "IO" }, { - "area": "Class", - "canonical_name": "class_parents", - "description": "", + "area": "Filesystem", + "canonical_name": "fileatime", + "description": "Gets last access time of file.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fileatime", + "codegen_line": 5468, + "notes": [ + "Lowers `fileatime(path)` and boxes the runtime integer-or-false result." + ], + "runtime_helpers": [ + "__rt_fileatime", + "__rt_filectime", + "__rt_fileowner", + "__rt_fileperms" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/fileatime.rs", "sig_line": null }, - "name": "class_parents", + "name": "fileatime", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "filename", "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "autoload", - "optional": true, - "type": "bool" + "type": "string" } ], "return_type": "mixed", "variadic": null }, - "slug": "class_parents", - "sub_area": "Class" + "slug": "fileatime", + "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "class_uses", - "description": "", + "area": "Filesystem", + "canonical_name": "filectime", + "description": "Gets inode change time of file.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_filectime", + "codegen_line": 5476, + "notes": [ + "Lowers `filectime(path)` and boxes the runtime integer-or-false result." + ], + "runtime_helpers": [ + "__rt_filectime", + "__rt_filegroup", + "__rt_fileowner", + "__rt_fileperms" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/filectime.rs", "sig_line": null }, - "name": "class_uses", + "name": "filectime", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "filename", "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "autoload", - "optional": true, - "type": "bool" + "type": "string" } ], "return_type": "mixed", "variadic": null }, - "slug": "class_uses", - "sub_area": "Class" + "slug": "filectime", + "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "enum_exists", - "description": "", + "area": "Filesystem", + "canonical_name": "filegroup", + "description": "Gets file group.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_filegroup", + "codegen_line": 5500, + "notes": [ + "Lowers `filegroup(path)` and boxes the runtime integer-or-false result." + ], + "runtime_helpers": [ + "__rt_filegroup", + "__rt_fileinode", + "__rt_filetype", + "__rt_stat_array" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/filegroup.rs", "sig_line": null }, - "name": "enum_exists", + "name": "filegroup", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "enum", + "name": "filename", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "autoload", - "optional": true, - "type": "bool" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "enum_exists", - "sub_area": "Class" + "slug": "filegroup", + "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "function_exists", - "description": "Lowers `function_exists(\"name\")` for compile-time string names.", + "area": "Filesystem", + "canonical_name": "fileinode", + "description": "Gets file inode.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_function_exists", - "codegen_line": 759, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fileinode", + "codegen_line": 5508, "notes": [ - "Lowers `function_exists(\"name\")` for compile-time string names.", - "Recognizes user functions, externs, catalog builtins, and the date/time procedural aliases", - "that `name_resolver` desugars (including the injected timezone-introspection prelude", - "functions). The aliases are matched through `is_date_procedural_alias` rather than the catalog", - "because their call sites are rewritten before codegen, so they never reach the builtin catalog", - "yet must still report as existing to match PHP." + "Lowers `fileinode(path)` and boxes the runtime integer-or-false result." + ], + "runtime_helpers": [ + "__rt_fileinode", + "__rt_filetype", + "__rt_lstat_array", + "__rt_stat_array" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fileinode.rs", "sig_line": null }, - "name": "function_exists", + "name": "fileinode", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "function", + "name": "filename", "optional": false, "type": "string" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "function_exists", - "sub_area": "Class" + "slug": "fileinode", + "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "get_class", - "description": "", + "area": "Filesystem", + "canonical_name": "filemtime", + "description": "Gets file modification time.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_filemtime", + "codegen_line": 5432, + "notes": [ + "Lowers `filemtime(path)` through the target-aware runtime stat helper." + ], + "runtime_helpers": [ + "__rt_filemtime", + "__rt_link", + "__rt_linkinfo", + "__rt_readlink", + "__rt_symlink" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/filemtime.rs", "sig_line": null }, - "name": "get_class", + "name": "filemtime", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object", - "optional": true, - "type": "object" + "name": "filename", + "optional": false, + "type": "string" } ], - "return_type": "string", + "return_type": "int", "variadic": null }, - "slug": "get_class", - "sub_area": "Class" + "slug": "filemtime", + "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "get_parent_class", - "description": "", + "area": "Filesystem", + "canonical_name": "fileowner", + "description": "Gets file owner.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fileowner", + "codegen_line": 5492, + "notes": [ + "Lowers `fileowner(path)` and boxes the runtime integer-or-false result." + ], + "runtime_helpers": [ + "__rt_filegroup", + "__rt_fileinode", + "__rt_fileowner" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/fileowner.rs", "sig_line": null }, - "name": "get_parent_class", + "name": "fileowner", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object_or_class", - "optional": true, - "type": "mixed" + "name": "filename", + "optional": false, + "type": "string" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "get_parent_class", - "sub_area": "Class" + "slug": "fileowner", + "sub_area": "Filesystem" }, { - "area": "Type", - "canonical_name": "get_resource_id", - "description": "Lowers `get_resource_id(resource)` by unboxing the native handle and making it one-based.", + "area": "Filesystem", + "canonical_name": "fileperms", + "description": "Gets file permissions.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", - "codegen_function": "lower_get_resource_id", - "codegen_line": 424, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fileperms", + "codegen_line": 5484, "notes": [ - "Lowers `get_resource_id(resource)` by unboxing the native handle and making it one-based." + "Lowers `fileperms(path)` and boxes the runtime integer-or-false result." + ], + "runtime_helpers": [ + "__rt_filegroup", + "__rt_fileinode", + "__rt_fileowner", + "__rt_fileperms" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fileperms.rs", "sig_line": null }, - "name": "get_resource_id", + "name": "fileperms", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "resource", + "name": "filename", "optional": false, - "type": "resource" + "type": "string" } ], - "return_type": "int", + "return_type": "mixed", "variadic": null }, - "slug": "get_resource_id", - "sub_area": "Type" + "slug": "fileperms", + "sub_area": "Filesystem" }, { - "area": "Type", - "canonical_name": "get_resource_type", - "description": "Lowers `get_resource_type(resource)` to elephc's current resource type label.", + "area": "Filesystem", + "canonical_name": "filesize", + "description": "Gets file size.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", - "codegen_function": "lower_get_resource_type", - "codegen_line": 412, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_filesize", + "codegen_line": 5424, "notes": [ - "Lowers `get_resource_type(resource)` to elephc's current resource type label." + "Lowers `filesize(path)` through the target-aware runtime stat helper." + ], + "runtime_helpers": [ + "__rt_filemtime", + "__rt_link", + "__rt_linkinfo", + "__rt_symlink" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/filesize.rs", "sig_line": null }, - "name": "get_resource_type", + "name": "filesize", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "resource", + "name": "filename", "optional": false, - "type": "resource" + "type": "string" } ], - "return_type": "string", - "variadic": null - }, - "slug": "get_resource_type", - "sub_area": "Type" - }, - { - "area": "Class", - "canonical_name": "get_declared_classes", - "description": "", - "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": "src/types/signatures.rs", - "sig_line": null - }, - "name": "get_declared_classes", - "sig": { - "params": [], - "return_type": "array", + "return_type": "int", "variadic": null }, - "slug": "get_declared_classes", - "sub_area": "Class" + "slug": "filesize", + "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "get_declared_interfaces", - "description": "", + "area": "Filesystem", + "canonical_name": "filetype", + "description": "Gets file type.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_filetype", + "codegen_line": 5516, + "notes": [ + "Lowers `filetype(path)` and boxes the runtime string-or-false result." + ], + "runtime_helpers": [ + "__rt_filetype", + "__rt_lstat_array", + "__rt_stat_array" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/filetype.rs", "sig_line": null }, - "name": "get_declared_interfaces", + "name": "filetype", "sig": { - "params": [], - "return_type": "array", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", "variadic": null }, - "slug": "get_declared_interfaces", - "sub_area": "Class" + "slug": "filetype", + "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "get_declared_traits", - "description": "", + "area": "Type", + "canonical_name": "floatval", + "description": "Returns the float value of a variable.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_floatval", + "codegen_line": 556, + "notes": [ + "Lowers `floatval()` for concrete scalar operands." + ], + "runtime_helpers": [ + "__rt_str_to_number" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/types/floatval.rs", "sig_line": null }, - "name": "get_declared_traits", + "name": "floatval", "sig": { - "params": [], - "return_type": "array", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + } + ], + "return_type": "float", "variadic": null }, - "slug": "get_declared_traits", - "sub_area": "Class" + "slug": "floatval", + "sub_area": "Casts" }, { - "area": "Class", - "canonical_name": "interface_exists", - "description": "", + "area": "IO", + "canonical_name": "flock", + "description": "Portable advisory file locking.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_flock", + "codegen_line": 3309, + "notes": [ + "Lowers `flock(stream, operation, would_block?)` through the libc flock wrapper." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/flock.rs", "sig_line": null }, - "name": "interface_exists", + "name": "flock", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "interface", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "autoload", + "name": "operation", + "optional": false, + "type": "int" + }, + { + "by_ref": true, + "default": "null", + "name": "would_block", "optional": true, "type": "bool" } @@ -6546,708 +6371,2078 @@ "return_type": "bool", "variadic": null }, - "slug": "interface_exists", - "sub_area": "Class" + "slug": "flock", + "sub_area": "IO" }, { - "area": "Class", - "canonical_name": "trait_exists", - "description": "", + "area": "Math", + "canonical_name": "floor", + "description": "Rounds a number down to the nearest integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_floor", + "codegen_line": 70, + "notes": [ + "Lowers `floor()` for concrete integer-like and floating operands." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/math/floor.rs", "sig_line": null }, - "name": "trait_exists", + "name": "floor", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "trait", + "name": "num", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "autoload", - "optional": true, - "type": "bool" + "type": "float" } ], - "return_type": "bool", + "return_type": "float", "variadic": null }, - "slug": "trait_exists", - "sub_area": "Class" + "slug": "floor", + "sub_area": "Math" }, { - "area": "IO", - "canonical_name": "fwrite", - "description": "Lowers `fwrite(stream, data)` and returns the number of bytes written.", + "area": "Math", + "canonical_name": "fmod", + "description": "Returns the floating point remainder of the division of the arguments.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_fwrite", - "codegen_line": 2617, + "codegen_file": "src/codegen/lower_inst/builtins/math/binary.rs", + "codegen_function": "lower_fmod", + "codegen_line": 92, "notes": [ - "Lowers `fwrite(stream, data)` and returns the number of bytes written." - ], - "runtime_helpers": [ - "__rt_fwrite" + "Lowers `fmod()` for concrete integer-like and floating operands." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/fmod.rs", "sig_line": null }, - "name": "fwrite", + "name": "fmod", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "num1", "optional": false, - "type": "resource" + "type": "float" }, { "by_ref": false, "default": null, - "name": "data", + "name": "num2", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": false, - "type": "int" + "type": "float" } ], - "return_type": "int", + "return_type": "float", "variadic": null }, - "slug": "fwrite", - "sub_area": "IO" + "slug": "fmod", + "sub_area": "Math" }, { "area": "Filesystem", - "canonical_name": "getcwd", - "description": "Lowers `getcwd()` through the target-aware runtime helper.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_getcwd", - "codegen_line": 4753, - "notes": [ - "Lowers `getcwd()` through the target-aware runtime helper." - ], - "runtime_helpers": [ - "__rt_getcwd", - "__rt_tmpfile" - ], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "getcwd", - "sig": { - "params": [], - "return_type": "string", - "variadic": null - }, - "slug": "getcwd", - "sub_area": "Filesystem" - }, - { - "area": "Date", - "canonical_name": "getdate", - "description": "Lowers `getdate([$timestamp])` through the shared decomposition runtime helper.", + "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_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_getdate", - "codegen_line": 183, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fnmatch", + "codegen_line": 4603, "notes": [ - "Lowers `getdate([$timestamp])` through the shared decomposition runtime helper.", - "Marshals the optional timestamp (the `-1` current-time sentinel when omitted; a boxed", - "`Mixed`/`Union` argument is unboxed) into the integer result register where `__rt_getdate`", - "reads it, then boxes the returned associative-array hash pointer into a `Mixed` cell \u2014 the same", - "representation `stat`/`getdate` use, so the checker types the result `Mixed`." - ], - "runtime_helpers": [ - "__rt_getdate", - "__rt_mixed_from_value" + "Lowers `fnmatch(pattern, filename, flags?)` through the target-aware runtime helper." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fnmatch.rs", "sig_line": null }, - "name": "getdate", + "name": "fnmatch", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "timestamp", + "name": "pattern", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "flags", "optional": true, "type": "int" } ], - "return_type": "array", + "return_type": "bool", "variadic": null }, - "slug": "getdate", - "sub_area": "Date" + "slug": "fnmatch", + "sub_area": "Filesystem" }, { - "area": "Filesystem", - "canonical_name": "getenv", - "description": "Lowers `getenv(name)` through the target-aware environment lookup helper.", + "area": "IO", + "canonical_name": "fopen", + "description": "Opens file or URL.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_getenv", - "codegen_line": 645, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fopen", + "codegen_line": 340, "notes": [ - "Lowers `getenv(name)` through the target-aware environment lookup helper." + "Lowers `fopen(filename, mode)` and boxes stream resources or PHP false." ], "runtime_helpers": [ - "__rt_getenv" + "__rt_tmpfile" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fopen.rs", "sig_line": null }, - "name": "getenv", + "name": "fopen", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "name", + "name": "filename", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "local_only", + "name": "mode", "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "false", + "name": "use_include_path", + "optional": true, "type": "bool" + }, + { + "by_ref": false, + "default": "null", + "name": "context", + "optional": true, + "type": "mixed" } ], "return_type": "mixed", "variadic": null }, - "slug": "getenv", - "sub_area": "Filesystem" + "slug": "fopen", + "sub_area": "IO" }, { - "area": "Type", - "canonical_name": "gettype", - "description": "Lowers `gettype(value)` for statically concrete PHP types.", + "area": "IO", + "canonical_name": "fpassthru", + "description": "Output all remaining data on a file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_gettype", - "codegen_line": 612, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fpassthru", + "codegen_line": 3027, "notes": [ - "Lowers `gettype(value)` for statically concrete PHP types." + "Lowers `fpassthru(stream)` through the remaining-bytes stream runtime helper." + ], + "runtime_helpers": [ + "__rt_feof", + "__rt_fpassthru" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fpassthru.rs", "sig_line": null }, - "name": "gettype", + "name": "fpassthru", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "stream", "optional": false, - "type": "mixed" + "type": "resource" } ], - "return_type": "string", + "return_type": "int", "variadic": null }, - "slug": "gettype", - "sub_area": "Type" + "slug": "fpassthru", + "sub_area": "IO" }, { - "area": "Filesystem", - "canonical_name": "glob", - "description": "Lowers `glob(pattern)` through the target-aware runtime glob expansion helper.", + "area": "IO", + "canonical_name": "fprintf", + "description": "Write a formatted string to a stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_glob", - "codegen_line": 3820, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fprintf", + "codegen_line": 2862, "notes": [ - "Lowers `glob(pattern)` through the target-aware runtime glob expansion helper." + "Lowers `fprintf(stream, format, values...)` as `sprintf()` plus stream write." ], "runtime_helpers": [ - "__rt_glob" + "__rt_sprintf" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fprintf.rs", "sig_line": null }, - "name": "glob", + "name": "fprintf", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pattern", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "flags", + "name": "format", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "array", - "variadic": null + "return_type": "int", + "variadic": "values" }, - "slug": "glob", - "sub_area": "Filesystem" + "slug": "fprintf", + "sub_area": "IO" }, { - "area": "Date", - "canonical_name": "gmdate", - "description": "Lowers `gmdate(format[, timestamp])`: the UTC counterpart of `date()`.", + "area": "IO", + "canonical_name": "fputcsv", + "description": "Format line as CSV and write to file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_gmdate", - "codegen_line": 33, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fputcsv", + "codegen_line": 3005, "notes": [ - "Lowers `gmdate(format[, timestamp])`: the UTC counterpart of `date()`.", - "Identical argument marshalling to `date()`, but dispatches to `__rt_gmdate`, which formats", - "the instant in UTC regardless of the active default timezone." + "Lowers `fputcsv(stream, fields, separator?, enclosure?)` for string arrays." ], "runtime_helpers": [ - "__rt_date", - "__rt_gmdate" + "__rt_fputcsv" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fputcsv.rs", "sig_line": null }, - "name": "gmdate", + "name": "fputcsv", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "format", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "timestamp", + "name": "fields", + "optional": false, + "type": "array" + }, + { + "by_ref": false, + "default": "','", + "name": "separator", "optional": true, - "type": "int" + "type": "string" + }, + { + "by_ref": false, + "default": "'\"'", + "name": "enclosure", + "optional": true, + "type": "string" } ], - "return_type": "string", + "return_type": "int", "variadic": null }, - "slug": "gmdate", - "sub_area": "Date" + "slug": "fputcsv", + "sub_area": "IO" }, { - "area": "Date", - "canonical_name": "gmmktime", - "description": "Lowers `gmmktime(...)`: the UTC counterpart of `mktime()`.", + "area": "IO", + "canonical_name": "fread", + "description": "Binary-safe file read.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_gmmktime", - "codegen_line": 151, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fread", + "codegen_line": 2816, "notes": [ - "Lowers `gmmktime(...)`: the UTC counterpart of `mktime()`.", - "Identical six-integer argument marshalling, but dispatches to `__rt_gmmktime`, which", - "interprets the broken-down date/time as UTC (`timegm`) instead of local time." + "Lowers `fread(stream, length)` using the shared runtime file-read helper." ], "runtime_helpers": [ - "__rt_checkdate", - "__rt_getdate", - "__rt_gmmktime" + "__rt_fread" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fread.rs", "sig_line": null }, - "name": "gmmktime", + "name": "fread", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "hour", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "minute", + "name": "stream", "optional": false, - "type": "int" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "second", + "name": "length", "optional": false, "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "month", - "optional": false, - "type": "int" - }, + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "fread", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "fscanf", + "description": "Parses input from a file according to a format.", + "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_fscanf", + "codegen_line": 2938, + "notes": [ + "Lowers `fscanf(stream, format)` through `__rt_fgets` and `__rt_sscanf`." + ], + "runtime_helpers": [ + "__rt_fgets", + "__rt_sscanf" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/fscanf.rs", + "sig_line": null + }, + "name": "fscanf", + "sig": { + "params": [ { "by_ref": false, "default": null, - "name": "day", + "name": "stream", "optional": false, - "type": "int" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "year", + "name": "format", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "int", - "variadic": null + "return_type": "array", + "variadic": "vars" }, - "slug": "gmmktime", - "sub_area": "Date" + "slug": "fscanf", + "sub_area": "IO" }, { - "area": "String", - "canonical_name": "grapheme_strrev", - "description": "Lowers `grapheme_strrev()` and boxes its `string|false` result as `Mixed`.", + "area": "IO", + "canonical_name": "fseek", + "description": "Seeks on a file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_grapheme_strrev", - "codegen_line": 88, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fseek", + "codegen_line": 3172, "notes": [ - "Lowers `grapheme_strrev()` and boxes its `string|false` result as `Mixed`." - ], - "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "Lowers `fseek(stream, offset, whence?)` and clears EOF state on success." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fseek.rs", "sig_line": null }, - "name": "grapheme_strrev", + "name": "fseek", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": "0", + "name": "whence", + "optional": true, + "type": "int" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "grapheme_strrev", - "sub_area": "String" + "slug": "fseek", + "sub_area": "IO" }, { - "area": "String", - "canonical_name": "hash", - "description": "Lowers `hash(algo, data, binary?)` through the shared runtime digest dispatcher.", + "area": "Streams", + "canonical_name": "fsockopen", + "description": "Open Internet or Unix domain socket connection.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_hash", - "codegen_line": 209, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fsockopen", + "codegen_line": 3644, "notes": [ - "Lowers `hash(algo, data, binary?)` through the shared runtime digest dispatcher." - ], - "runtime_helpers": [ - "__rt_hash" + "Lowers `fsockopen(host, port, errno?, errstr?, timeout?)`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fsockopen.rs", "sig_line": null }, - "name": "hash", + "name": "fsockopen", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "algo", + "name": "hostname", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "data", + "name": "port", "optional": false, - "type": "string" + "type": "int" }, { - "by_ref": false, - "default": "false", - "name": "binary", + "by_ref": true, + "default": "null", + "name": "error_code", "optional": true, - "type": "bool" + "type": "int" + }, + { + "by_ref": true, + "default": "null", + "name": "error_message", + "optional": true, + "type": "string" }, { "by_ref": false, - "default": "[]", - "name": "options", + "default": "null", + "name": "timeout", "optional": true, - "type": "array" + "type": "float" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "hash", - "sub_area": "String" + "slug": "fsockopen", + "sub_area": "Streams" }, { - "area": "String", - "canonical_name": "hash_algos", - "description": "Lowers `hash_algos()` through the runtime algorithm-list builder.", + "area": "IO", + "canonical_name": "fstat", + "description": "Gets information about a file using an open file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_hash_algos", - "codegen_line": 254, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fstat", + "codegen_line": 5539, "notes": [ - "Lowers `hash_algos()` through the runtime algorithm-list builder." + "Lowers `fstat(stream)` and boxes the runtime stat array or PHP false result." ], "runtime_helpers": [ - "__rt_hash_algos_list", - "__rt_hash_init" + "__rt_fstat_array" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fstat.rs", "sig_line": null }, - "name": "hash_algos", + "name": "fstat", "sig": { - "params": [], - "return_type": "array", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false, + "type": "resource" + } + ], + "return_type": "mixed", "variadic": null }, - "slug": "hash_algos", - "sub_area": "String" + "slug": "fstat", + "sub_area": "IO" }, { - "area": "String", - "canonical_name": "hash_copy", - "description": "Lowers `hash_copy(context)` through the incremental hash clone helper.", + "area": "IO", + "canonical_name": "fsync", + "description": "Synchronizes changes to the file (including meta-data).", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_hash_copy", - "codegen_line": 333, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fsync", + "codegen_line": 3261, "notes": [ - "Lowers `hash_copy(context)` through the incremental hash clone helper." + "Lowers `fsync(stream)` through the shared fd sync runtime helper." ], "runtime_helpers": [ - "__rt_crc32", - "__rt_hash_copy", - "__rt_md5", - "__rt_sha1" + "__rt_fflush", + "__rt_fsync" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/fsync.rs", "sig_line": null }, - "name": "hash_copy", + "name": "fsync", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "context", + "name": "stream", "optional": false, "type": "resource" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "hash_copy", - "sub_area": "String" + "slug": "fsync", + "sub_area": "IO" }, { - "area": "String", - "canonical_name": "hash_equals", - "description": "Lowers `hash_equals(known, user)` through the timing-safe runtime compare helper.", + "area": "IO", + "canonical_name": "ftell", + "description": "Returns the current position of the file read/write pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_hash_equals", - "codegen_line": 247, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_ftell", + "codegen_line": 3133, "notes": [ - "Lowers `hash_equals(known, user)` through the timing-safe runtime compare helper." + "Lowers `ftell(stream)` as `lseek(fd, 0, SEEK_CUR)`." ], "runtime_helpers": [ - "__rt_hash_algos_list", - "__rt_hash_equals", - "__rt_hash_init" + "__rt_user_wrapper_ftell" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/ftell.rs", "sig_line": null }, - "name": "hash_equals", + "name": "ftell", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "known_string", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "user_string", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "hash_equals", - "sub_area": "String" + "slug": "ftell", + "sub_area": "IO" }, { "area": "IO", - "canonical_name": "hash_file", - "description": "Lowers `hash_file(algo, filename, binary?)` by reading bytes then hashing them.", + "canonical_name": "ftruncate", + "description": "Truncates a file to a given length.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_hash_file", - "codegen_line": 180, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_ftruncate", + "codegen_line": 3210, "notes": [ - "Lowers `hash_file(algo, filename, binary?)` by reading bytes then hashing them." + "Lowers `ftruncate(stream, size)` through the shared fd truncate runtime helper." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/ftruncate.rs", "sig_line": null }, - "name": "hash_file", + "name": "ftruncate", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "algo", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "filename", + "name": "size", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": "false", - "name": "binary", - "optional": true, - "type": "bool" - }, - { - "by_ref": false, - "default": "[]", - "name": "options", - "optional": true, - "type": "array" + "type": "int" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "ftruncate", + "sub_area": "IO" + }, + { + "area": "Class", + "canonical_name": "function_exists", + "description": "Returns true if the given function has been defined.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_function_exists", + "codegen_line": 276, + "notes": [ + "Lowers `function_exists(\"name\")` for compile-time string names.", + "Recognizes user functions, externs, catalog builtins, and the date/time procedural aliases", + "that `name_resolver` desugars (including the injected timezone-introspection prelude", + "functions). The aliases are matched through `is_date_procedural_alias` rather than the catalog", + "because their call sites are rewritten before codegen, so they never reach the builtin catalog", + "yet must still report as existing to match PHP." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/function_exists.rs", + "sig_line": null + }, + "name": "function_exists", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "function", + "optional": false, + "type": "string" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "function_exists", + "sub_area": "Class" + }, + { + "area": "IO", + "canonical_name": "fwrite", + "description": "Binary-safe file write.", + "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_fwrite", + "codegen_line": 2838, + "notes": [ + "Lowers `fwrite(stream, data)` and returns the number of bytes written." + ], + "runtime_helpers": [ + "__rt_fwrite" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/fwrite.rs", + "sig_line": null + }, + "name": "fwrite", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false, + "type": "resource" + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false, + "type": "string" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "fwrite", + "sub_area": "IO" + }, + { + "area": "Class", + "canonical_name": "get_class", + "description": "Returns the name of the class of an object.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_class_name_lookup", + "codegen_line": 331, + "notes": [ + "Lowers `get_class()` and `get_parent_class()` through static or dynamic class metadata." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/get_class.rs", + "sig_line": null + }, + "name": "get_class", + "sig": { + "params": [ + { + "by_ref": false, + "default": "null", + "name": "object", + "optional": true, + "type": "object" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "get_class", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "get_declared_classes", + "description": "Returns an array of the names of the defined classes.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_get_declared_names", + "codegen_line": 388, + "notes": [ + "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/get_declared_classes.rs", + "sig_line": null + }, + "name": "get_declared_classes", + "sig": { + "params": [], + "return_type": "array", + "variadic": null + }, + "slug": "get_declared_classes", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "get_declared_interfaces", + "description": "Returns an array of all declared interfaces.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_get_declared_names", + "codegen_line": 388, + "notes": [ + "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/get_declared_interfaces.rs", + "sig_line": null + }, + "name": "get_declared_interfaces", + "sig": { + "params": [], + "return_type": "array", + "variadic": null + }, + "slug": "get_declared_interfaces", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "get_declared_traits", + "description": "Returns an array of all declared traits.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_get_declared_names", + "codegen_line": 388, + "notes": [ + "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/get_declared_traits.rs", + "sig_line": null + }, + "name": "get_declared_traits", + "sig": { + "params": [], + "return_type": "array", + "variadic": null + }, + "slug": "get_declared_traits", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "get_parent_class", + "description": "Returns the name of the parent class of an object or class.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_class_name_lookup", + "codegen_line": 331, + "notes": [ + "Lowers `get_class()` and `get_parent_class()` through static or dynamic class metadata." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/get_parent_class.rs", + "sig_line": null + }, + "name": "get_parent_class", + "sig": { + "params": [ + { + "by_ref": false, + "default": "null", + "name": "object_or_class", + "optional": true, + "type": "mixed" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "get_parent_class", + "sub_area": "Class" + }, + { + "area": "Type", + "canonical_name": "get_resource_id", + "description": "Returns an integer identifier for the given resource.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_get_resource_id", + "codegen_line": 424, + "notes": [ + "Lowers `get_resource_id(resource)` by unboxing the native handle and making it one-based." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/types/get_resource_id.rs", + "sig_line": null + }, + "name": "get_resource_id", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "resource", + "optional": false, + "type": "resource" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "get_resource_id", + "sub_area": "Type" + }, + { + "area": "Type", + "canonical_name": "get_resource_type", + "description": "Returns the type of a resource.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_get_resource_type", + "codegen_line": 412, + "notes": [ + "Lowers `get_resource_type(resource)` to elephc's current resource type label." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/types/get_resource_type.rs", + "sig_line": null + }, + "name": "get_resource_type", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "resource", + "optional": false, + "type": "resource" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "get_resource_type", + "sub_area": "Type" + }, + { + "area": "Filesystem", + "canonical_name": "getcwd", + "description": "Gets the current working directory.", + "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_getcwd", + "codegen_line": 5396, + "notes": [ + "Lowers `getcwd()` through the target-aware runtime helper." + ], + "runtime_helpers": [ + "__rt_getcwd", + "__rt_tmpfile" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/getcwd.rs", + "sig_line": null + }, + "name": "getcwd", + "sig": { + "params": [], + "return_type": "string", + "variadic": null + }, + "slug": "getcwd", + "sub_area": "Filesystem" + }, + { + "area": "Date", + "canonical_name": "getdate", + "description": "Returns date/time information.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_getdate", + "codegen_line": 183, + "notes": [ + "Lowers `getdate([$timestamp])` through the shared decomposition runtime helper.", + "Marshals the optional timestamp (the `-1` current-time sentinel when omitted; a boxed", + "`Mixed`/`Union` argument is unboxed) into the integer result register where `__rt_getdate`", + "reads it, then boxes the returned associative-array hash pointer into a `Mixed` cell \u2014 the same", + "representation `stat`/`getdate` use, so the checker types the result `Mixed`." + ], + "runtime_helpers": [ + "__rt_getdate", + "__rt_mixed_from_value" + ], + "sig_arm": null, + "sig_file": "src/builtins/system/getdate.rs", + "sig_line": null + }, + "name": "getdate", + "sig": { + "params": [ + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true, + "type": "int" + } + ], + "return_type": "array", + "variadic": null + }, + "slug": "getdate", + "sub_area": "Date" + }, + { + "area": "Filesystem", + "canonical_name": "getenv", + "description": "Gets the value of an environment variable.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_getenv", + "codegen_line": 645, + "notes": [ + "Lowers `getenv(name)` through the target-aware environment lookup helper." + ], + "runtime_helpers": [ + "__rt_getenv" + ], + "sig_arm": null, + "sig_file": "src/builtins/system/getenv.rs", + "sig_line": null + }, + "name": "getenv", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "name", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "getenv", + "sub_area": "Filesystem" + }, + { + "area": "IO", + "canonical_name": "gethostbyaddr", + "description": "Gets the Internet host name corresponding to a given IP address.", + "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_gethostbyaddr", + "codegen_line": 3431, + "notes": [ + "Lowers `gethostbyaddr(address)` and boxes malformed addresses as PHP `false`." + ], + "runtime_helpers": [ + "__rt_gethostbyaddr", + "__rt_getprotobyname" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/gethostbyaddr.rs", + "sig_line": null + }, + "name": "gethostbyaddr", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "gethostbyaddr", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "gethostbyname", + "description": "Gets the IPv4 address corresponding to the given Internet host name.", + "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_gethostbyname", + "codegen_line": 3419, + "notes": [ + "Lowers `gethostbyname(hostname)` through the shared runtime resolver." + ], + "runtime_helpers": [ + "__rt_gethostbyaddr", + "__rt_gethostbyname" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/gethostbyname.rs", + "sig_line": null + }, + "name": "gethostbyname", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "hostname", + "optional": false, + "type": "string" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "gethostbyname", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "gethostname", + "description": "Gets the standard host name for the local machine.", + "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_gethostname", + "codegen_line": 3409, + "notes": [ + "Lowers `gethostname()` through the shared runtime helper." + ], + "runtime_helpers": [ + "__rt_gethostbyaddr", + "__rt_gethostbyname", + "__rt_gethostname" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/gethostname.rs", + "sig_line": null + }, + "name": "gethostname", + "sig": { + "params": [], + "return_type": "string", + "variadic": null + }, + "slug": "gethostname", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "getprotobyname", + "description": "Gets the protocol number associated with the given protocol name.", + "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_getprotobyname", + "codegen_line": 3444, + "notes": [ + "Lowers `getprotobyname(protocol)` and boxes a missing entry as PHP `false`." + ], + "runtime_helpers": [ + "__rt_getprotobyname" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/getprotobyname.rs", + "sig_line": null + }, + "name": "getprotobyname", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "getprotobyname", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "getprotobynumber", + "description": "Gets the protocol name associated with the given protocol number.", + "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_getprotobynumber", + "codegen_line": 3467, + "notes": [ + "Lowers `getprotobynumber(number)` and boxes a missing entry as PHP `false`." + ], + "runtime_helpers": [ + "__rt_getprotobynumber" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/getprotobynumber.rs", + "sig_line": null + }, + "name": "getprotobynumber", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false, + "type": "int" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "getprotobynumber", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "getservbyname", + "description": "Gets port number associated with an Internet service and protocol.", + "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_getservbyname", + "codegen_line": 3486, + "notes": [ + "Lowers `getservbyname(service, protocol)` and boxes a missing entry as PHP `false`." + ], + "runtime_helpers": [ + "__rt_getservbyname" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/getservbyname.rs", + "sig_line": null + }, + "name": "getservbyname", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "service", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "getservbyname", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "getservbyport", + "description": "Gets the Internet service that corresponds to a port and protocol.", + "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_getservbyport", + "codegen_line": 3517, + "notes": [ + "Lowers `getservbyport(port, protocol)` and boxes a missing entry as PHP `false`." + ], + "runtime_helpers": [ + "__rt_getservbyport" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/getservbyport.rs", + "sig_line": null + }, + "name": "getservbyport", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "port", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "getservbyport", + "sub_area": "IO" + }, + { + "area": "Type", + "canonical_name": "gettype", + "description": "Returns the type of a variable as a string.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_gettype", + "codegen_line": 129, + "notes": [ + "Lowers `gettype(value)` for statically concrete PHP types." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/types/gettype.rs", + "sig_line": null + }, + "name": "gettype", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "gettype", + "sub_area": "Type" + }, + { + "area": "Filesystem", + "canonical_name": "glob", + "description": "Finds pathnames matching 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_glob", + "codegen_line": 4463, + "notes": [ + "Lowers `glob(pattern)` through the target-aware runtime glob expansion helper." + ], + "runtime_helpers": [ + "__rt_glob" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/glob.rs", + "sig_line": null + }, + "name": "glob", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false, + "type": "string" + } + ], + "return_type": "array", + "variadic": null + }, + "slug": "glob", + "sub_area": "Filesystem" + }, + { + "area": "Date", + "canonical_name": "gmdate", + "description": "Formats a GMT/UTC date and time.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_gmdate", + "codegen_line": 33, + "notes": [ + "Lowers `gmdate(format[, timestamp])`: the UTC counterpart of `date()`.", + "Identical argument marshalling to `date()`, but dispatches to `__rt_gmdate`, which formats", + "the instant in UTC regardless of the active default timezone." + ], + "runtime_helpers": [ + "__rt_date", + "__rt_gmdate" + ], + "sig_arm": null, + "sig_file": "src/builtins/system/gmdate.rs", + "sig_line": null + }, + "name": "gmdate", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "gmdate", + "sub_area": "Date" + }, + { + "area": "Date", + "canonical_name": "gmmktime", + "description": "Returns the Unix timestamp for a GMT date.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_gmmktime", + "codegen_line": 151, + "notes": [ + "Lowers `gmmktime(...)`: the UTC counterpart of `mktime()`.", + "Identical six-integer argument marshalling, but dispatches to `__rt_gmmktime`, which", + "interprets the broken-down date/time as UTC (`timegm`) instead of local time." + ], + "runtime_helpers": [ + "__rt_checkdate", + "__rt_getdate", + "__rt_gmmktime" + ], + "sig_arm": null, + "sig_file": "src/builtins/system/gmmktime.rs", + "sig_line": null + }, + "name": "gmmktime", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "hour", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "minute", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "second", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "month", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "day", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "year", + "optional": false, + "type": "int" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "gmmktime", + "sub_area": "Date" + }, + { + "area": "String", + "canonical_name": "grapheme_strrev", + "description": "Reverses a string by grapheme cluster, returning false on failure.", + "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_grapheme_strrev", + "codegen_line": 88, + "notes": [ + "Lowers `grapheme_strrev()` and boxes its `string|false` result as `Mixed`." + ], + "runtime_helpers": [ + "__rt_grapheme_strrev", + "__rt_strcopy" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/grapheme_strrev.rs", + "sig_line": null + }, + "name": "grapheme_strrev", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "grapheme_strrev", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "gzcompress", + "description": "Compress a string using the ZLIB data format.", + "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_gzcompress", + "codegen_line": 402, + "notes": [ + "Lowers `gzcompress(data, level?)` through inline zlib `compress2` calls." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/gzcompress.rs", + "sig_line": null + }, + "name": "gzcompress", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "-1", + "name": "level", + "optional": true, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "gzcompress", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "gzdeflate", + "description": "Deflate a string using the DEFLATE data format.", + "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_gzdeflate", + "codegen_line": 418, + "notes": [ + "Lowers `gzdeflate(data, level?)` through inline raw-DEFLATE zlib calls." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/gzdeflate.rs", + "sig_line": null + }, + "name": "gzdeflate", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "-1", + "name": "level", + "optional": true, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "gzdeflate", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "gzinflate", + "description": "Inflate a deflated string.", + "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_gzinflate", + "codegen_line": 436, + "notes": [ + "Lowers `gzinflate(data, max_length?)` and boxes zlib failures as PHP false." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/gzinflate.rs", + "sig_line": null + }, + "name": "gzinflate", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "max_length", + "optional": true, + "type": "int" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "gzinflate", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "gzuncompress", + "description": "Uncompress a compressed string.", + "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_gzuncompress", + "codegen_line": 457, + "notes": [ + "Lowers `gzuncompress(data, max_length?)` and boxes zlib failures as PHP false." + ], + "runtime_helpers": [ + "__rt_long2ip" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/gzuncompress.rs", + "sig_line": null + }, + "name": "gzuncompress", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "max_length", + "optional": true, + "type": "int" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "gzuncompress", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "hash", + "description": "Generates a hash value using the given algorithm.", + "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_hash", + "codegen_line": 209, + "notes": [ + "Lowers `hash(algo, data, binary?)` through the shared runtime digest dispatcher." + ], + "runtime_helpers": [ + "__rt_hash" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/hash.rs", + "sig_line": null + }, + "name": "hash", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true, + "type": "bool" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "hash", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "hash_algos", + "description": "Returns an array of supported hashing algorithm names.", + "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_hash_algos", + "codegen_line": 254, + "notes": [ + "Lowers `hash_algos()` through the runtime algorithm-list builder." + ], + "runtime_helpers": [ + "__rt_hash_algos_list", + "__rt_hash_init" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/hash_algos.rs", + "sig_line": null + }, + "name": "hash_algos", + "sig": { + "params": [], + "return_type": "array", + "variadic": null + }, + "slug": "hash_algos", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "hash_copy", + "description": "Copies the state of an incremental hashing context.", + "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_hash_copy", + "codegen_line": 333, + "notes": [ + "Lowers `hash_copy(context)` through the incremental hash clone helper." + ], + "runtime_helpers": [ + "__rt_crc32", + "__rt_hash_copy", + "__rt_md5", + "__rt_sha1" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/hash_copy.rs", + "sig_line": null + }, + "name": "hash_copy", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false, + "type": "resource" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "hash_copy", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "hash_equals", + "description": "Compares two strings using a constant-time algorithm.", + "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_hash_equals", + "codegen_line": 247, + "notes": [ + "Lowers `hash_equals(known, user)` through the timing-safe runtime compare helper." + ], + "runtime_helpers": [ + "__rt_hash_algos_list", + "__rt_hash_equals", + "__rt_hash_init" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/hash_equals.rs", + "sig_line": null + }, + "name": "hash_equals", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "known_string", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "user_string", + "optional": false, + "type": "string" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "hash_equals", + "sub_area": "String" + }, + { + "area": "IO", + "canonical_name": "hash_file", + "description": "Generates a hash value using the contents of a given 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_hash_file", + "codegen_line": 287, + "notes": [ + "Lowers `hash_file(algo, filename, binary?)` by reading bytes then hashing them." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/io/hash_file.rs", + "sig_line": null + }, + "name": "hash_file", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true, + "type": "bool" } ], "return_type": "mixed", @@ -7259,13 +8454,13 @@ { "area": "String", "canonical_name": "hash_final", - "description": "Lowers `hash_final(context, binary?)` through the incremental hash finalizer.", + "description": "Finalizes an incremental hash and returns the digest string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_final", "codegen_line": 302, "notes": [ @@ -7275,7 +8470,7 @@ "__rt_hash_final" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/hash_final.rs", "sig_line": null }, "name": "hash_final", @@ -7290,7 +8485,7 @@ }, { "by_ref": false, - "default": null, + "default": "false", "name": "binary", "optional": true, "type": "bool" @@ -7305,13 +8500,13 @@ { "area": "String", "canonical_name": "hash_hmac", - "description": "Lowers `hash_hmac(algo, data, key, binary?)` through the shared HMAC runtime dispatcher.", + "description": "Generates a keyed hash value using the HMAC method.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_hmac", "codegen_line": 228, "notes": [ @@ -7322,7 +8517,7 @@ "__rt_hash_hmac" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/hash_hmac.rs", "sig_line": null }, "name": "hash_hmac", @@ -7351,7 +8546,7 @@ }, { "by_ref": false, - "default": null, + "default": "false", "name": "binary", "optional": true, "type": "bool" @@ -7366,13 +8561,13 @@ { "area": "String", "canonical_name": "hash_init", - "description": "Lowers `hash_init(algo)` and returns a boxed HashContext resource.", + "description": "Initialize an incremental hashing context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_init", "codegen_line": 266, "notes": [ @@ -7382,7 +8577,7 @@ "__rt_hash_init" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/hash_init.rs", "sig_line": null }, "name": "hash_init", @@ -7408,13 +8603,6 @@ "name": "key", "optional": true, "type": "string" - }, - { - "by_ref": false, - "default": "[]", - "name": "options", - "optional": true, - "type": "array" } ], "return_type": "mixed", @@ -7426,13 +8614,13 @@ { "area": "String", "canonical_name": "hash_update", - "description": "Lowers `hash_update(context, data)` through the incremental hash runtime helper.", + "description": "Pumps data into an active incremental hashing context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_update", "codegen_line": 277, "notes": [ @@ -7442,7 +8630,7 @@ "__rt_hash_update" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/hash_update.rs", "sig_line": null }, "name": "hash_update", @@ -7472,13 +8660,13 @@ { "area": "Misc", "canonical_name": "header", - "description": "Lowers `header($line[, $replace[, $code]])` to `__rt_header`, materializing the", + "description": "Sends a raw HTTP header.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_header", "codegen_line": 289, "notes": [ @@ -7493,7 +8681,7 @@ "__rt_header" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/header.rs", "sig_line": null }, "name": "header", @@ -7504,21 +8692,21 @@ "default": null, "name": "header", "optional": false, - "type": "mixed" + "type": "string" }, { "by_ref": false, - "default": null, + "default": "true", "name": "replace", "optional": true, - "type": "mixed" + "type": "bool" }, { "by_ref": false, - "default": null, + "default": "0", "name": "response_code", "optional": true, - "type": "mixed" + "type": "int" } ], "return_type": "void", @@ -7530,13 +8718,13 @@ { "area": "String", "canonical_name": "hex2bin", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "description": "Decodes a hexadecimal string back into its binary representation.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_unary_string_runtime", "codegen_line": 76, "notes": [ @@ -7547,7 +8735,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/hex2bin.rs", "sig_line": null }, "name": "hex2bin", @@ -7570,13 +8758,13 @@ { "area": "Date", "canonical_name": "hrtime", - "description": "Lowers `hrtime([$as_number])` through the monotonic-clock runtime helper.", + "description": "Returns the current high-resolution time.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_hrtime", "codegen_line": 247, "notes": [ @@ -7591,7 +8779,7 @@ "__rt_http_response_code" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/hrtime.rs", "sig_line": null }, "name": "hrtime", @@ -7599,7 +8787,7 @@ "params": [ { "by_ref": false, - "default": null, + "default": "false", "name": "as_number", "optional": true, "type": "bool" @@ -7614,13 +8802,13 @@ { "area": "String", "canonical_name": "html_entity_decode", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "description": "Converts HTML entities in a string back into their corresponding characters.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_unary_string_runtime", "codegen_line": 76, "notes": [ @@ -7631,7 +8819,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/html_entity_decode.rs", "sig_line": null }, "name": "html_entity_decode", @@ -7643,20 +8831,6 @@ "name": "string", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "encoding", - "optional": false, - "type": "string" } ], "return_type": "string", @@ -7668,13 +8842,13 @@ { "area": "String", "canonical_name": "htmlentities", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "description": "Converts all applicable characters in a string into their HTML entities.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_unary_string_runtime", "codegen_line": 76, "notes": [ @@ -7685,7 +8859,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/htmlentities.rs", "sig_line": null }, "name": "htmlentities", @@ -7697,27 +8871,6 @@ "name": "string", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "encoding", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "double_encode", - "optional": false, - "type": "bool" } ], "return_type": "string", @@ -7729,13 +8882,13 @@ { "area": "String", "canonical_name": "htmlspecialchars", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "description": "Converts the HTML special characters in a string into their entities.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_unary_string_runtime", "codegen_line": 76, "notes": [ @@ -7746,7 +8899,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/htmlspecialchars.rs", "sig_line": null }, "name": "htmlspecialchars", @@ -7758,27 +8911,6 @@ "name": "string", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "encoding", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "double_encode", - "optional": false, - "type": "bool" } ], "return_type": "string", @@ -7790,13 +8922,13 @@ { "area": "Misc", "canonical_name": "http_response_code", - "description": "Lowers `http_response_code([$code])` to `__rt_http_response_code`. The code (or", + "description": "Gets or sets the HTTP response code.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_http_response_code", "codegen_line": 264, "notes": [ @@ -7810,7 +8942,7 @@ "__rt_http_response_code" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/http_response_code.rs", "sig_line": null }, "name": "http_response_code", @@ -7818,10 +8950,10 @@ "params": [ { "by_ref": false, - "default": null, + "default": "0", "name": "response_code", "optional": true, - "type": "mixed" + "type": "int" } ], "return_type": "int", @@ -7833,13 +8965,13 @@ { "area": "Math", "canonical_name": "hypot", - "description": "Lowers `hypot()` using the C ABI argument order `x, y`.", + "description": "Calculates the length of the hypotenuse of a right-angle triangle.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/libm.rs", + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", "codegen_function": "lower_hypot", "codegen_line": 43, "notes": [ @@ -7847,7 +8979,7 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/hypot.rs", "sig_line": null }, "name": "hypot", @@ -7877,13 +9009,13 @@ { "area": "String", "canonical_name": "implode", - "description": "Lowers `implode(glue, array)` by selecting the string or integer array helper.", + "description": "Joins array elements into a single string using a separator.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_implode", "codegen_line": 192, "notes": [ @@ -7891,7 +9023,7 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/implode.rs", "sig_line": null }, "name": "implode", @@ -7906,7 +9038,7 @@ }, { "by_ref": false, - "default": null, + "default": "null", "name": "array", "optional": true, "type": "array" @@ -7921,21 +9053,21 @@ { "area": "Array", "canonical_name": "in_array", - "description": "Lowers `in_array()` for indexed arrays with scalar or string payloads.", + "description": "Checks if a value exists in an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_in_array", - "codegen_line": 1160, + "codegen_line": 1786, "notes": [ - "Lowers `in_array()` for indexed arrays with scalar or string payloads." + "Lowers `in_array()` for indexed and associative arrays with PHP loose or strict membership." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/in_array.rs", "sig_line": null }, "name": "in_array", @@ -7957,36 +9089,114 @@ }, { "by_ref": false, - "default": null, + "default": "false", "name": "strict", "optional": true, "type": "bool" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, "slug": "in_array", "sub_area": "Array" }, + { + "area": "String", + "canonical_name": "inet_ntop", + "description": "Converts a packed internet address to a human-readable representation.", + "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_inet", + "codegen_line": 497, + "notes": [ + "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." + ], + "runtime_helpers": [ + "__rt_sprintf" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/inet_ntop.rs", + "sig_line": null + }, + "name": "inet_ntop", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "inet_ntop", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "inet_pton", + "description": "Converts a human-readable IP address to its packed in_addr representation.", + "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_inet", + "codegen_line": 497, + "notes": [ + "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." + ], + "runtime_helpers": [ + "__rt_sprintf" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/inet_pton.rs", + "sig_line": null + }, + "name": "inet_pton", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "inet_pton", + "sub_area": "String" + }, { "area": "Math", "canonical_name": "intdiv", - "description": "Lowers `intdiv()` for concrete integer-like numeric operands.", + "description": "Integer division.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/binary.rs", + "codegen_file": "src/codegen/lower_inst/builtins/math/binary.rs", "codegen_function": "lower_intdiv", - "codegen_line": 21, + "codegen_line": 23, "notes": [ "Lowers `intdiv()` for concrete integer-like numeric operands." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/intdiv.rs", "sig_line": null }, "name": "intdiv", @@ -8013,18 +9223,62 @@ "slug": "intdiv", "sub_area": "Math" }, + { + "area": "Class", + "canonical_name": "interface_exists", + "description": "Checks if the interface has been defined.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_class_like_exists", + "codegen_line": 293, + "notes": [ + "Lowers AOT class/interface/enum existence checks for literal names." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/interface_exists.rs", + "sig_line": null + }, + "name": "interface_exists", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "interface", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "interface_exists", + "sub_area": "Class" + }, { "area": "Type", "canonical_name": "intval", - "description": "Lowers `intval()` for concrete scalar operands.", + "description": "Returns the integer value of a variable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", + "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_intval", - "codegen_line": 1001, + "codegen_line": 523, "notes": [ "Lowers `intval()` for concrete scalar operands." ], @@ -8033,7 +9287,7 @@ "__rt_str_to_int" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/intval.rs", "sig_line": null }, "name": "intval", @@ -8045,13 +9299,6 @@ "name": "value", "optional": false, "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "base", - "optional": false, - "type": "int" } ], "return_type": "int", @@ -8061,153 +9308,153 @@ "sub_area": "Casts" }, { - "area": "SPL", - "canonical_name": "iterator_apply", - "description": "Lowers `iterator_apply()` over supported Traversable sources and callback forms.", + "area": "String", + "canonical_name": "ip2long", + "description": "Converts a string containing an IPv4 address into a long integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_iterator_apply", - "codegen_line": 289, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_ip2long", + "codegen_line": 488, "notes": [ - "Lowers `iterator_apply()` over supported Traversable sources and callback forms." + "Lowers `ip2long(string)` and boxes invalid-address results as PHP false." + ], + "runtime_helpers": [ + "__rt_ip2long", + "__rt_sprintf" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ip2long.rs", "sig_line": null }, - "name": "iterator_apply", + "name": "ip2long", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "iterator", - "optional": false, - "type": "traversable" - }, - { - "by_ref": false, - "default": null, - "name": "callback", + "name": "ip", "optional": false, - "type": "callable" - }, - { - "by_ref": false, - "default": null, - "name": "args", - "optional": true, - "type": "array" + "type": "string" } ], - "return_type": "int", + "return_type": "mixed", "variadic": null }, - "slug": "iterator_apply", - "sub_area": "SPL" + "slug": "ip2long", + "sub_area": "String" }, { - "area": "SPL", - "canonical_name": "iterator_count", - "description": "Lowers `iterator_count()` over arrays, `iterable`, and Traversable objects.", + "area": "Class", + "canonical_name": "is_a", + "description": "Checks whether an object is of a given type or has it as one of its parents.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_iterator_count", - "codegen_line": 236, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_is_a_relation", + "codegen_line": 369, "notes": [ - "Lowers `iterator_count()` over arrays, `iterable`, and Traversable objects." + "Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/is_a.rs", "sig_line": null }, - "name": "iterator_count", + "name": "is_a", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "iterator", + "name": "object_or_class", "optional": false, - "type": "traversable" + "type": "object" + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "false", + "name": "allow_string", + "optional": true, + "type": "bool" } ], - "return_type": "int", + "return_type": "bool", "variadic": null }, - "slug": "iterator_count", - "sub_area": "SPL" + "slug": "is_a", + "sub_area": "Class" }, { - "area": "SPL", - "canonical_name": "iterator_to_array", - "description": "Lowers `iterator_to_array()` over arrays, `iterable`, and Traversable objects.", + "area": "Type", + "canonical_name": "is_array", + "description": "Checks whether a variable is an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_iterator_to_array", - "codegen_line": 265, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_is_array", + "codegen_line": 1002, "notes": [ - "Lowers `iterator_to_array()` over arrays, `iterable`, and Traversable objects." + "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", + "not treated as a definite array here (it may hold a Traversable); use `is_iterable` for that." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/is_array.rs", "sig_line": null }, - "name": "iterator_to_array", + "name": "is_array", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "iterator", + "name": "value", "optional": false, - "type": "traversable" - }, - { - "by_ref": false, - "default": null, - "name": "preserve_keys", - "optional": true, - "type": "bool" + "type": "mixed" } ], - "return_type": "array", + "return_type": "bool", "variadic": null }, - "slug": "iterator_to_array", - "sub_area": "SPL" + "slug": "is_array", + "sub_area": "Type" }, { "area": "Type", "canonical_name": "is_bool", - "description": "", + "description": "Checks whether a variable is a boolean.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_static_type_predicate", + "codegen_line": 736, + "notes": [ + "Lowers a static `is_*` predicate for concrete non-Mixed values." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/types/is_bool.rs", "sig_line": null }, "name": "is_bool", @@ -8230,15 +9477,15 @@ { "area": "Type", "canonical_name": "is_callable", - "description": "Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers.", + "description": "Checks whether a variable can be called as a function.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", + "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_callable", - "codegen_line": 802, + "codegen_line": 319, "notes": [ "Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers." ], @@ -8248,7 +9495,7 @@ "__rt_is_callable_object" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/is_callable.rs", "sig_line": null }, "name": "is_callable", @@ -8260,20 +9507,6 @@ "name": "value", "optional": false, "type": "mixed" - }, - { - "by_ref": false, - "default": "false", - "name": "syntax_only", - "optional": true, - "type": "bool" - }, - { - "by_ref": true, - "default": "null", - "name": "callable_name", - "optional": true, - "type": "string" } ], "return_type": "bool", @@ -8285,15 +9518,15 @@ { "area": "Filesystem", "canonical_name": "is_dir", - "description": "Lowers `is_dir(path)` through the target-aware runtime stat helper.", + "description": "Tells whether the filename is a directory.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_is_dir", - "codegen_line": 4957, + "codegen_line": 5600, "notes": [ "Lowers `is_dir(path)` through the target-aware runtime stat helper." ], @@ -8303,7 +9536,7 @@ "__rt_is_writable" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/is_dir.rs", "sig_line": null }, "name": "is_dir", @@ -8326,15 +9559,15 @@ { "area": "Filesystem", "canonical_name": "is_executable", - "description": "Lowers `is_executable(path)` through the target-aware runtime access helper.", + "description": "Tells whether the filename is executable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_is_executable", - "codegen_line": 4989, + "codegen_line": 5632, "notes": [ "Lowers `is_executable(path)` through the target-aware runtime access helper." ], @@ -8345,7 +9578,7 @@ "__rt_readfile" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/is_executable.rs", "sig_line": null }, "name": "is_executable", @@ -8368,15 +9601,15 @@ { "area": "Filesystem", "canonical_name": "is_file", - "description": "Lowers `is_file(path)` through the target-aware runtime stat helper.", + "description": "Tells whether the filename is a regular file.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_is_file", - "codegen_line": 4949, + "codegen_line": 5592, "notes": [ "Lowers `is_file(path)` through the target-aware runtime stat helper." ], @@ -8386,7 +9619,7 @@ "__rt_is_writable" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/is_file.rs", "sig_line": null }, "name": "is_file", @@ -8407,77 +9640,63 @@ "sub_area": "Filesystem" }, { - "area": "Class", - "canonical_name": "is_a", - "description": "", + "area": "Math", + "canonical_name": "is_finite", + "description": "Checks whether a float is finite.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_is_finite", + "codegen_line": 169, + "notes": [ + "Lowers `is_finite()` by rejecting NaN and both infinities." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/types/is_finite.rs", "sig_line": null }, - "name": "is_a", + "name": "is_finite", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object_or_class", - "optional": false, - "type": "object" - }, - { - "by_ref": false, - "default": null, - "name": "class", + "name": "num", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "allow_string", - "optional": true, - "type": "bool" + "type": "float" } ], "return_type": "bool", "variadic": null }, - "slug": "is_a", - "sub_area": "Class" + "slug": "is_finite", + "sub_area": "Math" }, { "area": "Type", - "canonical_name": "is_array", - "description": "Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value", + "canonical_name": "is_float", + "description": "Checks whether a variable is a floating-point number.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_is_array", - "codegen_line": 1480, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_static_type_predicate", + "codegen_line": 736, "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", - "not treated as a definite array here (it may hold a Traversable); use `is_iterable` for that." + "Lowers a static `is_*` predicate for concrete non-Mixed values." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/is_float.rs", "sig_line": null }, - "name": "is_array", + "name": "is_float", "sig": { "params": [ { @@ -8491,31 +9710,67 @@ "return_type": "bool", "variadic": null }, - "slug": "is_array", + "slug": "is_float", "sub_area": "Type" }, + { + "area": "Math", + "canonical_name": "is_infinite", + "description": "Checks whether a float is infinite.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_is_infinite", + "codegen_line": 132, + "notes": [ + "Lowers `is_infinite()` by comparing the normalized float against +/- infinity." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/types/is_infinite.rs", + "sig_line": null + }, + "name": "is_infinite", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "float" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "is_infinite", + "sub_area": "Math" + }, { "area": "Type", - "canonical_name": "is_object", - "description": "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose", + "canonical_name": "is_int", + "description": "Checks whether a variable is an integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_is_object", - "codegen_line": 1495, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_static_type_predicate", + "codegen_line": 736, "notes": [ - "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose", - "runtime tag is an object (6)." + "Lowers a static `is_*` predicate for concrete non-Mixed values." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/is_int.rs", "sig_line": null }, - "name": "is_object", + "name": "is_int", "sig": { "params": [ { @@ -8529,32 +9784,30 @@ "return_type": "bool", "variadic": null }, - "slug": "is_object", + "slug": "is_int", "sub_area": "Type" }, { "area": "Type", - "canonical_name": "is_scalar", - "description": "Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed", + "canonical_name": "is_iterable", + "description": "Checks whether a variable is iterable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_is_scalar", - "codegen_line": 1511, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_is_iterable", + "codegen_line": 794, "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,", - "arrays, objects, and resources are not scalars, matching PHP." + "Lowers `is_iterable()` for concrete values and boxed Mixed payloads." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/is_iterable.rs", "sig_line": null }, - "name": "is_scalar", + "name": "is_iterable", "sig": { "params": [ { @@ -8568,186 +9821,184 @@ "return_type": "bool", "variadic": null }, - "slug": "is_scalar", + "slug": "is_iterable", "sub_area": "Type" }, { - "area": "Math", - "canonical_name": "is_finite", - "description": "Lowers `is_finite()` by rejecting NaN and both infinities.", + "area": "Filesystem", + "canonical_name": "is_link", + "description": "Tells whether the filename is a symbolic link.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_is_finite", - "codegen_line": 169, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_is_link", + "codegen_line": 5640, "notes": [ - "Lowers `is_finite()` by rejecting NaN and both infinities." + "Lowers `is_link(path)` through the target-aware runtime lstat helper." + ], + "runtime_helpers": [ + "__rt_is_link", + "__rt_path_is_wrapper", + "__rt_readfile", + "__rt_readfile_wrapper" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/is_link.rs", "sig_line": null }, - "name": "is_finite", + "name": "is_link", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "filename", "optional": false, - "type": "float" + "type": "string" } ], "return_type": "bool", "variadic": null }, - "slug": "is_finite", - "sub_area": "Math" + "slug": "is_link", + "sub_area": "Filesystem" }, { - "area": "Type", - "canonical_name": "is_float", - "description": "", + "area": "Math", + "canonical_name": "is_nan", + "description": "Checks whether a float is NAN.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_is_nan", + "codegen_line": 113, + "notes": [ + "Lowers `is_nan()` by checking whether the normalized float is unordered with itself." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/types/is_nan.rs", "sig_line": null }, - "name": "is_float", + "name": "is_nan", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "num", "optional": false, - "type": "mixed" + "type": "float" } ], "return_type": "bool", "variadic": null }, - "slug": "is_float", - "sub_area": "Type" + "slug": "is_nan", + "sub_area": "Math" }, { - "area": "Class", - "canonical_name": "is_subclass_of", - "description": "", + "area": "Type", + "canonical_name": "is_null", + "description": "Checks whether a variable is null.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_is_null_builtin", + "codegen_line": 992, + "notes": [ + "Lowers `is_null()` for concrete scalar values and boxed Mixed payloads." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/types/is_null.rs", "sig_line": null }, - "name": "is_subclass_of", + "name": "is_null", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "value", "optional": false, "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "class", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "allow_string", - "optional": true, - "type": "bool" } ], "return_type": "bool", "variadic": null }, - "slug": "is_subclass_of", - "sub_area": "Class" + "slug": "is_null", + "sub_area": "Type" }, { - "area": "Math", - "canonical_name": "is_infinite", - "description": "Lowers `is_infinite()` by comparing the normalized float against +/- infinity.", + "area": "Type", + "canonical_name": "is_numeric", + "description": "Checks whether a variable is a number or a numeric string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_is_infinite", - "codegen_line": 132, + "codegen_file": "src/codegen/lower_inst/builtins/is_numeric.rs", + "codegen_function": "lower_is_numeric", + "codegen_line": 22, "notes": [ - "Lowers `is_infinite()` by comparing the normalized float against +/- infinity." + "Lowers `is_numeric()` for concrete scalar values." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/is_numeric.rs", "sig_line": null }, - "name": "is_infinite", + "name": "is_numeric", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "value", "optional": false, - "type": "float" + "type": "mixed" } ], "return_type": "bool", "variadic": null }, - "slug": "is_infinite", - "sub_area": "Math" + "slug": "is_numeric", + "sub_area": "Type" }, { "area": "Type", - "canonical_name": "is_int", - "description": "", + "canonical_name": "is_object", + "description": "Checks whether a variable is an object.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_is_object", + "codegen_line": 1017, + "notes": [ + "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose", + "runtime tag is an object (6)." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/types/is_object.rs", "sig_line": null }, - "name": "is_int", + "name": "is_object", "sig": { "params": [ { @@ -8761,102 +10012,110 @@ "return_type": "bool", "variadic": null }, - "slug": "is_int", + "slug": "is_object", "sub_area": "Type" }, { - "area": "Type", - "canonical_name": "is_iterable", - "description": "Lowers `is_iterable()` for concrete values and boxed Mixed payloads.", + "area": "Filesystem", + "canonical_name": "is_readable", + "description": "Tells whether the filename is readable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_is_iterable", - "codegen_line": 1272, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_is_readable", + "codegen_line": 5608, "notes": [ - "Lowers `is_iterable()` for concrete values and boxed Mixed payloads." + "Lowers `is_readable(path)` through the target-aware runtime access helper." + ], + "runtime_helpers": [ + "__rt_is_executable", + "__rt_is_readable", + "__rt_is_writable" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/is_readable.rs", "sig_line": null }, - "name": "is_iterable", + "name": "is_readable", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "filename", "optional": false, - "type": "mixed" + "type": "string" } ], "return_type": "bool", "variadic": null }, - "slug": "is_iterable", - "sub_area": "Type" + "slug": "is_readable", + "sub_area": "Filesystem" }, { - "area": "Math", - "canonical_name": "is_nan", - "description": "Lowers `is_nan()` by checking whether the normalized float is unordered with itself.", + "area": "Type", + "canonical_name": "is_resource", + "description": "Checks whether a variable is a resource.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_is_nan", - "codegen_line": 113, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_is_resource", + "codegen_line": 400, "notes": [ - "Lowers `is_nan()` by checking whether the normalized float is unordered with itself." + "Lowers `is_resource(value)` for static resources and boxed Mixed resource cells." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/is_resource.rs", "sig_line": null }, - "name": "is_nan", + "name": "is_resource", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "value", "optional": false, - "type": "float" + "type": "mixed" } ], "return_type": "bool", "variadic": null }, - "slug": "is_nan", - "sub_area": "Math" + "slug": "is_resource", + "sub_area": "Type" }, { "area": "Type", - "canonical_name": "is_null", - "description": "", + "canonical_name": "is_scalar", + "description": "Checks whether a variable is a scalar.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_is_scalar", + "codegen_line": 1033, + "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,", + "arrays, objects, and resources are not scalars, matching PHP." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/types/is_scalar.rs", "sig_line": null }, - "name": "is_null", + "name": "is_scalar", "sig": { "params": [ { @@ -8870,30 +10129,30 @@ "return_type": "bool", "variadic": null }, - "slug": "is_null", + "slug": "is_scalar", "sub_area": "Type" }, { "area": "Type", - "canonical_name": "is_numeric", - "description": "Lowers `is_numeric()` for concrete scalar values.", + "canonical_name": "is_string", + "description": "Checks whether a variable is a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/is_numeric.rs", - "codegen_function": "lower_is_numeric", - "codegen_line": 22, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_static_type_predicate", + "codegen_line": 736, "notes": [ - "Lowers `is_numeric()` for concrete scalar values." + "Lowers a static `is_*` predicate for concrete non-Mixed values." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/is_string.rs", "sig_line": null }, - "name": "is_numeric", + "name": "is_string", "sig": { "params": [ { @@ -8907,71 +10166,85 @@ "return_type": "bool", "variadic": null }, - "slug": "is_numeric", + "slug": "is_string", "sub_area": "Type" }, { - "area": "Type", - "canonical_name": "is_resource", - "description": "Lowers `is_resource(value)` for static resources and boxed Mixed resource cells.", + "area": "Class", + "canonical_name": "is_subclass_of", + "description": "Checks if the object has a given class as one of its parents or implements it.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", - "codegen_function": "lower_is_resource", - "codegen_line": 400, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_is_a_relation", + "codegen_line": 369, "notes": [ - "Lowers `is_resource(value)` for static resources and boxed Mixed resource cells." + "Lowers `is_a()` and `is_subclass_of()` for object operands and literal targets." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/is_subclass_of.rs", "sig_line": null }, - "name": "is_resource", + "name": "is_subclass_of", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "object_or_class", "optional": false, "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "true", + "name": "allow_string", + "optional": true, + "type": "bool" } ], "return_type": "bool", "variadic": null }, - "slug": "is_resource", - "sub_area": "Type" + "slug": "is_subclass_of", + "sub_area": "Class" }, { "area": "Filesystem", - "canonical_name": "is_readable", - "description": "Lowers `is_readable(path)` through the target-aware runtime access helper.", + "canonical_name": "is_writable", + "description": "Tells whether the filename is writable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_is_readable", - "codegen_line": 4965, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_is_writable", + "codegen_line": 5616, "notes": [ - "Lowers `is_readable(path)` through the target-aware runtime access helper." + "Lowers `is_writable(path)` through the target-aware runtime access helper." ], "runtime_helpers": [ "__rt_is_executable", - "__rt_is_readable", + "__rt_is_link", "__rt_is_writable" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/is_writable.rs", "sig_line": null }, - "name": "is_readable", + "name": "is_writable", "sig": { "params": [ { @@ -8985,215 +10258,229 @@ "return_type": "bool", "variadic": null }, - "slug": "is_readable", + "slug": "is_writable", "sub_area": "Filesystem" }, { - "area": "Type", - "canonical_name": "is_string", - "description": "", + "area": "Filesystem", + "canonical_name": "is_writeable", + "description": "Tells whether the filename is writable (alias of is_writable).", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_is_writeable", + "codegen_line": 5624, + "notes": [ + "Lowers `is_writeable(path)`, PHP's alias of `is_writable(path)`." + ], + "runtime_helpers": [ + "__rt_is_executable", + "__rt_is_link", + "__rt_is_writable" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/is_writeable.rs", "sig_line": null }, - "name": "is_string", + "name": "is_writeable", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "filename", "optional": false, - "type": "mixed" + "type": "string" } ], "return_type": "bool", "variadic": null }, - "slug": "is_string", - "sub_area": "Type" + "slug": "is_writeable", + "sub_area": "Filesystem" }, { - "area": "Filesystem", - "canonical_name": "is_link", - "description": "Lowers `is_link(path)` through the target-aware runtime lstat helper.", + "area": "Misc", + "canonical_name": "isset", + "description": "Determines whether a variable is set and is not null.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_is_link", - "codegen_line": 4997, + "codegen_file": "src/codegen/lower_inst/builtins/isset.rs", + "codegen_function": "lower_isset", + "codegen_line": 24, "notes": [ - "Lowers `is_link(path)` through the target-aware runtime lstat helper." - ], - "runtime_helpers": [ - "__rt_is_link", - "__rt_path_is_wrapper", - "__rt_readfile", - "__rt_readfile_wrapper" + "Lowers `isset()` for values already evaluated by the EIR frontend." ], + "runtime_helpers": [], "sig_arm": null, "sig_file": null, "sig_line": null }, - "name": "is_link", + "name": "isset", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "var", "optional": false, - "type": "string" + "type": "mixed" } ], "return_type": "bool", - "variadic": null + "variadic": "vars" }, - "slug": "is_link", - "sub_area": "Filesystem" + "slug": "isset", + "sub_area": "Variable" }, { - "area": "Filesystem", - "canonical_name": "is_writeable", - "description": "Lowers `is_writeable(path)`, PHP's alias of `is_writable(path)`.", + "area": "SPL", + "canonical_name": "iterator_apply", + "description": "Call a function for every element in an iterator.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_is_writeable", - "codegen_line": 4981, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_iterator_apply", + "codegen_line": 290, "notes": [ - "Lowers `is_writeable(path)`, PHP's alias of `is_writable(path)`." - ], - "runtime_helpers": [ - "__rt_is_executable", - "__rt_is_link", - "__rt_is_writable" + "Lowers `iterator_apply()` over supported Traversable sources and callback forms." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/iterator_apply.rs", "sig_line": null }, - "name": "is_writeable", + "name": "iterator_apply", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "iterator", "optional": false, - "type": "string" + "type": "traversable" + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false, + "type": "callable" + }, + { + "by_ref": false, + "default": "null", + "name": "args", + "optional": true, + "type": "array" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "is_writeable", - "sub_area": "Filesystem" + "slug": "iterator_apply", + "sub_area": "SPL" }, { - "area": "Filesystem", - "canonical_name": "is_writable", - "description": "Lowers `is_writable(path)` through the target-aware runtime access helper.", + "area": "SPL", + "canonical_name": "iterator_count", + "description": "Count the elements in an iterator.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_is_writable", - "codegen_line": 4973, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_iterator_count", + "codegen_line": 237, "notes": [ - "Lowers `is_writable(path)` through the target-aware runtime access helper." - ], - "runtime_helpers": [ - "__rt_is_executable", - "__rt_is_link", - "__rt_is_writable" + "Lowers `iterator_count()` over arrays, `iterable`, and Traversable objects." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/iterator_count.rs", "sig_line": null }, - "name": "is_writable", + "name": "iterator_count", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "iterator", "optional": false, - "type": "string" + "type": "traversable" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "is_writable", - "sub_area": "Filesystem" + "slug": "iterator_count", + "sub_area": "SPL" }, { - "area": "Misc", - "canonical_name": "isset", - "description": "Determines whether a variable is set and is not null.", + "area": "SPL", + "canonical_name": "iterator_to_array", + "description": "Copy the iterator into an array.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/isset.rs", - "codegen_function": "lower_isset", - "codegen_line": 24, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_iterator_to_array", + "codegen_line": 266, "notes": [ - "Lowers `isset()` for values already evaluated by the EIR frontend." + "Lowers `iterator_to_array()` over arrays, `iterable`, and Traversable objects." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/iterator_to_array.rs", "sig_line": null }, - "name": "isset", + "name": "iterator_to_array", "sig": { "params": [ { "by_ref": false, - "default": null, - "name": "var", - "optional": false, - "type": "mixed" + "default": null, + "name": "iterator", + "optional": false, + "type": "traversable" + }, + { + "by_ref": false, + "default": "true", + "name": "preserve_keys", + "optional": true, + "type": "bool" } ], - "return_type": "bool", - "variadic": "vars" + "return_type": "array", + "variadic": null }, - "slug": "isset", - "sub_area": "Variable" + "slug": "iterator_to_array", + "sub_area": "SPL" }, { "area": "JSON", "canonical_name": "json_decode", - "description": "Lowers `json_decode(json, associative?, depth?, flags?)` through the shared JSON decoder runtime.", + "description": "Decodes a JSON string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/json.rs", + "codegen_file": "src/codegen/lower_inst/builtins/json.rs", "codegen_function": "lower_json_decode", "codegen_line": 30, "notes": [ @@ -9203,7 +10490,7 @@ "__rt_json_decode_mixed" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/json_decode.rs", "sig_line": null }, "name": "json_decode", @@ -9218,21 +10505,21 @@ }, { "by_ref": false, - "default": null, + "default": "null", "name": "associative", "optional": true, "type": "bool" }, { "by_ref": false, - "default": null, + "default": "512", "name": "depth", "optional": true, "type": "int" }, { "by_ref": false, - "default": null, + "default": "0", "name": "flags", "optional": true, "type": "int" @@ -9247,13 +10534,13 @@ { "area": "JSON", "canonical_name": "json_encode", - "description": "Lowers `json_encode(value, flags?, depth?)` through the shared JSON encoder runtime.", + "description": "Returns the JSON representation of a value.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/json.rs", + "codegen_file": "src/codegen/lower_inst/builtins/json.rs", "codegen_function": "lower_json_encode", "codegen_line": 52, "notes": [ @@ -9261,7 +10548,7 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/json_encode.rs", "sig_line": null }, "name": "json_encode", @@ -9276,14 +10563,14 @@ }, { "by_ref": false, - "default": null, + "default": "0", "name": "flags", "optional": true, "type": "int" }, { "by_ref": false, - "default": null, + "default": "512", "name": "depth", "optional": true, "type": "int" @@ -9298,13 +10585,13 @@ { "area": "JSON", "canonical_name": "json_last_error", - "description": "Lowers `json_last_error()` by reading the shared runtime error-code symbol.", + "description": "Returns the last error (if any) occurred during the last JSON encoding/decoding.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/json.rs", + "codegen_file": "src/codegen/lower_inst/builtins/json.rs", "codegen_function": "lower_json_last_error", "codegen_line": 70, "notes": [ @@ -9314,7 +10601,7 @@ "__rt_json_last_error_msg" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/json_last_error.rs", "sig_line": null }, "name": "json_last_error", @@ -9329,13 +10616,13 @@ { "area": "JSON", "canonical_name": "json_last_error_msg", - "description": "Lowers `json_last_error_msg()` through the runtime message lookup table.", + "description": "Returns the error string of the last json_encode() or json_decode() call.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/json.rs", + "codegen_file": "src/codegen/lower_inst/builtins/json.rs", "codegen_function": "lower_json_last_error_msg", "codegen_line": 85, "notes": [ @@ -9346,7 +10633,7 @@ "__rt_json_validate" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/json_last_error_msg.rs", "sig_line": null }, "name": "json_last_error_msg", @@ -9361,13 +10648,13 @@ { "area": "JSON", "canonical_name": "json_validate", - "description": "Lowers `json_validate(json, depth?, flags?)` into the shared validator runtime.", + "description": "Checks if a string contains valid JSON.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/json.rs", + "codegen_file": "src/codegen/lower_inst/builtins/json.rs", "codegen_function": "lower_json_validate", "codegen_line": 95, "notes": [ @@ -9377,7 +10664,7 @@ "__rt_json_validate" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/json_validate.rs", "sig_line": null }, "name": "json_validate", @@ -9392,14 +10679,14 @@ }, { "by_ref": false, - "default": null, + "default": "512", "name": "depth", "optional": true, "type": "int" }, { "by_ref": false, - "default": null, + "default": "0", "name": "flags", "optional": true, "type": "int" @@ -9414,17 +10701,17 @@ { "area": "Array", "canonical_name": "krsort", - "description": "Lowers `krsort()` through the legacy reverse key-sort helper surface.", + "description": "Sorts an array by key in descending order.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_krsort", - "codegen_line": 1101, + "codegen_line": 1104, "notes": [ - "Lowers `krsort()` through the legacy reverse key-sort helper surface." + "Lowers `krsort()` through the reverse key-sort helper surface." ], "runtime_helpers": [ "__rt_krsort", @@ -9432,7 +10719,7 @@ "__rt_natsort" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/krsort.rs", "sig_line": null }, "name": "krsort", @@ -9444,13 +10731,6 @@ "name": "array", "optional": false, "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": false, - "type": "int" } ], "return_type": "bool", @@ -9462,17 +10742,17 @@ { "area": "Array", "canonical_name": "ksort", - "description": "Lowers `ksort()` through the legacy key-sort helper surface.", + "description": "Sorts an array by key in ascending order.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_ksort", - "codegen_line": 1096, + "codegen_line": 1099, "notes": [ - "Lowers `ksort()` through the legacy key-sort helper surface." + "Lowers `ksort()` through the key-sort helper surface." ], "runtime_helpers": [ "__rt_krsort", @@ -9481,7 +10761,7 @@ "__rt_natsort" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/ksort.rs", "sig_line": null }, "name": "ksort", @@ -9493,13 +10773,6 @@ "name": "array", "optional": false, "type": "array" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": false, - "type": "int" } ], "return_type": "bool", @@ -9511,13 +10784,13 @@ { "area": "String", "canonical_name": "lcfirst", - "description": "Lowers `lcfirst()` by copying the string and lowercasing the first ASCII byte.", + "description": "Lowercases the first character of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_lcfirst", "codegen_line": 104, "notes": [ @@ -9527,7 +10800,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/lcfirst.rs", "sig_line": null }, "name": "lcfirst", @@ -9549,43 +10822,40 @@ }, { "area": "Filesystem", - "canonical_name": "link", - "description": "Lowers `link(oldpath, newpath)` through the target-aware libc wrapper.", + "canonical_name": "lchgrp", + "description": "Changes group ownership of a symlink.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_link", - "codegen_line": 4810, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_lchgrp", + "codegen_line": 4488, "notes": [ - "Lowers `link(oldpath, newpath)` through the target-aware libc wrapper." + "Lowers `lchgrp(path, group)` for integer GIDs and string group names without following symlinks." ], "runtime_helpers": [ - "__rt_fileatime", - "__rt_filectime", - "__rt_link", - "__rt_readlink" + "__rt_umask" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/lchgrp.rs", "sig_line": null }, - "name": "link", + "name": "lchgrp", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "target", + "name": "filename", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "link", + "name": "group", "optional": false, "type": "string" } @@ -9593,153 +10863,156 @@ "return_type": "bool", "variadic": null }, - "slug": "link", + "slug": "lchgrp", "sub_area": "Filesystem" }, { "area": "Filesystem", - "canonical_name": "linkinfo", - "description": "Lowers `linkinfo(path)` through the target-aware runtime lstat helper.", + "canonical_name": "lchown", + "description": "Changes user ownership of a symlink.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_linkinfo", - "codegen_line": 4797, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_lchown", + "codegen_line": 4483, "notes": [ - "Lowers `linkinfo(path)` through the target-aware runtime lstat helper." + "Lowers `lchown(path, owner)` for integer UIDs and string user names without following symlinks." ], "runtime_helpers": [ - "__rt_link", - "__rt_linkinfo", - "__rt_readlink", - "__rt_symlink" + "__rt_umask" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/lchown.rs", "sig_line": null }, - "name": "linkinfo", + "name": "lchown", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "path", + "name": "filename", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "user", "optional": false, "type": "string" } ], - "return_type": "int", + "return_type": "bool", "variadic": null }, - "slug": "linkinfo", + "slug": "lchown", "sub_area": "Filesystem" }, { "area": "Filesystem", - "canonical_name": "lchgrp", - "description": "Lowers `lchgrp(path, group)` for integer GIDs and string group names without following symlinks.", + "canonical_name": "link", + "description": "Creates a hard link.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_lchgrp", - "codegen_line": 3845, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_link", + "codegen_line": 5453, "notes": [ - "Lowers `lchgrp(path, group)` for integer GIDs and string group names without following symlinks." + "Lowers `link(oldpath, newpath)` through the target-aware libc wrapper." ], "runtime_helpers": [ - "__rt_umask" + "__rt_fileatime", + "__rt_filectime", + "__rt_link", + "__rt_readlink" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/link.rs", "sig_line": null }, - "name": "lchgrp", + "name": "link", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "target", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "group", + "name": "link", "optional": false, - "type": "int" + "type": "string" } ], "return_type": "bool", "variadic": null }, - "slug": "lchgrp", + "slug": "link", "sub_area": "Filesystem" }, { "area": "Filesystem", - "canonical_name": "lchown", - "description": "Lowers `lchown(path, owner)` for integer UIDs and string user names without following symlinks.", + "canonical_name": "linkinfo", + "description": "Gets information about a link.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_lchown", - "codegen_line": 3840, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_linkinfo", + "codegen_line": 5440, "notes": [ - "Lowers `lchown(path, owner)` for integer UIDs and string user names without following symlinks." + "Lowers `linkinfo(path)` through the target-aware runtime lstat helper." ], "runtime_helpers": [ - "__rt_umask" + "__rt_link", + "__rt_linkinfo", + "__rt_readlink", + "__rt_symlink" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/linkinfo.rs", "sig_line": null }, - "name": "lchown", + "name": "linkinfo", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "path", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "user", - "optional": false, - "type": "int" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "lchown", + "slug": "linkinfo", "sub_area": "Filesystem" }, { "area": "Date", "canonical_name": "localtime", - "description": "Lowers `localtime([$timestamp[, $associative]])` through the shared decomposition runtime helper.", + "description": "Returns the local time.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_localtime", "codegen_line": 220, "notes": [ @@ -9755,7 +11028,7 @@ "__rt_localtime" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/localtime.rs", "sig_line": null }, "name": "localtime", @@ -9763,14 +11036,14 @@ "params": [ { "by_ref": false, - "default": null, + "default": "-1", "name": "timestamp", "optional": true, "type": "int" }, { "by_ref": false, - "default": null, + "default": "false", "name": "associative", "optional": true, "type": "bool" @@ -9785,13 +11058,13 @@ { "area": "Math", "canonical_name": "log", - "description": "Lowers `log()` in one-argument and base-changing two-argument forms.", + "description": "Natural logarithm.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/libm.rs", + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", "codegen_function": "lower_log", "codegen_line": 51, "notes": [ @@ -9799,7 +11072,7 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/log.rs", "sig_line": null }, "name": "log", @@ -9814,7 +11087,7 @@ }, { "by_ref": false, - "default": null, + "default": "2.718281828459045", "name": "base", "optional": true, "type": "float" @@ -9829,19 +11102,21 @@ { "area": "Math", "canonical_name": "log10", - "description": "", + "description": "Returns the base-10 logarithm of a number.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, + "notes": [ + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/math/log10.rs", "sig_line": null }, "name": "log10", @@ -9864,19 +11139,21 @@ { "area": "Math", "canonical_name": "log2", - "description": "", + "description": "Returns the base-2 logarithm of a number.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, + "notes": [ + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/math/log2.rs", "sig_line": null }, "name": "log2", @@ -9896,18 +11173,58 @@ "slug": "log2", "sub_area": "Math" }, + { + "area": "String", + "canonical_name": "long2ip", + "description": "Converts an IPv4 address from long integer to dotted string notation.", + "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_long2ip", + "codegen_line": 476, + "notes": [ + "Lowers `long2ip(value)` through the IPv4 formatting runtime helper." + ], + "runtime_helpers": [ + "__rt_ip2long", + "__rt_long2ip" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/long2ip.rs", + "sig_line": null + }, + "name": "long2ip", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "long2ip", + "sub_area": "String" + }, { "area": "Filesystem", "canonical_name": "lstat", - "description": "Lowers `lstat(path)` and boxes the runtime lstat array or PHP false result.", + "description": "Gives information about a file or symbolic link.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_lstat", - "codegen_line": 4891, + "codegen_line": 5534, "notes": [ "Lowers `lstat(path)` and boxes the runtime lstat array or PHP false result." ], @@ -9916,7 +11233,7 @@ "__rt_lstat_array" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/lstat.rs", "sig_line": null }, "name": "lstat", @@ -9939,13 +11256,13 @@ { "area": "String", "canonical_name": "ltrim", - "description": "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks.", + "description": "Strips whitespace (or other characters) from the beginning of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", "codegen_line": 112, "notes": [ @@ -9953,7 +11270,7 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ltrim.rs", "sig_line": null }, "name": "ltrim", @@ -9968,7 +11285,7 @@ }, { "by_ref": false, - "default": null, + "default": "' \\n\\r\\t\\x0b\\x0c\\x00'", "name": "characters", "optional": true, "type": "string" @@ -9983,13 +11300,13 @@ { "area": "Math", "canonical_name": "max", - "description": "Lowers numeric `min()` and `max()` over concrete integer-like or float operands.", + "description": "Find highest value.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", "codegen_function": "lower_min_max", "codegen_line": 204, "notes": [ @@ -9997,7 +11314,7 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/max.rs", "sig_line": null }, "name": "max", @@ -10011,7 +11328,7 @@ "type": "mixed" } ], - "return_type": "float", + "return_type": "mixed", "variadic": "values" }, "slug": "max", @@ -10020,13 +11337,13 @@ { "area": "String", "canonical_name": "md5", - "description": "Lowers `md5(data, binary?)` through the shared crypto-backed runtime helper.", + "description": "Calculates the MD5 hash of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_md5", "codegen_line": 355, "notes": [ @@ -10038,7 +11355,7 @@ "__rt_sha1" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/md5.rs", "sig_line": null }, "name": "md5", @@ -10053,7 +11370,7 @@ }, { "by_ref": false, - "default": null, + "default": "false", "name": "binary", "optional": true, "type": "bool" @@ -10068,13 +11385,13 @@ { "area": "Date", "canonical_name": "microtime", - "description": "Lowers `microtime()` / `microtime(true)` / `microtime(false)` / `microtime($flag)`.", + "description": "Returns the current Unix timestamp with microseconds.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_microtime", "codegen_line": 111, "notes": [ @@ -10093,7 +11410,7 @@ "__rt_microtime_str" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/microtime.rs", "sig_line": null }, "name": "microtime", @@ -10101,13 +11418,13 @@ "params": [ { "by_ref": false, - "default": null, + "default": "false", "name": "as_float", "optional": true, "type": "bool" } ], - "return_type": "int", + "return_type": "mixed", "variadic": null }, "slug": "microtime", @@ -10116,13 +11433,13 @@ { "area": "Math", "canonical_name": "min", - "description": "Lowers numeric `min()` and `max()` over concrete integer-like or float operands.", + "description": "Find lowest value.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", "codegen_function": "lower_min_max", "codegen_line": 204, "notes": [ @@ -10130,7 +11447,7 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/min.rs", "sig_line": null }, "name": "min", @@ -10144,7 +11461,7 @@ "type": "mixed" } ], - "return_type": "float", + "return_type": "mixed", "variadic": "values" }, "slug": "min", @@ -10153,59 +11470,38 @@ { "area": "Filesystem", "canonical_name": "mkdir", - "description": "Lowers `mkdir(path)` through the target-aware runtime helper.", + "description": "Makes a directory.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_mkdir", - "codegen_line": 3785, + "codegen_line": 4428, "notes": [ "Lowers `mkdir(path)` through the target-aware runtime helper." ], "runtime_helpers": [ - "__rt_chdir", - "__rt_copy", - "__rt_mkdir", - "__rt_rmdir", - "__rt_tempnam" - ], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "mkdir", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "directory", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "permissions", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "recursive", - "optional": false, - "type": "bool" - }, + "__rt_chdir", + "__rt_copy", + "__rt_mkdir", + "__rt_rmdir", + "__rt_tempnam" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/mkdir.rs", + "sig_line": null + }, + "name": "mkdir", + "sig": { + "params": [ { "by_ref": false, "default": null, - "name": "context", + "name": "directory", "optional": false, - "type": "bool" + "type": "string" } ], "return_type": "bool", @@ -10217,13 +11513,13 @@ { "area": "Date", "canonical_name": "mktime", - "description": "Lowers `mktime(hour, minute, second, month, day, year)` through the runtime helper.", + "description": "Returns the Unix timestamp for a date.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_mktime", "codegen_line": 140, "notes": [ @@ -10235,7 +11531,7 @@ "__rt_mktime" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/mktime.rs", "sig_line": null }, "name": "mktime", @@ -10293,19 +11589,23 @@ { "area": "Math", "canonical_name": "mt_rand", - "description": "", + "description": "Generate a random value via the Mersenne Twister Random Number Generator.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/math/random.rs", + "codegen_function": "lower_rand", + "codegen_line": 22, + "notes": [ + "Lowers `rand()` and `mt_rand()` with either zero args or an inclusive range." + ], + "runtime_helpers": [ + "__rt_random_u32" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/math/mt_rand.rs", "sig_line": null }, "name": "mt_rand", @@ -10335,15 +11635,15 @@ { "area": "Array", "canonical_name": "natcasesort", - "description": "Lowers `natcasesort()` for indexed integer arrays through the case-insensitive wrapper.", + "description": "Sorts an array using a case-insensitive natural order algorithm.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_natcasesort", - "codegen_line": 1111, + "codegen_line": 1114, "notes": [ "Lowers `natcasesort()` for indexed integer arrays through the case-insensitive wrapper." ], @@ -10351,7 +11651,7 @@ "__rt_natcasesort" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/natcasesort.rs", "sig_line": null }, "name": "natcasesort", @@ -10374,15 +11674,15 @@ { "area": "Array", "canonical_name": "natsort", - "description": "Lowers `natsort()` for indexed integer arrays through the natural-sort runtime wrapper.", + "description": "Sorts an array using a natural order algorithm.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_natsort", - "codegen_line": 1106, + "codegen_line": 1109, "notes": [ "Lowers `natsort()` for indexed integer arrays through the natural-sort runtime wrapper." ], @@ -10391,7 +11691,7 @@ "__rt_natsort" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/natsort.rs", "sig_line": null }, "name": "natsort", @@ -10414,306 +11714,93 @@ { "area": "String", "canonical_name": "nl2br", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "description": "Inserts HTML line breaks before newlines in a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_unary_string_runtime", "codegen_line": 76, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." - ], - "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" - ], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "nl2br", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "string", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "use_xhtml", - "optional": false, - "type": "bool" - } - ], - "return_type": "string", - "variadic": null - }, - "slug": "nl2br", - "sub_area": "String" - }, - { - "area": "String", - "canonical_name": "number_format", - "description": "Lowers `number_format()` by arranging its runtime helper arguments.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_number_format", - "codegen_line": 875, - "notes": [ - "Lowers `number_format()` by arranging its runtime helper arguments." - ], - "runtime_helpers": [ - "__rt_number_format" - ], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "number_format", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "num", - "optional": false, - "type": "float" - }, - { - "by_ref": false, - "default": null, - "name": "decimals", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "decimal_separator", - "optional": true, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "thousands_separator", - "optional": true, - "type": "string" - } - ], - "return_type": "string", - "variadic": null - }, - "slug": "number_format", - "sub_area": "String" - }, - { - "area": "String", - "canonical_name": "ord", - "description": "Lowers `ord()` by returning the first byte of a string or zero for empty input.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_ord", - "codegen_line": 834, - "notes": [ - "Lowers `ord()` by returning the first byte of a string or zero for empty input." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "ord", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "character", - "optional": false, - "type": "string" - } - ], - "return_type": "int", - "variadic": null - }, - "slug": "ord", - "sub_area": "String" - }, - { - "area": "Process", - "canonical_name": "passthru", - "description": "Lowers `passthru(command)` through libc `system()` for direct stdout passthrough.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_passthru", - "codegen_line": 714, - "notes": [ - "Lowers `passthru(command)` through libc `system()` for direct stdout passthrough." - ], - "runtime_helpers": [ - "__rt_cstr", - "__rt_shell_exec" - ], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "passthru", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "command", - "optional": false, - "type": "string" - }, - { - "by_ref": true, - "default": null, - "name": "result_code", - "optional": false, - "type": "int" - } - ], - "return_type": "void", - "variadic": null - }, - "slug": "passthru", - "sub_area": "Process" - }, - { - "area": "Filesystem", - "canonical_name": "pathinfo", - "description": "Lowers `pathinfo(path, flags?)` through string, array, or boxed dynamic helpers.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_pathinfo", - "codegen_line": 4001, - "notes": [ - "Lowers `pathinfo(path, flags?)` through string, array, or boxed dynamic helpers." - ], - "runtime_helpers": [ - "__rt_pathinfo_array" - ], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "pathinfo", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "path", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": true, - "type": "int" - } - ], - "return_type": "mixed", - "variadic": null - }, - "slug": "pathinfo", - "sub_area": "Filesystem" - }, - { - "area": "Misc", - "canonical_name": "php_uname", - "description": "Returns information about the operating system PHP is running on.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_php_uname", - "codegen_line": 672, - "notes": [ - "Lowers `php_uname(mode?)` through the target-aware uname runtime helper." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_php_uname" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/nl2br.rs", "sig_line": null }, - "name": "php_uname", + "name": "nl2br", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "mode", - "optional": true, + "name": "string", + "optional": false, "type": "string" } ], "return_type": "string", "variadic": null }, - "slug": "php_uname", - "sub_area": "Info" + "slug": "nl2br", + "sub_area": "String" }, { - "area": "Misc", - "canonical_name": "phpversion", - "description": "Returns the current PHP version information.", + "area": "String", + "canonical_name": "number_format", + "description": "Formats a number with grouped thousands.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_phpversion", - "codegen_line": 734, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_number_format", + "codegen_line": 875, "notes": [ - "Lowers `phpversion()` as the compiler package version string." + "Lowers `number_format()` by arranging its runtime helper arguments." + ], + "runtime_helpers": [ + "__rt_number_format" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/number_format.rs", "sig_line": null }, - "name": "phpversion", + "name": "number_format", "sig": { "params": [ { "by_ref": false, - "default": "null", - "name": "extension", + "default": null, + "name": "num", + "optional": false, + "type": "float" + }, + { + "by_ref": false, + "default": "0", + "name": "decimals", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "'.'", + "name": "decimal_separator", + "optional": true, + "type": "string" + }, + { + "by_ref": false, + "default": "','", + "name": "thousands_separator", "optional": true, "type": "string" } @@ -10721,833 +11808,831 @@ "return_type": "string", "variadic": null }, - "slug": "phpversion", - "sub_area": "Info" + "slug": "number_format", + "sub_area": "String" }, { - "area": "Math", - "canonical_name": "pi", - "description": "Lowers `pi()` as the same data-section float constant used by the legacy backend.", + "area": "IO", + "canonical_name": "opendir", + "description": "Open directory handle.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_pi", - "codegen_line": 596, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_opendir", + "codegen_line": 3547, "notes": [ - "Lowers `pi()` as the same data-section float constant used by the legacy backend." + "Lowers `opendir(path)` and boxes the directory stream as `resource|false`." + ], + "runtime_helpers": [ + "__rt_opendir", + "__rt_readdir", + "__rt_user_wrapper_dir_readdir" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/opendir.rs", "sig_line": null }, - "name": "pi", + "name": "opendir", "sig": { - "params": [], - "return_type": "float", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", "variadic": null }, - "slug": "pi", - "sub_area": "Math" + "slug": "opendir", + "sub_area": "IO" }, { - "area": "Math", - "canonical_name": "pow", - "description": "Lowers `pow()` for concrete integer-like and floating operands.", + "area": "String", + "canonical_name": "ord", + "description": "Returns the ASCII value of the first character of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/binary.rs", - "codegen_function": "lower_pow", - "codegen_line": 114, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_ord", + "codegen_line": 834, "notes": [ - "Lowers `pow()` for concrete integer-like and floating operands." + "Lowers `ord()` by returning the first byte of a string or zero for empty input." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ord.rs", "sig_line": null }, - "name": "pow", + "name": "ord", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", - "optional": false, - "type": "float" - }, - { - "by_ref": false, - "default": null, - "name": "exponent", + "name": "character", "optional": false, - "type": "float" + "type": "string" } ], - "return_type": "float", + "return_type": "int", "variadic": null }, - "slug": "pow", - "sub_area": "Math" + "slug": "ord", + "sub_area": "String" }, { - "area": "Regex", - "canonical_name": "preg_match", - "description": "Lowers `preg_match(pattern, subject)` through the shared regex runtime helper.", + "area": "Process", + "canonical_name": "passthru", + "description": "Executes an external program and passes its output directly.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/regex.rs", - "codegen_function": "lower_preg_match", - "codegen_line": 28, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_passthru", + "codegen_line": 714, "notes": [ - "Lowers `preg_match(pattern, subject)` through the shared regex runtime helper." + "Lowers `passthru(command)` through libc `system()` for direct stdout passthrough." ], "runtime_helpers": [ - "__rt_preg_match", - "__rt_preg_match_capture" + "__rt_cstr", + "__rt_shell_exec" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/passthru.rs", "sig_line": null }, - "name": "preg_match", + "name": "passthru", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pattern", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "subject", + "name": "command", "optional": false, "type": "string" - }, - { - "by_ref": true, - "default": null, - "name": "matches", - "optional": true, - "type": "array" } ], - "return_type": "int", + "return_type": "void", "variadic": null }, - "slug": "preg_match", - "sub_area": "Regex" + "slug": "passthru", + "sub_area": "Process" }, { - "area": "Regex", - "canonical_name": "preg_match_all", - "description": "Lowers `preg_match_all(pattern, subject)` through the shared regex runtime helper.", + "area": "Filesystem", + "canonical_name": "pathinfo", + "description": "Returns information about a file path.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/regex.rs", - "codegen_function": "lower_preg_match_all", - "codegen_line": 52, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_pathinfo", + "codegen_line": 4644, "notes": [ - "Lowers `preg_match_all(pattern, subject)` through the shared regex runtime helper." + "Lowers `pathinfo(path, flags?)` through string, array, or boxed dynamic helpers." ], "runtime_helpers": [ - "__rt_preg_match_all" + "__rt_pathinfo_array" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/pathinfo.rs", "sig_line": null }, - "name": "preg_match_all", + "name": "pathinfo", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pattern", + "name": "path", "optional": false, "type": "string" }, { "by_ref": false, - "default": null, - "name": "subject", - "optional": false, - "type": "string" - }, - { - "by_ref": true, - "default": null, - "name": "matches", - "optional": false, - "type": "array" + "default": "15", + "name": "flags", + "optional": true, + "type": "int" } ], - "return_type": "int", + "return_type": "array", "variadic": null }, - "slug": "preg_match_all", - "sub_area": "Regex" + "slug": "pathinfo", + "sub_area": "Filesystem" }, { - "area": "Regex", - "canonical_name": "preg_replace_callback", - "description": "Lowers `preg_replace_callback(pattern, callback, subject)` through supported direct callbacks.", + "area": "Process", + "canonical_name": "pclose", + "description": "Closes process file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/regex.rs", - "codegen_function": "lower_preg_replace_callback", - "codegen_line": 90, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_pclose", + "codegen_line": 3630, "notes": [ - "Lowers `preg_replace_callback(pattern, callback, subject)` through supported direct callbacks." + "Lowers `pclose(handle)` and returns the child process status." ], "runtime_helpers": [ - "__rt_preg_replace_callback" + "__rt_pclose" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/pclose.rs", "sig_line": null }, - "name": "preg_replace_callback", + "name": "pclose", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pattern", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "callback", - "optional": false, - "type": "callable" - }, - { - "by_ref": false, - "default": null, - "name": "subject", + "name": "handle", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": "-1", - "name": "limit", - "optional": true, - "type": "int" - }, - { - "by_ref": true, - "default": "null", - "name": "count", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": "0", - "name": "flags", - "optional": true, - "type": "int" + "type": "resource" } ], - "return_type": "array", + "return_type": "int", "variadic": null }, - "slug": "preg_replace_callback", - "sub_area": "Regex" + "slug": "pclose", + "sub_area": "Process" }, { - "area": "Regex", - "canonical_name": "preg_replace", - "description": "Lowers `preg_replace(pattern, replacement, subject)` through the regex replacement helper.", + "area": "Streams", + "canonical_name": "pfsockopen", + "description": "Open persistent Internet or Unix domain socket connection.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/regex.rs", - "codegen_function": "lower_preg_replace", - "codegen_line": 65, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_fsockopen", + "codegen_line": 3644, "notes": [ - "Lowers `preg_replace(pattern, replacement, subject)` through the regex replacement helper." - ], - "runtime_helpers": [ - "__rt_preg_replace" + "Lowers `fsockopen(host, port, errno?, errstr?, timeout?)`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/pfsockopen.rs", "sig_line": null }, - "name": "preg_replace", + "name": "pfsockopen", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pattern", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "replacement", + "name": "hostname", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "subject", + "name": "port", "optional": false, - "type": "string" + "type": "int" }, { - "by_ref": false, - "default": "-1", - "name": "limit", + "by_ref": true, + "default": "null", + "name": "error_code", "optional": true, "type": "int" }, { "by_ref": true, "default": "null", - "name": "count", + "name": "error_message", "optional": true, - "type": "int" + "type": "string" + }, + { + "by_ref": false, + "default": "null", + "name": "timeout", + "optional": true, + "type": "float" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "preg_replace", - "sub_area": "Regex" + "slug": "pfsockopen", + "sub_area": "Streams" }, { - "area": "Regex", - "canonical_name": "preg_split", - "description": "Lowers `preg_split(pattern, subject, limit?, flags?)` through the regex split helper.", + "area": "Misc", + "canonical_name": "php_uname", + "description": "Returns information about the operating system PHP is running on.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/regex.rs", - "codegen_function": "lower_preg_split", - "codegen_line": 374, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_php_uname", + "codegen_line": 672, "notes": [ - "Lowers `preg_split(pattern, subject, limit?, flags?)` through the regex split helper." + "Lowers `php_uname(mode?)` through the target-aware uname runtime helper." ], "runtime_helpers": [ - "__rt_preg_split" + "__rt_php_uname" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/php_uname.rs", "sig_line": null }, - "name": "preg_split", + "name": "php_uname", "sig": { "params": [ { "by_ref": false, - "default": null, - "name": "pattern", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "subject", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "limit", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "flags", + "default": "'a'", + "name": "mode", "optional": true, - "type": "int" + "type": "string" } ], - "return_type": "array", + "return_type": "string", "variadic": null }, - "slug": "preg_split", - "sub_area": "Regex" + "slug": "php_uname", + "sub_area": "Info" }, { "area": "Misc", - "canonical_name": "print_r", - "description": "Prints human-readable information about a variable.", + "canonical_name": "phpversion", + "description": "Returns the current PHP version information.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/debug.rs", - "codegen_function": "lower_print_r", - "codegen_line": 24, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_phpversion", + "codegen_line": 251, "notes": [ - "Lowers `print_r(value)` for concrete scalar/resource values and array/hash shells." + "Lowers `phpversion()` as the compiler package version string." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/phpversion.rs", "sig_line": null }, - "name": "print_r", + "name": "phpversion", "sig": { "params": [], - "return_type": "void", - "variadic": "values" + "return_type": "string", + "variadic": null }, - "slug": "print_r", - "sub_area": "Variable" + "slug": "phpversion", + "sub_area": "Info" }, { - "area": "String", - "canonical_name": "printf", - "description": "Lowers `printf(format, values...)` as `sprintf()` followed by stdout emission.", + "area": "Math", + "canonical_name": "pi", + "description": "Gets value of pi.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_printf", - "codegen_line": 517, + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_pi", + "codegen_line": 240, "notes": [ - "Lowers `printf(format, values...)` as `sprintf()` followed by stdout emission." - ], - "runtime_helpers": [ - "__rt_sprintf" + "Lowers `pi()` as a data-section float constant." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/pi.rs", "sig_line": null }, - "name": "printf", + "name": "pi", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "format", - "optional": false, - "type": "string" - } - ], - "return_type": "int", - "variadic": "values" + "params": [], + "return_type": "float", + "variadic": null }, - "slug": "printf", - "sub_area": "String" + "slug": "pi", + "sub_area": "Math" }, { - "area": "Pointer", - "canonical_name": "ptr", - "description": "Lowers `ptr(value)` by materializing the address of addressable local/global storage.", + "area": "Process", + "canonical_name": "popen", + "description": "Opens process file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr", - "codegen_line": 25, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_popen", + "codegen_line": 3602, "notes": [ - "Lowers `ptr(value)` by materializing the address of addressable local/global storage." + "Lowers `popen(command, mode)` and boxes the process pipe as `resource|false`." + ], + "runtime_helpers": [ + "__rt_popen" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/popen.rs", "sig_line": null }, - "name": "ptr", + "name": "popen", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "value", + "name": "command", "optional": false, - "type": "mixed" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "mode", + "optional": false, + "type": "string" } ], "return_type": "mixed", "variadic": null }, - "slug": "ptr", - "sub_area": "Pointer" + "slug": "popen", + "sub_area": "Process" }, { - "area": "Pointer", - "canonical_name": "ptr_get", - "description": "Lowers `ptr_get(pointer)` by reading one machine word through a checked pointer.", + "area": "Math", + "canonical_name": "pow", + "description": "Exponential expression.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_get", - "codegen_line": 109, + "codegen_file": "src/codegen/lower_inst/builtins/math/binary.rs", + "codegen_function": "lower_pow", + "codegen_line": 121, "notes": [ - "Lowers `ptr_get(pointer)` by reading one machine word through a checked pointer." + "Lowers `pow()` for concrete integer-like and floating operands." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/pow.rs", "sig_line": null }, - "name": "ptr_get", + "name": "pow", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pointer", + "name": "num", "optional": false, - "type": "pointer" + "type": "float" + }, + { + "by_ref": false, + "default": null, + "name": "exponent", + "optional": false, + "type": "float" } ], - "return_type": "int", + "return_type": "float", "variadic": null }, - "slug": "ptr_get", - "sub_area": "Pointer" + "slug": "pow", + "sub_area": "Math" }, { - "area": "Pointer", - "canonical_name": "ptr_is_null", - "description": "Lowers `ptr_is_null(pointer)` by comparing the raw pointer address to zero.", + "area": "Regex", + "canonical_name": "preg_match", + "description": "Performs a regular expression match.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_is_null", - "codegen_line": 56, + "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", + "codegen_function": "lower_preg_match", + "codegen_line": 28, "notes": [ - "Lowers `ptr_is_null(pointer)` by comparing the raw pointer address to zero." + "Lowers `preg_match(pattern, subject)` through the shared regex runtime helper." + ], + "runtime_helpers": [ + "__rt_preg_match", + "__rt_preg_match_all", + "__rt_preg_match_capture" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/preg_match.rs", "sig_line": null }, - "name": "ptr_is_null", + "name": "preg_match", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pointer", + "name": "pattern", "optional": false, - "type": "pointer" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false, + "type": "string" + }, + { + "by_ref": true, + "default": "[]", + "name": "matches", + "optional": true, + "type": "array" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "ptr_is_null", - "sub_area": "Pointer" + "slug": "preg_match", + "sub_area": "Regex" }, { - "area": "Pointer", - "canonical_name": "ptr_null", - "description": "Lowers `ptr_null()` by materializing the raw null pointer sentinel.", + "area": "Regex", + "canonical_name": "preg_match_all", + "description": "Performs a global regular expression match and returns the number of matches.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_null", + "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", + "codegen_function": "lower_preg_match_all", "codegen_line": 49, "notes": [ - "Lowers `ptr_null()` by materializing the raw null pointer sentinel." + "Lowers `preg_match_all(pattern, subject)` through the shared regex runtime helper." + ], + "runtime_helpers": [ + "__rt_preg_match_all", + "__rt_preg_replace" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/preg_match_all.rs", "sig_line": null }, - "name": "ptr_null", + "name": "preg_match_all", "sig": { - "params": [], - "return_type": "mixed", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false, + "type": "string" + } + ], + "return_type": "int", "variadic": null }, - "slug": "ptr_null", - "sub_area": "Pointer" + "slug": "preg_match_all", + "sub_area": "Regex" }, { - "area": "Pointer", - "canonical_name": "ptr_offset", - "description": "Lowers `ptr_offset(pointer, offset)` by adding a byte offset to a raw address.", + "area": "Regex", + "canonical_name": "preg_replace", + "description": "Performs a regular expression search and replace.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_offset", - "codegen_line": 86, + "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", + "codegen_function": "lower_preg_replace", + "codegen_line": 62, "notes": [ - "Lowers `ptr_offset(pointer, offset)` by adding a byte offset to a raw address." + "Lowers `preg_replace(pattern, replacement, subject)` through the regex replacement helper." + ], + "runtime_helpers": [ + "__rt_preg_replace" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/preg_replace.rs", "sig_line": null }, - "name": "ptr_offset", + "name": "preg_replace", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pointer", + "name": "pattern", "optional": false, - "type": "pointer" + "type": "string" }, { "by_ref": false, "default": null, - "name": "offset", + "name": "replacement", "optional": false, - "type": "int" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false, + "type": "string" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "ptr_offset", - "sub_area": "Pointer" + "slug": "preg_replace", + "sub_area": "Regex" }, { - "area": "Pointer", - "canonical_name": "ptr_read16", - "description": "Lowers `ptr_read16(pointer)` by reading one unsigned 16-bit word through a checked pointer.", + "area": "Regex", + "canonical_name": "preg_replace_callback", + "description": "Performs a regular expression search and replace using a callback.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_read16", - "codegen_line": 124, + "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", + "codegen_function": "lower_preg_replace_callback", + "codegen_line": 84, "notes": [ - "Lowers `ptr_read16(pointer)` by reading one unsigned 16-bit word through a checked pointer." + "Lowers `preg_replace_callback(pattern, callback, subject)` through supported direct callbacks." ], "runtime_helpers": [ - "__rt_ptr_read_string" + "__rt_preg_replace_callback" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/preg_replace_callback.rs", "sig_line": null }, - "name": "ptr_read16", + "name": "preg_replace_callback", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pointer", + "name": "pattern", "optional": false, - "type": "pointer" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false, + "type": "callable" + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false, + "type": "string" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "ptr_read16", - "sub_area": "Pointer" + "slug": "preg_replace_callback", + "sub_area": "Regex" }, { - "area": "Pointer", - "canonical_name": "ptr_read32", - "description": "Lowers `ptr_read32(pointer)` by reading one unsigned 32-bit word through a checked pointer.", + "area": "Regex", + "canonical_name": "preg_split", + "description": "Splits a string by a regular expression.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_read32", - "codegen_line": 129, + "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", + "codegen_function": "lower_preg_split", + "codegen_line": 388, "notes": [ - "Lowers `ptr_read32(pointer)` by reading one unsigned 32-bit word through a checked pointer." + "Lowers `preg_split(pattern, subject, limit?, flags?)` through the regex split helper." ], "runtime_helpers": [ - "__rt_ptr_read_string" + "__rt_preg_split" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/preg_split.rs", "sig_line": null }, - "name": "ptr_read32", + "name": "preg_split", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pointer", + "name": "pattern", "optional": false, - "type": "pointer" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "-1", + "name": "limit", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true, + "type": "int" } ], - "return_type": "int", + "return_type": "array", "variadic": null }, - "slug": "ptr_read32", - "sub_area": "Pointer" + "slug": "preg_split", + "sub_area": "Regex" }, { - "area": "Pointer", - "canonical_name": "ptr_read8", - "description": "Lowers `ptr_read8(pointer)` by reading one unsigned byte through a checked pointer.", + "area": "Misc", + "canonical_name": "print_r", + "description": "Prints human-readable information about a variable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_read8", - "codegen_line": 119, + "codegen_file": "src/codegen/lower_inst/builtins/debug.rs", + "codegen_function": "lower_print_r", + "codegen_line": 24, "notes": [ - "Lowers `ptr_read8(pointer)` by reading one unsigned byte through a checked pointer." + "Lowers `print_r(value)` for concrete scalar/resource values and array/hash shells." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/print_r.rs", "sig_line": null }, - "name": "ptr_read8", + "name": "print_r", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pointer", + "name": "value", "optional": false, - "type": "pointer" + "type": "mixed" } ], - "return_type": "int", + "return_type": "void", "variadic": null }, - "slug": "ptr_read8", - "sub_area": "Pointer" + "slug": "print_r", + "sub_area": "Variable" }, { - "area": "Pointer", - "canonical_name": "ptr_read_string", - "description": "Lowers `ptr_read_string(pointer, length)` by copying raw bytes into an owned PHP string.", + "area": "String", + "canonical_name": "printf", + "description": "Outputs a formatted string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_read_string", - "codegen_line": 134, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_printf", + "codegen_line": 517, "notes": [ - "Lowers `ptr_read_string(pointer, length)` by copying raw bytes into an owned PHP string." + "Lowers `printf(format, values...)` as `sprintf()` followed by stdout emission." ], "runtime_helpers": [ - "__rt_ptr_read_string" + "__rt_sprintf" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/printf.rs", "sig_line": null }, - "name": "ptr_read_string", + "name": "printf", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "pointer", - "optional": false, - "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "length", + "name": "format", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "string", - "variadic": null + "return_type": "int", + "variadic": "values" }, - "slug": "ptr_read_string", - "sub_area": "Pointer" + "slug": "printf", + "sub_area": "String" }, { "area": "Pointer", - "canonical_name": "ptr_set", - "description": "Lowers `ptr_set(pointer, value)` by writing one machine word through a checked pointer.", + "canonical_name": "ptr", + "description": "Returns a raw pointer to the given variable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_set", - "codegen_line": 114, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr", + "codegen_line": 25, "notes": [ - "Lowers `ptr_set(pointer, value)` by writing one machine word through a checked pointer." + "Lowers `ptr(value)` by materializing the address of addressable local/global storage." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr.rs", "sig_line": null }, - "name": "ptr_set", + "name": "ptr", "sig": { "params": [ - { - "by_ref": false, - "default": null, - "name": "pointer", - "optional": false, - "type": "pointer" - }, { "by_ref": false, "default": null, @@ -11556,72 +12641,70 @@ "type": "mixed" } ], - "return_type": "void", + "return_type": "mixed", "variadic": null }, - "slug": "ptr_set", + "slug": "ptr", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_sizeof", - "description": "Lowers `ptr_sizeof(\"type\")` by materializing the checked static byte size.", + "canonical_name": "ptr_get", + "description": "Reads one machine word through a raw pointer and returns it as an integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_sizeof", - "codegen_line": 75, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_get", + "codegen_line": 109, "notes": [ - "Lowers `ptr_sizeof(\"type\")` by materializing the checked static byte size." + "Lowers `ptr_get(pointer)` by reading one machine word through a checked pointer." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_get.rs", "sig_line": null }, - "name": "ptr_sizeof", + "name": "ptr_get", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "type", + "name": "pointer", "optional": false, - "type": "string" + "type": "pointer" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "ptr_sizeof", + "slug": "ptr_get", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_write16", - "description": "Lowers `ptr_write16(pointer, value)` by writing one 16-bit word through a checked pointer.", + "canonical_name": "ptr_is_null", + "description": "Returns true if the pointer is null.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_write16", - "codegen_line": 161, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_is_null", + "codegen_line": 56, "notes": [ - "Lowers `ptr_write16(pointer, value)` by writing one 16-bit word through a checked pointer." - ], - "runtime_helpers": [ - "__rt_ptr_write_string" + "Lowers `ptr_is_null(pointer)` by comparing the raw pointer address to zero." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_is_null.rs", "sig_line": null }, - "name": "ptr_write16", + "name": "ptr_is_null", "sig": { "params": [ { @@ -11630,88 +12713,64 @@ "name": "pointer", "optional": false, "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "value", - "optional": false, - "type": "int" } ], - "return_type": "void", + "return_type": "bool", "variadic": null }, - "slug": "ptr_write16", + "slug": "ptr_is_null", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_write32", - "description": "Lowers `ptr_write32(pointer, value)` by writing one 32-bit word through a checked pointer.", + "canonical_name": "ptr_null", + "description": "Returns a null raw pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_write32", - "codegen_line": 166, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_null", + "codegen_line": 49, "notes": [ - "Lowers `ptr_write32(pointer, value)` by writing one 32-bit word through a checked pointer." - ], - "runtime_helpers": [ - "__rt_ptr_write_string" + "Lowers `ptr_null()` by materializing the raw null pointer sentinel." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_null.rs", "sig_line": null }, - "name": "ptr_write32", + "name": "ptr_null", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "pointer", - "optional": false, - "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "value", - "optional": false, - "type": "int" - } - ], - "return_type": "void", + "params": [], + "return_type": "mixed", "variadic": null }, - "slug": "ptr_write32", + "slug": "ptr_null", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_write8", - "description": "Lowers `ptr_write8(pointer, value)` by writing one byte through a checked pointer.", + "canonical_name": "ptr_offset", + "description": "Returns a new pointer offset from the given pointer by the given byte count.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_write8", - "codegen_line": 156, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_offset", + "codegen_line": 86, "notes": [ - "Lowers `ptr_write8(pointer, value)` by writing one byte through a checked pointer." + "Lowers `ptr_offset(pointer, offset)` by adding a byte offset to a raw address." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_offset.rs", "sig_line": null }, - "name": "ptr_write8", + "name": "ptr_offset", "sig": { "params": [ { @@ -11724,40 +12783,40 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "offset", "optional": false, "type": "int" } ], - "return_type": "void", + "return_type": "mixed", "variadic": null }, - "slug": "ptr_write8", + "slug": "ptr_offset", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_write_string", - "description": "Lowers `ptr_write_string(pointer, string)` by copying PHP string bytes into raw memory.", + "canonical_name": "ptr_read16", + "description": "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/pointers.rs", - "codegen_function": "lower_ptr_write_string", - "codegen_line": 171, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_read16", + "codegen_line": 124, "notes": [ - "Lowers `ptr_write_string(pointer, string)` by copying PHP string bytes into raw memory." + "Lowers `ptr_read16(pointer)` by reading one unsigned 16-bit word through a checked pointer." ], "runtime_helpers": [ - "__rt_ptr_write_string" + "__rt_ptr_read_string" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_read16.rs", "sig_line": null }, - "name": "ptr_write_string", + "name": "ptr_read16", "sig": { "params": [ { @@ -11766,2347 +12825,2238 @@ "name": "pointer", "optional": false, "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "string", - "optional": false, - "type": "string" } ], "return_type": "int", "variadic": null }, - "slug": "ptr_write_string", + "slug": "ptr_read16", "sub_area": "Pointer" }, { - "area": "Filesystem", - "canonical_name": "putenv", - "description": "Lowers `putenv(assignment)` by copying the environment string into persistent heap storage.", + "area": "Pointer", + "canonical_name": "ptr_read32", + "description": "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_putenv", - "codegen_line": 657, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_read32", + "codegen_line": 129, "notes": [ - "Lowers `putenv(assignment)` by copying the environment string into persistent heap storage." + "Lowers `ptr_read32(pointer)` by reading one unsigned 32-bit word through a checked pointer." ], "runtime_helpers": [ - "__rt_php_uname" + "__rt_ptr_read_string" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_read32.rs", "sig_line": null }, - "name": "putenv", + "name": "ptr_read32", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "assignment", + "name": "pointer", "optional": false, - "type": "string" + "type": "pointer" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "putenv", - "sub_area": "Filesystem" + "slug": "ptr_read32", + "sub_area": "Pointer" }, { - "area": "Math", - "canonical_name": "rad2deg", - "description": "Lowers `rad2deg()` by multiplying with `180 / PI`.", + "area": "Pointer", + "canonical_name": "ptr_read8", + "description": "Reads one unsigned byte through a raw pointer and returns it as an integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/libm.rs", - "codegen_function": "lower_rad2deg", - "codegen_line": 83, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_read8", + "codegen_line": 119, "notes": [ - "Lowers `rad2deg()` by multiplying with `180 / PI`." + "Lowers `ptr_read8(pointer)` by reading one unsigned byte through a checked pointer." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_read8.rs", "sig_line": null }, - "name": "rad2deg", + "name": "ptr_read8", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "pointer", "optional": false, - "type": "float" + "type": "pointer" } ], - "return_type": "float", + "return_type": "int", "variadic": null }, - "slug": "rad2deg", - "sub_area": "Math" + "slug": "ptr_read8", + "sub_area": "Pointer" }, { - "area": "Math", - "canonical_name": "rand", - "description": "", + "area": "Pointer", + "canonical_name": "ptr_read_string", + "description": "Copies raw bytes from a pointer into a PHP string of the given length.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_read_string", + "codegen_line": 134, + "notes": [ + "Lowers `ptr_read_string(pointer, length)` by copying raw bytes into an owned PHP string." + ], + "runtime_helpers": [ + "__rt_ptr_read_string" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/pointers/ptr_read_string.rs", "sig_line": null }, - "name": "rand", + "name": "ptr_read_string", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "min", + "name": "pointer", "optional": false, - "type": "int" + "type": "pointer" }, { "by_ref": false, "default": null, - "name": "max", + "name": "length", "optional": false, "type": "int" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "rand", - "sub_area": "Math" + "slug": "ptr_read_string", + "sub_area": "Pointer" }, { - "area": "Math", - "canonical_name": "random_int", - "description": "Lowers `random_int()` over an inclusive integer range.", + "area": "Pointer", + "canonical_name": "ptr_set", + "description": "Writes one machine word through a raw pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math/random.rs", - "codegen_function": "lower_random_int", - "codegen_line": 40, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_set", + "codegen_line": 114, "notes": [ - "Lowers `random_int()` over an inclusive integer range." + "Lowers `ptr_set(pointer, value)` by writing one machine word through a checked pointer." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_set.rs", "sig_line": null }, - "name": "random_int", + "name": "ptr_set", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "min", + "name": "pointer", "optional": false, - "type": "int" + "type": "pointer" }, { "by_ref": false, "default": null, - "name": "max", + "name": "value", "optional": false, - "type": "int" + "type": "mixed" } ], - "return_type": "int", + "return_type": "void", "variadic": null }, - "slug": "random_int", - "sub_area": "Math" + "slug": "ptr_set", + "sub_area": "Pointer" }, { - "area": "Array", - "canonical_name": "range", - "description": "Lowers `range()` for integer endpoints through the shared runtime constructor.", + "area": "Pointer", + "canonical_name": "ptr_sizeof", + "description": "Returns the byte size of the named pointer target type.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_range", - "codegen_line": 1020, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_sizeof", + "codegen_line": 75, "notes": [ - "Lowers `range()` for integer endpoints through the shared runtime constructor." - ], - "runtime_helpers": [ - "__rt_mixed_cast_int", - "__rt_range" + "Lowers `ptr_sizeof(\"type\")` by materializing the checked static byte size." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_sizeof.rs", "sig_line": null }, - "name": "range", + "name": "ptr_sizeof", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "start", - "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "end", - "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "step", + "name": "type", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "array", + "return_type": "int", "variadic": null }, - "slug": "range", - "sub_area": "Array" + "slug": "ptr_sizeof", + "sub_area": "Pointer" }, { - "area": "String", - "canonical_name": "rawurldecode", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "area": "Pointer", + "canonical_name": "ptr_write16", + "description": "Writes one 16-bit word through a raw pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_write16", + "codegen_line": 161, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." + "Lowers `ptr_write16(pointer, value)` by writing one 16-bit word through a checked pointer." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_ptr_write_string" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_write16.rs", "sig_line": null }, - "name": "rawurldecode", + "name": "ptr_write16", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "pointer", "optional": false, - "type": "string" + "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "int" } ], - "return_type": "string", + "return_type": "void", "variadic": null }, - "slug": "rawurldecode", - "sub_area": "String" + "slug": "ptr_write16", + "sub_area": "Pointer" }, { - "area": "String", - "canonical_name": "rawurlencode", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "area": "Pointer", + "canonical_name": "ptr_write32", + "description": "Writes one 32-bit word through a raw pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_write32", + "codegen_line": 166, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." + "Lowers `ptr_write32(pointer, value)` by writing one 32-bit word through a checked pointer." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_ptr_write_string" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_write32.rs", "sig_line": null }, - "name": "rawurlencode", + "name": "ptr_write32", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "pointer", "optional": false, - "type": "string" + "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "int" } ], - "return_type": "string", + "return_type": "void", "variadic": null }, - "slug": "rawurlencode", - "sub_area": "String" + "slug": "ptr_write32", + "sub_area": "Pointer" }, { - "area": "Process", - "canonical_name": "readline", - "description": "Lowers `readline(prompt?)` by optionally writing a prompt and reading stdin.", + "area": "Pointer", + "canonical_name": "ptr_write8", + "description": "Writes one byte through a raw pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_readline", - "codegen_line": 203, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_write8", + "codegen_line": 156, "notes": [ - "Lowers `readline(prompt?)` by optionally writing a prompt and reading stdin." - ], - "runtime_helpers": [ - "__rt_fgets" + "Lowers `ptr_write8(pointer, value)` by writing one byte through a checked pointer." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_write8.rs", "sig_line": null }, - "name": "readline", + "name": "ptr_write8", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "prompt", - "optional": true, - "type": "string" + "name": "pointer", + "optional": false, + "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "int" } ], - "return_type": "mixed", + "return_type": "void", "variadic": null }, - "slug": "readline", - "sub_area": "Process" + "slug": "ptr_write8", + "sub_area": "Pointer" }, { - "area": "Filesystem", - "canonical_name": "readlink", - "description": "Lowers `readlink(path)` and boxes the owned runtime string-or-false result.", + "area": "Pointer", + "canonical_name": "ptr_write_string", + "description": "Copies PHP string bytes into raw memory at the given pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_readlink", - "codegen_line": 4815, + "codegen_file": "src/codegen/lower_inst/builtins/pointers.rs", + "codegen_function": "lower_ptr_write_string", + "codegen_line": 171, "notes": [ - "Lowers `readlink(path)` and boxes the owned runtime string-or-false result." + "Lowers `ptr_write_string(pointer, string)` by copying PHP string bytes into raw memory." ], "runtime_helpers": [ - "__rt_fileatime", - "__rt_filectime", - "__rt_fileperms", - "__rt_readlink" + "__rt_ptr_write_string" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/pointers/ptr_write_string.rs", "sig_line": null }, - "name": "readlink", + "name": "ptr_write_string", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "path", + "name": "pointer", + "optional": false, + "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "string", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "readlink", - "sub_area": "Filesystem" + "slug": "ptr_write_string", + "sub_area": "Pointer" }, { "area": "Filesystem", - "canonical_name": "realpath", - "description": "Lowers `realpath(path)` and boxes the owned runtime string-or-false result.", + "canonical_name": "putenv", + "description": "Sets an environment variable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_realpath", - "codegen_line": 3465, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_putenv", + "codegen_line": 657, "notes": [ - "Lowers `realpath(path)` and boxes the owned runtime string-or-false result." + "Lowers `putenv(assignment)` by copying the environment string into persistent heap storage." ], "runtime_helpers": [ - "__rt_realpath" + "__rt_php_uname" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/putenv.rs", "sig_line": null }, - "name": "realpath", + "name": "putenv", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "path", + "name": "assignment", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "realpath", + "slug": "putenv", "sub_area": "Filesystem" }, { - "area": "Filesystem", - "canonical_name": "realpath_cache_get", - "description": "Lowers `realpath_cache_get()` to elephc's empty realpath-cache view.", + "area": "Math", + "canonical_name": "rad2deg", + "description": "Converts a radian value to degrees.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_realpath_cache_get", - "codegen_line": 3475, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_rad2deg", + "codegen_line": 83, "notes": [ - "Lowers `realpath_cache_get()` to elephc's empty realpath-cache view." + "Lowers `rad2deg()` by multiplying with `180 / PI`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/rad2deg.rs", "sig_line": null }, - "name": "realpath_cache_get", + "name": "rad2deg", "sig": { - "params": [], - "return_type": "array", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "float" + } + ], + "return_type": "float", "variadic": null }, - "slug": "realpath_cache_get", - "sub_area": "Filesystem" + "slug": "rad2deg", + "sub_area": "Math" }, { - "area": "Filesystem", - "canonical_name": "realpath_cache_size", - "description": "Lowers `realpath_cache_size()` to zero because elephc has no realpath cache.", + "area": "Math", + "canonical_name": "rand", + "description": "Generate a random integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_realpath_cache_size", - "codegen_line": 3485, + "codegen_file": "src/codegen/lower_inst/builtins/math/random.rs", + "codegen_function": "lower_rand", + "codegen_line": 22, "notes": [ - "Lowers `realpath_cache_size()` to zero because elephc has no realpath cache." + "Lowers `rand()` and `mt_rand()` with either zero args or an inclusive range." + ], + "runtime_helpers": [ + "__rt_random_u32" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/rand.rs", "sig_line": null }, - "name": "realpath_cache_size", + "name": "rand", "sig": { - "params": [], + "params": [ + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false, + "type": "int" + } + ], "return_type": "int", "variadic": null }, - "slug": "realpath_cache_size", - "sub_area": "Filesystem" + "slug": "rand", + "sub_area": "Math" }, { - "area": "Filesystem", - "canonical_name": "rename", - "description": "Lowers `rename(from, to)` through the target-aware runtime helper.", + "area": "Math", + "canonical_name": "random_bytes", + "description": "Get a cryptographically secure random string of the given length.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_rename", - "codegen_line": 3805, + "codegen_file": "src/codegen_ir/lower_inst/builtins/math/random.rs", + "codegen_function": "lower_random_bytes", + "codegen_line": 58, "notes": [ - "Lowers `rename(from, to)` through the target-aware runtime helper." + "Lowers `random_bytes()` into an owned CSPRNG binary string of the given length.", + "Materializes the single length operand as an integer, passes it to the", + "`__rt_random_bytes` runtime helper (length in `x0` on AArch64, `rdi` on", + "x86_64), and stores the returned owned string result (`x1`/`x2` on AArch64,", + "`rax`/`rdx` on x86_64) into the instruction's result slot. The runtime helper", + "owns allocation, the cryptographic fill, and the fatal paths for a length", + "below 1 or an unavailable entropy source." ], "runtime_helpers": [ - "__rt_glob", - "__rt_scandir", - "__rt_tempnam" + "__rt_random_bytes" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/random_bytes.rs", "sig_line": null }, - "name": "rename", + "name": "random_bytes", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "from", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "to", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "context", + "name": "length", "optional": false, - "type": "mixed" + "type": "int" } ], - "return_type": "bool", + "return_type": "string", "variadic": null }, - "slug": "rename", - "sub_area": "Filesystem" + "slug": "random_bytes", + "sub_area": "Math" }, { - "area": "IO", - "canonical_name": "rewind", - "description": "Lowers `rewind(stream)` as `lseek(fd, 0, SEEK_SET)` and clears EOF state on success.", + "area": "Math", + "canonical_name": "random_int", + "description": "Get a cryptographically secure, uniformly selected integer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_rewind", - "codegen_line": 2975, + "codegen_file": "src/codegen/lower_inst/builtins/math/random.rs", + "codegen_function": "lower_random_int", + "codegen_line": 41, "notes": [ - "Lowers `rewind(stream)` as `lseek(fd, 0, SEEK_SET)` and clears EOF state on success." + "Lowers `random_int()` over an inclusive integer range." + ], + "runtime_helpers": [ + "__rt_random_bytes" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/random_int.rs", "sig_line": null }, - "name": "rewind", + "name": "random_int", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "min", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "max", "optional": false, - "type": "resource" + "type": "int" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "rewind", - "sub_area": "IO" + "slug": "random_int", + "sub_area": "Math" }, { - "area": "Filesystem", - "canonical_name": "rmdir", - "description": "Lowers `rmdir(path)` through the target-aware runtime helper.", + "area": "Array", + "canonical_name": "range", + "description": "Create an array containing a range of elements.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_rmdir", - "codegen_line": 3790, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_range", + "codegen_line": 1023, "notes": [ - "Lowers `rmdir(path)` through the target-aware runtime helper." + "Lowers `range()` for integer endpoints through the shared runtime constructor." ], "runtime_helpers": [ - "__rt_chdir", - "__rt_copy", - "__rt_rmdir", - "__rt_scandir", - "__rt_tempnam" + "__rt_mixed_cast_int", + "__rt_range" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/range.rs", "sig_line": null }, - "name": "rmdir", + "name": "range", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "directory", + "name": "start", "optional": false, - "type": "string" + "type": "mixed" }, { "by_ref": false, - "default": "null", - "name": "context", - "optional": true, + "default": null, + "name": "end", + "optional": false, "type": "mixed" } ], - "return_type": "bool", + "return_type": "array", "variadic": null }, - "slug": "rmdir", - "sub_area": "Filesystem" + "slug": "range", + "sub_area": "Array" }, { - "area": "Math", - "canonical_name": "round", - "description": "Lowers `round()` for concrete integer-like and floating operands.", + "area": "String", + "canonical_name": "rawurldecode", + "description": "Decodes an RFC 3986 percent-encoded string without treating '+' as a space.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_round", - "codegen_line": 186, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `round()` for concrete integer-like and floating operands." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." + ], + "runtime_helpers": [ + "__rt_grapheme_strrev", + "__rt_strcopy" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/rawurldecode.rs", "sig_line": null }, - "name": "round", + "name": "rawurldecode", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "string", "optional": false, - "type": "float" - }, - { - "by_ref": false, - "default": null, - "name": "precision", - "optional": true, - "type": "int" + "type": "string" } ], - "return_type": "float", + "return_type": "string", "variadic": null }, - "slug": "round", - "sub_area": "Math" + "slug": "rawurldecode", + "sub_area": "String" }, { - "area": "Array", - "canonical_name": "rsort", - "description": "Lowers `rsort()` for indexed integer arrays by mutating the source array in place.", + "area": "String", + "canonical_name": "rawurlencode", + "description": "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces).", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_rsort", - "codegen_line": 1081, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `rsort()` for indexed integer arrays by mutating the source array in place." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_arsort", - "__rt_asort", - "__rt_krsort", - "__rt_ksort", - "__rt_natsort", - "__rt_rsort_int", - "__rt_rsort_str" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/rawurlencode.rs", "sig_line": null }, - "name": "rsort", + "name": "rawurlencode", "sig": { "params": [ - { - "by_ref": true, - "default": null, - "name": "array", - "optional": false, - "type": "array" - }, { "by_ref": false, "default": null, - "name": "flags", + "name": "string", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "bool", + "return_type": "string", "variadic": null }, - "slug": "rsort", - "sub_area": "Array" + "slug": "rawurlencode", + "sub_area": "String" }, { - "area": "String", - "canonical_name": "rtrim", - "description": "", + "area": "IO", + "canonical_name": "readdir", + "description": "Read entry from directory handle.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_readdir", + "codegen_line": 3557, + "notes": [ + "Lowers `readdir(dir_handle)` for libc, glob, and userspace-wrapper handles." + ], + "runtime_helpers": [ + "__rt_closedir", + "__rt_readdir", + "__rt_user_wrapper_dir_closedir", + "__rt_user_wrapper_dir_readdir" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/readdir.rs", "sig_line": null }, - "name": "rtrim", + "name": "readdir", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "dir_handle", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "characters", - "optional": true, - "type": "string" + "type": "resource" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "rtrim", - "sub_area": "String" + "slug": "readdir", + "sub_area": "IO" }, { "area": "Filesystem", - "canonical_name": "scandir", - "description": "Lowers `scandir(path)` through the target-aware runtime directory listing helper.", + "canonical_name": "readfile", + "description": "Outputs a file.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_scandir", - "codegen_line": 3815, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_readfile", + "codegen_line": 300, "notes": [ - "Lowers `scandir(path)` through the target-aware runtime directory listing helper." - ], - "runtime_helpers": [ - "__rt_glob", - "__rt_scandir" + "Lowers `readfile(path)` and boxes the runtime byte-count-or-false result." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/readfile.rs", "sig_line": null }, - "name": "scandir", + "name": "readfile", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "directory", + "name": "filename", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "sorting_order", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "context", - "optional": false, - "type": "mixed" } ], - "return_type": "array", + "return_type": "mixed", "variadic": null }, - "slug": "scandir", + "slug": "readfile", "sub_area": "Filesystem" }, { - "area": "Type", - "canonical_name": "settype", - "description": "Lowers `settype($local, \"type\")` by mutating the resolved local slot and returning true.", + "area": "Process", + "canonical_name": "readline", + "description": "Reads a line from the user's terminal.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", - "codegen_function": "lower_settype", - "codegen_line": 25, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_readline", + "codegen_line": 310, "notes": [ - "Lowers `settype($local, \"type\")` by mutating the resolved local slot and returning true." + "Lowers `readline(prompt?)` by optionally writing a prompt and reading stdin." + ], + "runtime_helpers": [ + "__rt_fgets" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/readline.rs", "sig_line": null }, - "name": "settype", + "name": "readline", "sig": { "params": [ - { - "by_ref": true, - "default": null, - "name": "var", - "optional": false, - "type": "mixed" - }, { "by_ref": false, - "default": null, - "name": "type", - "optional": false, + "default": "null", + "name": "prompt", + "optional": true, "type": "string" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "settype", - "sub_area": "Casts" + "slug": "readline", + "sub_area": "Process" }, { - "area": "String", - "canonical_name": "sha1", - "description": "Lowers `sha1(data, binary?)` through the shared crypto-backed runtime helper.", + "area": "Filesystem", + "canonical_name": "readlink", + "description": "Returns the target of a symbolic link.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_sha1", - "codegen_line": 360, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_readlink", + "codegen_line": 5458, "notes": [ - "Lowers `sha1(data, binary?)` through the shared crypto-backed runtime helper." + "Lowers `readlink(path)` and boxes the owned runtime string-or-false result." ], "runtime_helpers": [ - "__rt_hash", - "__rt_sha1" + "__rt_fileatime", + "__rt_filectime", + "__rt_fileperms", + "__rt_readlink" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/readlink.rs", "sig_line": null }, - "name": "sha1", + "name": "readlink", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "path", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "binary", - "optional": true, - "type": "bool" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "sha1", - "sub_area": "String" + "slug": "readlink", + "sub_area": "Filesystem" }, { - "area": "Process", - "canonical_name": "shell_exec", - "description": "Lowers `shell_exec(command)` by capturing shell stdout through the shared runtime helper.", + "area": "Filesystem", + "canonical_name": "realpath", + "description": "Returns canonicalized absolute pathname.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_shell_exec", - "codegen_line": 698, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_realpath", + "codegen_line": 3690, "notes": [ - "Lowers `shell_exec(command)` by capturing shell stdout through the shared runtime helper." + "Lowers `realpath(path)` and boxes the owned runtime string-or-false result." + ], + "runtime_helpers": [ + "__rt_realpath" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/realpath.rs", "sig_line": null }, - "name": "shell_exec", + "name": "realpath", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "command", + "name": "path", "optional": false, "type": "string" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "shell_exec", - "sub_area": "Process" + "slug": "realpath", + "sub_area": "Filesystem" }, { - "area": "Array", - "canonical_name": "shuffle", - "description": "Lowers `shuffle()` for indexed arrays with 8-byte slots by mutating the source array in place.", + "area": "Filesystem", + "canonical_name": "realpath_cache_get", + "description": "Returns realpath cache entries.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_shuffle", - "codegen_line": 1116, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_realpath_cache_get", + "codegen_line": 3700, "notes": [ - "Lowers `shuffle()` for indexed arrays with 8-byte slots by mutating the source array in place." + "Lowers `realpath_cache_get()` to elephc's empty realpath-cache view." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/realpath_cache_get.rs", "sig_line": null }, - "name": "shuffle", + "name": "realpath_cache_get", "sig": { - "params": [ - { - "by_ref": true, - "default": null, - "name": "array", - "optional": false, - "type": "array" - } - ], - "return_type": "bool", + "params": [], + "return_type": "array", "variadic": null }, - "slug": "shuffle", - "sub_area": "Array" + "slug": "realpath_cache_get", + "sub_area": "Filesystem" }, { - "area": "Math", - "canonical_name": "sin", - "description": "", + "area": "Filesystem", + "canonical_name": "realpath_cache_size", + "description": "Returns the amount of memory used by the realpath cache.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_realpath_cache_size", + "codegen_line": 3710, + "notes": [ + "Lowers `realpath_cache_size()` to zero because elephc has no realpath cache." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/realpath_cache_size.rs", "sig_line": null }, - "name": "sin", + "name": "realpath_cache_size", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "num", - "optional": false, - "type": "float" - } - ], - "return_type": "float", + "params": [], + "return_type": "int", "variadic": null }, - "slug": "sin", - "sub_area": "Math" + "slug": "realpath_cache_size", + "sub_area": "Filesystem" }, { - "area": "SPL", - "canonical_name": "spl_autoload", - "description": "Lowers no-op autoload calls by preserving arg effects and returning PHP null if used.", + "area": "Filesystem", + "canonical_name": "rename", + "description": "Renames a file or directory.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_autoload_void", - "codegen_line": 150, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_rename", + "codegen_line": 4448, "notes": [ - "Lowers no-op autoload calls by preserving arg effects and returning PHP null if used." + "Lowers `rename(from, to)` through the target-aware runtime helper." + ], + "runtime_helpers": [ + "__rt_glob", + "__rt_scandir", + "__rt_tempnam" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/rename.rs", "sig_line": null }, - "name": "spl_autoload", + "name": "rename", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "class", + "name": "from", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "file_extensions", - "optional": true, + "name": "to", + "optional": false, "type": "string" } ], - "return_type": "void", + "return_type": "bool", "variadic": null }, - "slug": "spl_autoload", - "sub_area": "SPL" + "slug": "rename", + "sub_area": "Filesystem" }, { - "area": "SPL", - "canonical_name": "spl_autoload_call", - "description": "Lowers no-op autoload calls by preserving arg effects and returning PHP null if used.", + "area": "IO", + "canonical_name": "rewind", + "description": "Rewind the position of a file pointer.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_autoload_void", - "codegen_line": 150, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_rewind", + "codegen_line": 3196, "notes": [ - "Lowers no-op autoload calls by preserving arg effects and returning PHP null if used." + "Lowers `rewind(stream)` as `lseek(fd, 0, SEEK_SET)` and clears EOF state on success." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/rewind.rs", "sig_line": null }, - "name": "spl_autoload_call", + "name": "rewind", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "class", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" } ], - "return_type": "void", + "return_type": "bool", "variadic": null }, - "slug": "spl_autoload_call", - "sub_area": "SPL" + "slug": "rewind", + "sub_area": "IO" }, { - "area": "SPL", - "canonical_name": "spl_autoload_extensions", - "description": "Lowers `spl_autoload_extensions()` against the legacy mutable extension globals.", + "area": "IO", + "canonical_name": "rewinddir", + "description": "Rewind directory handle.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_autoload_extensions", - "codegen_line": 177, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_rewinddir", + "codegen_line": 3588, "notes": [ - "Lowers `spl_autoload_extensions()` against the legacy mutable extension globals." + "Lowers `rewinddir(dir_handle)` for libc, glob, and userspace-wrapper handles." + ], + "runtime_helpers": [ + "__rt_rewinddir", + "__rt_user_wrapper_dir_rewinddir" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/rewinddir.rs", "sig_line": null }, - "name": "spl_autoload_extensions", + "name": "rewinddir", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "file_extensions", - "optional": true, - "type": "string" + "name": "dir_handle", + "optional": false, + "type": "resource" } ], - "return_type": "string", + "return_type": "void", "variadic": null }, - "slug": "spl_autoload_extensions", - "sub_area": "SPL" + "slug": "rewinddir", + "sub_area": "IO" }, { - "area": "SPL", - "canonical_name": "spl_autoload_functions", - "description": "Lowers `spl_autoload_functions()` to an indexed array of AOT rule placeholders.", + "area": "Filesystem", + "canonical_name": "rmdir", + "description": "Removes a directory.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_autoload_functions", - "codegen_line": 166, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_rmdir", + "codegen_line": 4433, "notes": [ - "Lowers `spl_autoload_functions()` to an indexed array of AOT rule placeholders." + "Lowers `rmdir(path)` through the target-aware runtime helper." ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "spl_autoload_functions", - "sig": { - "params": [], - "return_type": "array", - "variadic": null - }, - "slug": "spl_autoload_functions", - "sub_area": "SPL" - }, - { - "area": "SPL", - "canonical_name": "spl_autoload_register", - "description": "Lowers autoload registration stubs by preserving arg effects and returning true.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_autoload_bool", - "codegen_line": 134, - "notes": [ - "Lowers autoload registration stubs by preserving arg effects and returning true." + "runtime_helpers": [ + "__rt_chdir", + "__rt_copy", + "__rt_rmdir", + "__rt_scandir", + "__rt_tempnam" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/rmdir.rs", "sig_line": null }, - "name": "spl_autoload_register", + "name": "rmdir", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "callback", - "optional": true, - "type": "callable" - }, - { - "by_ref": false, - "default": null, - "name": "throw", - "optional": true, - "type": "bool" - }, - { - "by_ref": false, - "default": null, - "name": "prepend", - "optional": true, - "type": "bool" + "name": "directory", + "optional": false, + "type": "string" } ], "return_type": "bool", "variadic": null }, - "slug": "spl_autoload_register", - "sub_area": "SPL" + "slug": "rmdir", + "sub_area": "Filesystem" }, { - "area": "SPL", - "canonical_name": "spl_autoload_unregister", - "description": "Lowers autoload registration stubs by preserving arg effects and returning true.", + "area": "Math", + "canonical_name": "round", + "description": "Rounds a float.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_autoload_bool", - "codegen_line": 134, + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_round", + "codegen_line": 186, "notes": [ - "Lowers autoload registration stubs by preserving arg effects and returning true." + "Lowers `round()` for concrete integer-like and floating operands." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/round.rs", "sig_line": null }, - "name": "spl_autoload_unregister", + "name": "round", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "callback", + "name": "num", "optional": false, - "type": "callable" + "type": "float" + }, + { + "by_ref": false, + "default": "0", + "name": "precision", + "optional": true, + "type": "int" } ], - "return_type": "bool", + "return_type": "float", "variadic": null }, - "slug": "spl_autoload_unregister", - "sub_area": "SPL" + "slug": "round", + "sub_area": "Math" }, { - "area": "SPL", - "canonical_name": "spl_classes", - "description": "Lowers `spl_classes()` to the static compiler-shipped SPL/core type snapshot.", + "area": "Array", + "canonical_name": "rsort", + "description": "Sorts an array in descending order.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_classes", - "codegen_line": 205, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_rsort", + "codegen_line": 1084, "notes": [ - "Lowers `spl_classes()` to the static compiler-shipped SPL/core type snapshot." + "Lowers `rsort()` for indexed integer arrays by mutating the source array in place." ], "runtime_helpers": [ - "__rt_itoa" + "__rt_arsort", + "__rt_asort", + "__rt_krsort", + "__rt_ksort", + "__rt_natsort", + "__rt_rsort_int", + "__rt_rsort_str" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/rsort.rs", "sig_line": null }, - "name": "spl_classes", + "name": "rsort", "sig": { - "params": [], - "return_type": "array", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false, + "type": "array" + } + ], + "return_type": "bool", "variadic": null }, - "slug": "spl_classes", - "sub_area": "SPL" + "slug": "rsort", + "sub_area": "Array" }, { - "area": "SPL", - "canonical_name": "spl_object_hash", - "description": "Lowers `spl_object_hash(object)` by formatting the loaded object pointer as a string.", + "area": "String", + "canonical_name": "rtrim", + "description": "Strips whitespace (or other characters) from the end of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_object_hash", - "codegen_line": 225, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_trim_like", + "codegen_line": 112, "notes": [ - "Lowers `spl_object_hash(object)` by formatting the loaded object pointer as a string." - ], - "runtime_helpers": [ - "__rt_itoa" + "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/rtrim.rs", "sig_line": null }, - "name": "spl_object_hash", + "name": "rtrim", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object", + "name": "string", "optional": false, - "type": "object" + "type": "string" + }, + { + "by_ref": false, + "default": "' \\n\\r\\t\\x0b\\x0c\\x00'", + "name": "characters", + "optional": true, + "type": "string" } ], "return_type": "string", "variadic": null }, - "slug": "spl_object_hash", - "sub_area": "SPL" + "slug": "rtrim", + "sub_area": "String" }, { - "area": "SPL", - "canonical_name": "spl_object_id", - "description": "Lowers `spl_object_id(object)` by returning the loaded object pointer as an integer.", + "area": "Filesystem", + "canonical_name": "scandir", + "description": "Lists files and directories inside the specified path.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/spl.rs", - "codegen_function": "lower_spl_object_id", - "codegen_line": 215, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_scandir", + "codegen_line": 4458, "notes": [ - "Lowers `spl_object_id(object)` by returning the loaded object pointer as an integer." + "Lowers `scandir(path)` through the target-aware runtime directory listing helper." ], "runtime_helpers": [ - "__rt_itoa" + "__rt_glob", + "__rt_scandir" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/scandir.rs", "sig_line": null }, - "name": "spl_object_id", + "name": "scandir", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "object", + "name": "directory", "optional": false, - "type": "object" + "type": "string" } ], - "return_type": "int", + "return_type": "array", "variadic": null }, - "slug": "spl_object_id", - "sub_area": "SPL" + "slug": "scandir", + "sub_area": "Filesystem" }, { - "area": "Math", - "canonical_name": "sinh", - "description": "", + "area": "Misc", + "canonical_name": "serialize", + "description": "Generates a storable representation of a value.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/serialize.rs", + "codegen_function": "lower_serialize", + "codegen_line": 33, + "notes": [ + "Lowers `serialize($value)` into the shared serialize runtime helper.", + "Scalar static types are formatted directly through `__rt_serialize_value`; a", + "Mixed/Union argument is unboxed and dispatched by `__rt_serialize_mixed`.", + "Non-scalar static types (arrays/objects) are not yet supported and are rejected." + ], + "runtime_helpers": [ + "__rt_serialize_begin", + "__rt_serialize_mixed", + "__rt_serialize_value" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/system/serialize.rs", "sig_line": null }, - "name": "sinh", + "name": "serialize", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "value", "optional": false, - "type": "float" + "type": "mixed" } ], - "return_type": "float", + "return_type": "string", "variadic": null }, - "slug": "sinh", - "sub_area": "Math" + "slug": "serialize", + "sub_area": "Misc" }, { - "area": "Process", - "canonical_name": "sleep", - "description": "Lowers `sleep(seconds)` through the target's C library symbol.", + "area": "Type", + "canonical_name": "settype", + "description": "Sets the type of a variable.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_sleep", - "codegen_line": 473, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_settype", + "codegen_line": 25, "notes": [ - "Lowers `sleep(seconds)` through the target's C library symbol." - ], - "runtime_helpers": [ - "__rt_strtotime" + "Lowers `settype($local, \"type\")` by mutating the resolved local slot and returning true." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/types/settype.rs", "sig_line": null }, - "name": "sleep", + "name": "settype", "sig": { "params": [ + { + "by_ref": true, + "default": null, + "name": "var", + "optional": false, + "type": "mixed" + }, { "by_ref": false, "default": null, - "name": "seconds", + "name": "type", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "int", + "return_type": "bool", "variadic": null }, - "slug": "sleep", - "sub_area": "Process" + "slug": "settype", + "sub_area": "Casts" }, { - "area": "Array", - "canonical_name": "sort", - "description": "Lowers `sort()` for indexed integer arrays by mutating the source array in place.", + "area": "String", + "canonical_name": "sha1", + "description": "Calculates the SHA-1 hash of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", - "codegen_function": "lower_sort", - "codegen_line": 1076, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_sha1", + "codegen_line": 360, "notes": [ - "Lowers `sort()` for indexed integer arrays by mutating the source array in place." + "Lowers `sha1(data, binary?)` through the shared crypto-backed runtime helper." ], "runtime_helpers": [ - "__rt_arsort", - "__rt_asort", - "__rt_krsort", - "__rt_ksort", - "__rt_rsort_int", - "__rt_rsort_str", - "__rt_sort_int", - "__rt_sort_str" + "__rt_hash", + "__rt_sha1" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/sha1.rs", "sig_line": null }, - "name": "sort", + "name": "sha1", "sig": { "params": [ { - "by_ref": true, + "by_ref": false, "default": null, - "name": "array", + "name": "string", "optional": false, - "type": "array" + "type": "string" }, { "by_ref": false, - "default": null, - "name": "flags", - "optional": false, - "type": "int" + "default": "false", + "name": "binary", + "optional": true, + "type": "bool" } ], - "return_type": "bool", + "return_type": "string", "variadic": null }, - "slug": "sort", - "sub_area": "Array" + "slug": "sha1", + "sub_area": "String" }, { - "area": "String", - "canonical_name": "sprintf", - "description": "Lowers `sprintf(format, values...)` by packing variadic records for `__rt_sprintf`.", + "area": "Process", + "canonical_name": "shell_exec", + "description": "Executes a command via the shell and returns the complete output as a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_sprintf", - "codegen_line": 511, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_shell_exec", + "codegen_line": 698, "notes": [ - "Lowers `sprintf(format, values...)` by packing variadic records for `__rt_sprintf`." - ], - "runtime_helpers": [ - "__rt_sprintf" + "Lowers `shell_exec(command)` by capturing shell stdout through the shared runtime helper." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/shell_exec.rs", "sig_line": null }, - "name": "sprintf", + "name": "shell_exec", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "format", + "name": "command", "optional": false, "type": "string" } ], "return_type": "string", - "variadic": "values" + "variadic": null }, - "slug": "sprintf", - "sub_area": "String" + "slug": "shell_exec", + "sub_area": "Process" }, { - "area": "Math", - "canonical_name": "sqrt", - "description": "Lowers `sqrt()` for concrete integer-like and floating operands.", + "area": "Array", + "canonical_name": "shuffle", + "description": "Shuffles an array into random order.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/math.rs", - "codegen_function": "lower_sqrt", - "codegen_line": 97, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_shuffle", + "codegen_line": 1119, "notes": [ - "Lowers `sqrt()` for concrete integer-like and floating operands." + "Lowers `shuffle()` for indexed arrays with 8-byte slots by mutating the source array in place." + ], + "runtime_helpers": [ + "__rt_array_is_list" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/shuffle.rs", "sig_line": null }, - "name": "sqrt", + "name": "shuffle", "sig": { "params": [ { - "by_ref": false, + "by_ref": true, "default": null, - "name": "num", + "name": "array", "optional": false, - "type": "float" + "type": "array" } ], - "return_type": "float", + "return_type": "bool", "variadic": null }, - "slug": "sqrt", - "sub_area": "Math" + "slug": "shuffle", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "sscanf", - "description": "Lowers `sscanf(string, format)` into the shared scanner helper.", + "area": "Math", + "canonical_name": "sin", + "description": "Returns the sine of a number (radians).", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_sscanf", - "codegen_line": 163, - "notes": [ - "Lowers `sscanf(string, format)` into the shared scanner helper." - ], - "runtime_helpers": [ - "__rt_sscanf", - "__rt_str_split" + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, + "notes": [ + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/sin.rs", "sig_line": null }, - "name": "sscanf", + "name": "sin", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "format", + "name": "num", "optional": false, - "type": "string" + "type": "float" } ], - "return_type": "array", - "variadic": "vars" + "return_type": "float", + "variadic": null }, - "slug": "sscanf", - "sub_area": "String" + "slug": "sin", + "sub_area": "Math" }, { - "area": "Filesystem", - "canonical_name": "stat", - "description": "Lowers `stat(path)` and boxes the runtime stat array or PHP false result.", + "area": "Math", + "canonical_name": "sinh", + "description": "Returns the hyperbolic sine of a number.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stat", - "codegen_line": 4886, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers `stat(path)` and boxes the runtime stat array or PHP false result." - ], - "runtime_helpers": [ - "__rt_fstat_array", - "__rt_lstat_array", - "__rt_stat_array" + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/sinh.rs", "sig_line": null }, - "name": "stat", + "name": "sinh", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "num", "optional": false, - "type": "string" + "type": "float" } ], - "return_type": "mixed", + "return_type": "float", "variadic": null }, - "slug": "stat", - "sub_area": "Filesystem" + "slug": "sinh", + "sub_area": "Math" }, { - "area": "String", - "canonical_name": "str_contains", - "description": "Lowers `str_contains()` through `strpos()` and converts found positions to bool.", + "area": "Process", + "canonical_name": "sleep", + "description": "Delays execution for a number of seconds.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_str_contains", - "codegen_line": 682, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_sleep", + "codegen_line": 473, "notes": [ - "Lowers `str_contains()` through `strpos()` and converts found positions to bool." + "Lowers `sleep(seconds)` through the target's C library symbol." ], "runtime_helpers": [ - "__rt_strpos" + "__rt_strtotime" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/sleep.rs", "sig_line": null }, - "name": "str_contains", + "name": "sleep", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "haystack", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "needle", + "name": "seconds", "optional": false, - "type": "string" + "type": "int" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "str_contains", - "sub_area": "String" + "slug": "sleep", + "sub_area": "Process" }, { - "area": "String", - "canonical_name": "str_ends_with", - "description": "Lowers a two-argument string builtin that directly delegates to a runtime helper.", + "area": "Array", + "canonical_name": "sort", + "description": "Sorts an array in ascending order.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_binary_string_runtime", - "codegen_line": 139, + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", + "codegen_function": "lower_sort", + "codegen_line": 1079, "notes": [ - "Lowers a two-argument string builtin that directly delegates to a runtime helper." + "Lowers `sort()` for indexed integer arrays by mutating the source array in place." ], "runtime_helpers": [ - "__rt_explode" + "__rt_arsort", + "__rt_asort", + "__rt_krsort", + "__rt_ksort", + "__rt_rsort_int", + "__rt_rsort_str", + "__rt_sort_int", + "__rt_sort_str" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/sort.rs", "sig_line": null }, - "name": "str_ends_with", + "name": "sort", "sig": { "params": [ { - "by_ref": false, - "default": null, - "name": "haystack", - "optional": false, - "type": "string" - }, - { - "by_ref": false, + "by_ref": true, "default": null, - "name": "needle", + "name": "array", "optional": false, - "type": "string" + "type": "array" } ], "return_type": "bool", "variadic": null }, - "slug": "str_ends_with", - "sub_area": "String" + "slug": "sort", + "sub_area": "Array" }, { - "area": "String", - "canonical_name": "str_ireplace", - "description": "", + "area": "SPL", + "canonical_name": "spl_autoload", + "description": "Default implementation for __autoload().", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_autoload_void", + "codegen_line": 151, + "notes": [ + "Lowers no-op autoload calls by preserving arg effects and returning PHP null if used." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/spl/spl_autoload.rs", "sig_line": null }, - "name": "str_ireplace", + "name": "spl_autoload", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "search", - "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "replace", + "name": "class", "optional": false, - "type": "mixed" + "type": "string" }, { "by_ref": false, - "default": null, - "name": "subject", - "optional": false, - "type": "mixed" - }, - { - "by_ref": true, - "default": null, - "name": "count", + "default": "null", + "name": "file_extensions", "optional": true, - "type": "int" + "type": "string" } ], - "return_type": "mixed", + "return_type": "void", "variadic": null }, - "slug": "str_ireplace", - "sub_area": "String" + "slug": "spl_autoload", + "sub_area": "SPL" }, { - "area": "String", - "canonical_name": "str_pad", - "description": "Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper.", + "area": "SPL", + "canonical_name": "spl_autoload_call", + "description": "Try all registered __autoload() functions to load the requested class.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_str_pad", - "codegen_line": 818, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_autoload_void", + "codegen_line": 151, "notes": [ - "Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper." - ], - "runtime_helpers": [ - "__rt_str_pad" + "Lowers no-op autoload calls by preserving arg effects and returning PHP null if used." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/spl_autoload_call.rs", "sig_line": null }, - "name": "str_pad", + "name": "spl_autoload_call", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "length", + "name": "class", "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "pad_string", - "optional": true, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "pad_type", - "optional": true, - "type": "int" } ], - "return_type": "string", + "return_type": "void", "variadic": null }, - "slug": "str_pad", - "sub_area": "String" + "slug": "spl_autoload_call", + "sub_area": "SPL" }, { - "area": "String", - "canonical_name": "str_repeat", - "description": "Lowers `str_repeat(string, times)` through the shared runtime helper.", + "area": "SPL", + "canonical_name": "spl_autoload_extensions", + "description": "Register and return default file extensions for spl_autoload.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_str_repeat", - "codegen_line": 746, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_autoload_extensions", + "codegen_line": 178, "notes": [ - "Lowers `str_repeat(string, times)` through the shared runtime helper." - ], - "runtime_helpers": [ - "__rt_str_repeat" + "Lowers `spl_autoload_extensions()` against the mutable extension globals." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/spl_autoload_extensions.rs", "sig_line": null }, - "name": "str_repeat", + "name": "spl_autoload_extensions", "sig": { "params": [ { "by_ref": false, - "default": null, - "name": "string", - "optional": false, + "default": "null", + "name": "file_extensions", + "optional": true, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "times", - "optional": false, - "type": "int" } ], - "return_type": "string", + "return_type": "string", + "variadic": null + }, + "slug": "spl_autoload_extensions", + "sub_area": "SPL" + }, + { + "area": "SPL", + "canonical_name": "spl_autoload_functions", + "description": "Return all registered __autoload() functions.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_autoload_functions", + "codegen_line": 167, + "notes": [ + "Lowers `spl_autoload_functions()` to an indexed array of AOT rule placeholders." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/spl/spl_autoload_functions.rs", + "sig_line": null + }, + "name": "spl_autoload_functions", + "sig": { + "params": [], + "return_type": "array", "variadic": null }, - "slug": "str_repeat", - "sub_area": "String" + "slug": "spl_autoload_functions", + "sub_area": "SPL" }, { - "area": "String", - "canonical_name": "str_replace", - "description": "Lowers `str_replace()`/`str_ireplace()` with three string operands.", + "area": "SPL", + "canonical_name": "spl_autoload_register", + "description": "Register given function as __autoload() implementation.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_string_replace", - "codegen_line": 780, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_autoload_bool", + "codegen_line": 135, "notes": [ - "Lowers `str_replace()`/`str_ireplace()` with three string operands." + "Lowers autoload registration stubs by preserving arg effects and returning true." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/spl_autoload_register.rs", "sig_line": null }, - "name": "str_replace", + "name": "spl_autoload_register", "sig": { "params": [ { "by_ref": false, - "default": null, - "name": "search", - "optional": false, - "type": "string" + "default": "null", + "name": "callback", + "optional": true, + "type": "callable" }, { "by_ref": false, - "default": null, - "name": "replace", - "optional": false, - "type": "string" + "default": "true", + "name": "throw", + "optional": true, + "type": "bool" }, { "by_ref": false, - "default": null, - "name": "subject", - "optional": false, - "type": "string" - }, - { - "by_ref": true, - "default": null, - "name": "count", + "default": "false", + "name": "prepend", "optional": true, - "type": "int" + "type": "bool" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "str_replace", - "sub_area": "String" + "slug": "spl_autoload_register", + "sub_area": "SPL" }, { - "area": "String", - "canonical_name": "str_split", - "description": "Lowers `str_split(string, length?)` into the fixed-width string-array splitter.", + "area": "SPL", + "canonical_name": "spl_autoload_unregister", + "description": "Unregister given function as __autoload() implementation.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_str_split", - "codegen_line": 176, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_autoload_bool", + "codegen_line": 135, "notes": [ - "Lowers `str_split(string, length?)` into the fixed-width string-array splitter." - ], - "runtime_helpers": [ - "__rt_str_split" + "Lowers autoload registration stubs by preserving arg effects and returning true." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/spl_autoload_unregister.rs", "sig_line": null }, - "name": "str_split", + "name": "spl_autoload_unregister", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "callback", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": true, - "type": "int" + "type": "callable" } ], - "return_type": "array", + "return_type": "bool", "variadic": null }, - "slug": "str_split", - "sub_area": "String" + "slug": "spl_autoload_unregister", + "sub_area": "SPL" }, { - "area": "String", - "canonical_name": "str_starts_with", - "description": "Lowers a two-argument string builtin that directly delegates to a runtime helper.", + "area": "SPL", + "canonical_name": "spl_classes", + "description": "Return available SPL classes.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_binary_string_runtime", - "codegen_line": 139, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_classes", + "codegen_line": 206, "notes": [ - "Lowers a two-argument string builtin that directly delegates to a runtime helper." + "Lowers `spl_classes()` to the static compiler-shipped SPL/core type snapshot." ], "runtime_helpers": [ - "__rt_explode" + "__rt_itoa" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/spl_classes.rs", "sig_line": null }, - "name": "str_starts_with", + "name": "spl_classes", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "haystack", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "needle", - "optional": false, - "type": "string" - } - ], - "return_type": "bool", + "params": [], + "return_type": "array", "variadic": null }, - "slug": "str_starts_with", - "sub_area": "String" + "slug": "spl_classes", + "sub_area": "SPL" }, { - "area": "String", - "canonical_name": "strcasecmp", - "description": "", + "area": "SPL", + "canonical_name": "spl_object_hash", + "description": "Return hash id for given object.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_object_hash", + "codegen_line": 226, + "notes": [ + "Lowers `spl_object_hash(object)` by formatting the loaded object pointer as a string." + ], + "runtime_helpers": [ + "__rt_itoa" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/spl/spl_object_hash.rs", "sig_line": null }, - "name": "strcasecmp", + "name": "spl_object_hash", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string1", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "string2", + "name": "object", "optional": false, - "type": "string" + "type": "object" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "strcasecmp", - "sub_area": "String" + "slug": "spl_object_hash", + "sub_area": "SPL" }, { - "area": "String", - "canonical_name": "strcmp", - "description": "Lowers a two-argument string builtin that directly delegates to a runtime helper.", + "area": "SPL", + "canonical_name": "spl_object_id", + "description": "Return the integer object handle for given object.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_binary_string_runtime", - "codegen_line": 139, + "codegen_file": "src/codegen/lower_inst/builtins/spl.rs", + "codegen_function": "lower_spl_object_id", + "codegen_line": 216, "notes": [ - "Lowers a two-argument string builtin that directly delegates to a runtime helper." + "Lowers `spl_object_id(object)` by returning the loaded object pointer as an integer." ], "runtime_helpers": [ - "__rt_explode" + "__rt_itoa" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/spl/spl_object_id.rs", "sig_line": null }, - "name": "strcmp", + "name": "spl_object_id", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string1", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "string2", + "name": "object", "optional": false, - "type": "string" + "type": "object" } ], "return_type": "int", "variadic": null }, - "slug": "strcmp", - "sub_area": "String" + "slug": "spl_object_id", + "sub_area": "SPL" }, { "area": "String", - "canonical_name": "stripslashes", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "canonical_name": "sprintf", + "description": "Returns a formatted string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_sprintf", + "codegen_line": 511, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." + "Lowers `sprintf(format, values...)` by packing variadic records for `__rt_sprintf`." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_sprintf" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/sprintf.rs", "sig_line": null }, - "name": "stripslashes", + "name": "sprintf", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "format", "optional": false, "type": "string" } ], "return_type": "string", - "variadic": null + "variadic": "values" }, - "slug": "stripslashes", + "slug": "sprintf", "sub_area": "String" }, { - "area": "String", - "canonical_name": "strlen", - "description": "Lowers `strlen()` by coercing string-like values and returning the byte length.", + "area": "Math", + "canonical_name": "sqrt", + "description": "Returns the square root of a number.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins.rs", - "codegen_function": "lower_strlen", - "codegen_line": 971, + "codegen_file": "src/codegen/lower_inst/builtins/math.rs", + "codegen_function": "lower_sqrt", + "codegen_line": 97, "notes": [ - "Lowers `strlen()` by coercing string-like values and returning the byte length." - ], - "runtime_helpers": [ - "__rt_mixed_cast_string" + "Lowers `sqrt()` for concrete integer-like and floating operands." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/sqrt.rs", "sig_line": null }, - "name": "strlen", + "name": "sqrt", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "num", "optional": false, - "type": "string" + "type": "float" } ], - "return_type": "int", + "return_type": "float", "variadic": null }, - "slug": "strlen", - "sub_area": "String" + "slug": "sqrt", + "sub_area": "Math" }, { "area": "String", - "canonical_name": "strpos", - "description": "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed.", + "canonical_name": "sscanf", + "description": "Parses a string according to a format.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_string_position", - "codegen_line": 700, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_sscanf", + "codegen_line": 163, "notes": [ - "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." + "Lowers `sscanf(string, format)` into the shared scanner helper." + ], + "runtime_helpers": [ + "__rt_sscanf", + "__rt_str_split" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/sscanf.rs", "sig_line": null }, - "name": "strpos", + "name": "sscanf", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "haystack", + "name": "string", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "needle", + "name": "format", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "offset", - "optional": true, - "type": "int" } ], - "return_type": "mixed", - "variadic": null + "return_type": "array", + "variadic": "vars" }, - "slug": "strpos", + "slug": "sscanf", "sub_area": "String" }, { - "area": "String", - "canonical_name": "strrev", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "area": "Filesystem", + "canonical_name": "stat", + "description": "Gives information about a file.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stat", + "codegen_line": 5529, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." + "Lowers `stat(path)` and boxes the runtime stat array or PHP false result." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_fstat_array", + "__rt_lstat_array", + "__rt_stat_array" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stat.rs", "sig_line": null }, - "name": "strrev", + "name": "stat", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "filename", "optional": false, "type": "string" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "strrev", - "sub_area": "String" + "slug": "stat", + "sub_area": "Filesystem" }, { "area": "String", - "canonical_name": "strrpos", - "description": "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed.", + "canonical_name": "str_contains", + "description": "Determines if a string contains a given substring.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_string_position", - "codegen_line": 700, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_str_contains", + "codegen_line": 682, "notes": [ - "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." + "Lowers `str_contains()` through `strpos()` and converts found positions to bool." + ], + "runtime_helpers": [ + "__rt_strpos" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/str_contains.rs", "sig_line": null }, - "name": "strrpos", + "name": "str_contains", "sig": { "params": [ { @@ -14122,42 +15072,37 @@ "name": "needle", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "offset", - "optional": true, - "type": "int" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "strrpos", + "slug": "str_contains", "sub_area": "String" }, { "area": "String", - "canonical_name": "strstr", - "description": "Lowers `strstr(haystack, needle)` by searching and returning the matching suffix.", + "canonical_name": "str_ends_with", + "description": "Checks if a string ends with a given substring.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_strstr", - "codegen_line": 762, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_binary_string_runtime", + "codegen_line": 139, "notes": [ - "Lowers `strstr(haystack, needle)` by searching and returning the matching suffix." + "Lowers a two-argument string builtin that directly delegates to a runtime helper." + ], + "runtime_helpers": [ + "__rt_explode" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/str_ends_with.rs", "sig_line": null }, - "name": "strstr", + "name": "str_ends_with", "sig": { "params": [ { @@ -14173,137 +15118,95 @@ "name": "needle", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "before_needle", - "optional": true, - "type": "bool" } ], - "return_type": "string", + "return_type": "bool", "variadic": null }, - "slug": "strstr", + "slug": "str_ends_with", "sub_area": "String" }, { "area": "String", - "canonical_name": "strtolower", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "canonical_name": "str_ireplace", + "description": "Case-insensitive version of str_replace().", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_string_replace", + "codegen_line": 780, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." - ], - "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/str_ireplace.rs", "sig_line": null }, - "name": "strtolower", + "name": "str_ireplace", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "search", "optional": false, "type": "string" - } - ], - "return_type": "string", - "variadic": null - }, - "slug": "strtolower", - "sub_area": "String" - }, - { - "area": "Date", - "canonical_name": "strtotime", - "description": "Lowers `strtotime(datetime[, baseTimestamp])` through the shared parser runtime helper.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_strtotime", - "codegen_line": 487, - "notes": [ - "Lowers `strtotime(datetime[, baseTimestamp])` through the shared parser runtime helper.", - "Returns PHP's `int|false`: the `__rt_strtotime` `i64::MIN` parse-failure sentinel is boxed as", - "`Mixed` `false`, and every other value (including a real `-1` pre-epoch timestamp) is boxed as", - "a `Mixed` integer, so `=== false`, `=== -1`, and `echo` all observe the distinct results.", - "Supports PHP's optional `$baseTimestamp`. (The `__elephc_strtotime_raw` alias keeps the plain", - "`-1` integer shape for the synthetic `DateTime` internals.)" - ], - "runtime_helpers": [ - "__rt_mixed_from_value", - "__rt_strtotime" - ], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "strtotime", - "sig": { - "params": [ + }, { "by_ref": false, "default": null, - "name": "datetime", + "name": "replace", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "baseTimestamp", + "name": "subject", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "null", + "name": "count", "optional": true, "type": "int" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "strtotime", - "sub_area": "Date" + "slug": "str_ireplace", + "sub_area": "String" }, { "area": "String", - "canonical_name": "strtoupper", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "canonical_name": "str_pad", + "description": "Pads a string to a certain length with another string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_str_pad", + "codegen_line": 818, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." + "Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_str_pad" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/str_pad.rs", "sig_line": null }, - "name": "strtoupper", + "name": "str_pad", "sig": { "params": [ { @@ -14312,37 +15215,58 @@ "name": "string", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": "' '", + "name": "pad_string", + "optional": true, + "type": "string" + }, + { + "by_ref": false, + "default": "1", + "name": "pad_type", + "optional": true, + "type": "int" } ], "return_type": "string", "variadic": null }, - "slug": "strtoupper", + "slug": "str_pad", "sub_area": "String" }, { "area": "String", - "canonical_name": "substr", - "description": "Lowers `substr(string, offset, length?)` with target-local pointer arithmetic.", + "canonical_name": "str_repeat", + "description": "Repeats a string a given number of times.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_substr", - "codegen_line": 713, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_str_repeat", + "codegen_line": 746, "notes": [ - "Lowers `substr(string, offset, length?)` with target-local pointer arithmetic." + "Lowers `str_repeat(string, times)` through the shared runtime helper." ], "runtime_helpers": [ - "__rt_substr_replace" + "__rt_str_repeat" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/str_repeat.rs", "sig_line": null }, - "name": "substr", + "name": "str_repeat", "sig": { "params": [ { @@ -14355,54 +15279,44 @@ { "by_ref": false, "default": null, - "name": "offset", + "name": "times", "optional": false, "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": true, - "type": "int" } ], "return_type": "string", "variadic": null }, - "slug": "substr", + "slug": "str_repeat", "sub_area": "String" }, { "area": "String", - "canonical_name": "substr_replace", - "description": "Lowers `substr_replace(string, replacement, start, length?)`.", + "canonical_name": "str_replace", + "description": "Replaces all occurrences of a search string with a replacement string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", - "codegen_function": "lower_substr_replace", - "codegen_line": 730, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_string_replace", + "codegen_line": 780, "notes": [ - "Lowers `substr_replace(string, replacement, start, length?)`." - ], - "runtime_helpers": [ - "__rt_str_repeat", - "__rt_substr_replace" + "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/str_replace.rs", "sig_line": null }, - "name": "substr_replace", + "name": "str_replace", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "string", + "name": "search", "optional": false, "type": "string" }, @@ -14416,14 +15330,14 @@ { "by_ref": false, "default": null, - "name": "offset", + "name": "subject", "optional": false, - "type": "int" + "type": "string" }, { "by_ref": false, - "default": null, - "name": "length", + "default": "null", + "name": "count", "optional": true, "type": "int" } @@ -14431,79 +15345,91 @@ "return_type": "string", "variadic": null }, - "slug": "substr_replace", + "slug": "str_replace", "sub_area": "String" }, { - "area": "Filesystem", - "canonical_name": "sys_get_temp_dir", - "description": "Lowers `sys_get_temp_dir()` as the project's hardcoded `/tmp` string.", + "area": "String", + "canonical_name": "str_split", + "description": "Converts a string into an array of chunks of the given length.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_sys_get_temp_dir", - "codegen_line": 4760, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_str_split", + "codegen_line": 176, "notes": [ - "Lowers `sys_get_temp_dir()` as the project's hardcoded `/tmp` string." + "Lowers `str_split(string, length?)` into the fixed-width string-array splitter." ], "runtime_helpers": [ - "__rt_tmpfile" + "__rt_str_split" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/str_split.rs", "sig_line": null }, - "name": "sys_get_temp_dir", + "name": "str_split", "sig": { - "params": [], - "return_type": "string", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "1", + "name": "length", + "optional": true, + "type": "int" + } + ], + "return_type": "array", "variadic": null }, - "slug": "sys_get_temp_dir", - "sub_area": "Filesystem" + "slug": "str_split", + "sub_area": "String" }, { - "area": "Filesystem", - "canonical_name": "symlink", - "description": "Lowers `symlink(target, link)` through the target-aware libc wrapper.", + "area": "String", + "canonical_name": "str_starts_with", + "description": "Checks if a string starts with a given substring.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_symlink", - "codegen_line": 4805, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_binary_string_runtime", + "codegen_line": 139, "notes": [ - "Lowers `symlink(target, link)` through the target-aware libc wrapper." + "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_fileatime", - "__rt_link", - "__rt_readlink", - "__rt_symlink" + "__rt_explode" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/str_starts_with.rs", "sig_line": null }, - "name": "symlink", + "name": "str_starts_with", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "target", + "name": "haystack", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "link", + "name": "needle", "optional": false, "type": "string" } @@ -14511,777 +15437,706 @@ "return_type": "bool", "variadic": null }, - "slug": "symlink", - "sub_area": "Filesystem" + "slug": "str_starts_with", + "sub_area": "String" }, { - "area": "Process", - "canonical_name": "system", - "description": "Lowers `system(command)` through libc `system()` and returns the legacy empty string result.", + "area": "String", + "canonical_name": "strcasecmp", + "description": "Binary safe case-insensitive string comparison. Returns negative, zero, or positive.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_system", - "codegen_line": 706, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_binary_string_runtime", + "codegen_line": 139, "notes": [ - "Lowers `system(command)` through libc `system()` and returns the legacy empty string result." + "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_shell_exec" + "__rt_explode" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/strcasecmp.rs", "sig_line": null }, - "name": "system", + "name": "strcasecmp", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "command", + "name": "string1", "optional": false, "type": "string" }, { - "by_ref": true, + "by_ref": false, "default": null, - "name": "result_code", + "name": "string2", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "string", + "return_type": "int", "variadic": null }, - "slug": "system", - "sub_area": "Process" + "slug": "strcasecmp", + "sub_area": "String" }, { - "area": "Math", - "canonical_name": "tan", - "description": "", + "area": "String", + "canonical_name": "strcmp", + "description": "Binary safe string comparison. Returns negative, zero, or positive.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_binary_string_runtime", + "codegen_line": 139, + "notes": [ + "Lowers a two-argument string builtin that directly delegates to a runtime helper." + ], + "runtime_helpers": [ + "__rt_explode" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/string/strcmp.rs", "sig_line": null }, - "name": "tan", + "name": "strcmp", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "num", + "name": "string1", "optional": false, - "type": "float" - } - ], - "return_type": "float", - "variadic": null - }, - "slug": "tan", - "sub_area": "Math" - }, - { - "area": "Math", - "canonical_name": "tanh", - "description": "", - "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": "src/types/signatures.rs", - "sig_line": null - }, - "name": "tanh", - "sig": { - "params": [ + "type": "string" + }, { "by_ref": false, "default": null, - "name": "num", + "name": "string2", "optional": false, - "type": "float" + "type": "string" } ], - "return_type": "float", + "return_type": "int", "variadic": null }, - "slug": "tanh", - "sub_area": "Math" + "slug": "strcmp", + "sub_area": "String" }, { - "area": "Filesystem", - "canonical_name": "tempnam", - "description": "Lowers `tempnam(directory, prefix)` through the target-aware runtime helper.", + "area": "Streams", + "canonical_name": "stream_bucket_append", + "description": "Appends a bucket to the brigade.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_tempnam", - "codegen_line": 3810, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_bucket_append_or_prepend", + "codegen_line": 2064, "notes": [ - "Lowers `tempnam(directory, prefix)` through the target-aware runtime helper." - ], - "runtime_helpers": [ - "__rt_glob", - "__rt_scandir", - "__rt_tempnam" + "Lowers `stream_bucket_append` and `stream_bucket_prepend` over the `_buckets` array." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_bucket_append.rs", "sig_line": null }, - "name": "tempnam", + "name": "stream_bucket_append", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "directory", + "name": "brigade", "optional": false, - "type": "string" + "type": "mixed" }, { "by_ref": false, "default": null, - "name": "prefix", + "name": "bucket", "optional": false, - "type": "string" + "type": "mixed" } ], - "return_type": "string", + "return_type": "void", "variadic": null }, - "slug": "tempnam", - "sub_area": "Filesystem" + "slug": "stream_bucket_append", + "sub_area": "Streams" }, { - "area": "Date", - "canonical_name": "time", - "description": "Lowers `time()` through the shared wall-clock runtime helper.", + "area": "IO", + "canonical_name": "stream_bucket_make_writeable", + "description": "Returns a bucket object from the brigade for use in a stream filter.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", - "codegen_function": "lower_time", - "codegen_line": 615, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_bucket_make_writeable", + "codegen_line": 1987, "notes": [ - "Lowers `time()` through the shared wall-clock runtime helper." + "Lowers `stream_bucket_make_writeable(brigade)` by popping the brigade head." ], "runtime_helpers": [ - "__rt_time" + "__rt_stdclass_get" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_bucket_make_writeable.rs", "sig_line": null }, - "name": "time", + "name": "stream_bucket_make_writeable", "sig": { - "params": [], - "return_type": "int", + "params": [ + { + "by_ref": false, + "default": null, + "name": "brigade", + "optional": false, + "type": "mixed" + } + ], + "return_type": "mixed", "variadic": null }, - "slug": "time", - "sub_area": "Date" + "slug": "stream_bucket_make_writeable", + "sub_area": "IO" }, { - "area": "Filesystem", - "canonical_name": "tmpfile", - "description": "Lowers `tmpfile()` and boxes the anonymous stream descriptor or PHP false.", + "area": "IO", + "canonical_name": "stream_bucket_new", + "description": "Creates a new bucket for use in a stream filter.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_tmpfile", - "codegen_line": 4773, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_bucket_new", + "codegen_line": 1970, "notes": [ - "Lowers `tmpfile()` and boxes the anonymous stream descriptor or PHP false." - ], - "runtime_helpers": [ - "__rt_filemtime", - "__rt_linkinfo", - "__rt_tmpfile" + "Lowers `stream_bucket_new(stream, data)` into a stdClass-backed bucket object." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_bucket_new.rs", "sig_line": null }, - "name": "tmpfile", + "name": "stream_bucket_new", "sig": { - "params": [], + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false, + "type": "resource" + }, + { + "by_ref": false, + "default": null, + "name": "buffer", + "optional": false, + "type": "string" + } + ], "return_type": "mixed", "variadic": null }, - "slug": "tmpfile", - "sub_area": "Filesystem" + "slug": "stream_bucket_new", + "sub_area": "IO" }, { - "area": "Filesystem", - "canonical_name": "touch", - "description": "Lowers `touch(path, mtime?, atime?)` through the target-aware runtime helper.", + "area": "Streams", + "canonical_name": "stream_bucket_prepend", + "description": "Prepends a bucket to the brigade.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_touch", - "codegen_line": 3880, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_bucket_append_or_prepend", + "codegen_line": 2064, "notes": [ - "Lowers `touch(path, mtime?, atime?)` through the target-aware runtime helper." + "Lowers `stream_bucket_append` and `stream_bucket_prepend` over the `_buckets` array." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_bucket_prepend.rs", "sig_line": null }, - "name": "touch", + "name": "stream_bucket_prepend", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "brigade", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "mtime", - "optional": true, - "type": "int" + "type": "mixed" }, { "by_ref": false, "default": null, - "name": "atime", - "optional": true, - "type": "int" + "name": "bucket", + "optional": false, + "type": "mixed" } ], - "return_type": "bool", + "return_type": "void", "variadic": null }, - "slug": "touch", - "sub_area": "Filesystem" + "slug": "stream_bucket_prepend", + "sub_area": "Streams" }, { "area": "IO", - "canonical_name": "stream_isatty", - "description": "Lowers `stream_isatty(stream)`.", + "canonical_name": "stream_context_create", + "description": "Creates a stream context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_isatty", - "codegen_line": 1908, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_context_create", + "codegen_line": 1064, "notes": [ - "Lowers `stream_isatty(stream)`." - ], - "runtime_helpers": [ - "__rt_stream_isatty" + "Lowers `stream_context_create(options?, params?)`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_context_create.rs", "sig_line": null }, - "name": "stream_isatty", + "name": "stream_context_create", "sig": { "params": [ { "by_ref": false, - "default": null, - "name": "stream", - "optional": false, - "type": "resource" + "default": "null", + "name": "options", + "optional": true, + "type": "array" + }, + { + "by_ref": false, + "default": "null", + "name": "params", + "optional": true, + "type": "array" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "stream_isatty", + "slug": "stream_context_create", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_socket_server", - "description": "Lowers `stream_socket_server(address)` and boxes `resource|false`.", + "canonical_name": "stream_context_get_default", + "description": "Retrieves the default stream context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_socket_server", - "codegen_line": 2155, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_context_get_default", + "codegen_line": 1078, "notes": [ - "Lowers `stream_socket_server(address)` and boxes `resource|false`." - ], - "runtime_helpers": [ - "__rt_stream_socket_server" + "Lowers `stream_context_get_default(options?)`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_context_get_default.rs", "sig_line": null }, - "name": "stream_socket_server", + "name": "stream_context_get_default", "sig": { "params": [ { "by_ref": false, - "default": null, - "name": "address", - "optional": false, - "type": "string" - }, - { - "by_ref": true, - "default": null, - "name": "error_code", - "optional": false, - "type": "int" - }, - { - "by_ref": true, - "default": null, - "name": "error_message", - "optional": false, - "type": "int" + "default": "null", + "name": "options", + "optional": true, + "type": "array" } ], "return_type": "mixed", "variadic": null }, - "slug": "stream_socket_server", + "slug": "stream_context_get_default", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_socket_client", - "description": "Lowers `stream_socket_client(address)` and records the connected host for TLS defaults.", + "canonical_name": "stream_context_get_options", + "description": "Retrieves options for the specified stream context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_socket_client", - "codegen_line": 2178, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_context_get_options", + "codegen_line": 1252, "notes": [ - "Lowers `stream_socket_client(address)` and records the connected host for TLS defaults." + "Lowers `stream_context_get_options(context)`." ], "runtime_helpers": [ - "__rt_stash_connect_host", - "__rt_stream_socket_client" + "__rt_hash_new", + "__rt_incref" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_context_get_options.rs", "sig_line": null }, - "name": "stream_socket_client", + "name": "stream_context_get_options", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "address", - "optional": false, - "type": "string" - }, - { - "by_ref": true, - "default": null, - "name": "error_code", - "optional": false, - "type": "int" - }, - { - "by_ref": true, - "default": null, - "name": "error_message", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "timeout", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "flags", + "name": "context", "optional": false, - "type": "float" + "type": "resource" } ], - "return_type": "mixed", + "return_type": "array", "variadic": null }, - "slug": "stream_socket_client", + "slug": "stream_context_get_options", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_socket_accept", - "description": "Lowers `stream_socket_accept(server, timeout?, peer_name?)`.", + "canonical_name": "stream_context_get_params", + "description": "Retrieves parameters from the specified stream context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_socket_accept", - "codegen_line": 2217, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_context_get_params", + "codegen_line": 1291, "notes": [ - "Lowers `stream_socket_accept(server, timeout?, peer_name?)`." - ], - "runtime_helpers": [ - "__rt_stream_socket_accept" + "Lowers `stream_context_get_params(context)` to an empty associative hash." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_context_get_params.rs", "sig_line": null }, - "name": "stream_socket_accept", + "name": "stream_context_get_params", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "socket", + "name": "context", "optional": false, "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "timeout", - "optional": true, - "type": "float" - }, - { - "by_ref": true, - "default": null, - "name": "peer_name", - "optional": true, - "type": "string" } ], - "return_type": "mixed", + "return_type": "array", "variadic": null }, - "slug": "stream_socket_accept", + "slug": "stream_context_get_params", "sub_area": "IO" }, { - "area": "Streams", - "canonical_name": "fsockopen", - "description": "", + "area": "IO", + "canonical_name": "stream_context_set_default", + "description": "Sets the default stream context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_context_set_default", + "codegen_line": 1088, + "notes": [ + "Lowers `stream_context_set_default(options)`." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/stream_context_set_default.rs", "sig_line": null }, - "name": "fsockopen", + "name": "stream_context_set_default", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "hostname", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "port", + "name": "options", "optional": false, - "type": "int" - }, - { - "by_ref": true, - "default": null, - "name": "error_code", - "optional": true, - "type": "int" - }, - { - "by_ref": true, - "default": null, - "name": "error_message", - "optional": true, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "timeout", - "optional": true, - "type": "float" + "type": "array" } ], "return_type": "mixed", "variadic": null }, - "slug": "fsockopen", - "sub_area": "Streams" + "slug": "stream_context_set_default", + "sub_area": "IO" }, { - "area": "Streams", - "canonical_name": "pfsockopen", - "description": "", + "area": "IO", + "canonical_name": "stream_context_set_option", + "description": "Sets an option on the specified context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_context_set_option", + "codegen_line": 1098, + "notes": [ + "Lowers `stream_context_set_option(context, options)` and the four-argument form." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/stream_context_set_option.rs", "sig_line": null }, - "name": "pfsockopen", + "name": "stream_context_set_option", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "hostname", + "name": "context", "optional": false, - "type": "string" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "port", - "optional": false, - "type": "int" - }, - { - "by_ref": true, - "default": null, - "name": "error_code", - "optional": true, - "type": "int" + "name": "wrapper_or_options", + "optional": false, + "type": "string" }, { - "by_ref": true, - "default": null, - "name": "error_message", + "by_ref": false, + "default": "null", + "name": "option_name", "optional": true, "type": "string" }, { "by_ref": false, - "default": null, - "name": "timeout", + "default": "null", + "name": "value", "optional": true, - "type": "float" + "type": "mixed" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "pfsockopen", - "sub_area": "Streams" + "slug": "stream_context_set_option", + "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_wrapper_register", - "description": "Lowers `stream_wrapper_register(protocol, class, flags?)`.", + "canonical_name": "stream_context_set_params", + "description": "Sets parameters on the specified context.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_wrapper_register", - "codegen_line": 887, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_context_set_params", + "codegen_line": 1118, "notes": [ - "Lowers `stream_wrapper_register(protocol, class, flags?)`." - ], - "runtime_helpers": [ - "__rt_stream_wrapper_register" + "Lowers `stream_context_set_params(context, params)` as an accepted parameter update." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_context_set_params.rs", "sig_line": null }, - "name": "stream_wrapper_register", + "name": "stream_context_set_params", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "protocol", + "name": "context", "optional": false, - "type": "string" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "class", + "name": "params", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": true, - "type": "int" + "type": "array" } ], "return_type": "bool", "variadic": null }, - "slug": "stream_wrapper_register", + "slug": "stream_context_set_params", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_wrapper_unregister", - "description": "Lowers `stream_wrapper_unregister(protocol)`.", + "canonical_name": "stream_copy_to_stream", + "description": "Copies data from one stream to another.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_wrapper_unregister", - "codegen_line": 917, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_copy_to_stream", + "codegen_line": 1360, "notes": [ - "Lowers `stream_wrapper_unregister(protocol)`." - ], - "runtime_helpers": [ - "__rt_stream_wrapper_unregister" + "Lowers `stream_copy_to_stream(from, to, length?, offset?)` through wrapper-aware read/write loops." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_copy_to_stream.rs", "sig_line": null }, - "name": "stream_wrapper_unregister", + "name": "stream_copy_to_stream", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "protocol", + "name": "from", "optional": false, - "type": "string" + "type": "resource" + }, + { + "by_ref": false, + "default": null, + "name": "to", + "optional": false, + "type": "resource" + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "-1", + "name": "offset", + "optional": true, + "type": "int" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "stream_wrapper_unregister", + "slug": "stream_copy_to_stream", "sub_area": "IO" }, { - "area": "IO", - "canonical_name": "stream_wrapper_restore", - "description": "Lowers `stream_wrapper_restore(protocol)` as a successful no-op.", + "area": "Streams", + "canonical_name": "stream_filter_append", + "description": "Attaches a filter to a stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_wrapper_restore", - "codegen_line": 939, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_filter_attach", + "codegen_line": 1550, "notes": [ - "Lowers `stream_wrapper_restore(protocol)` as a successful no-op." + "Lowers `stream_filter_append` and `stream_filter_prepend`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_filter_append.rs", "sig_line": null }, - "name": "stream_wrapper_restore", + "name": "stream_filter_append", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "protocol", + "name": "stream", + "optional": false, + "type": "resource" + }, + { + "by_ref": false, + "default": null, + "name": "filtername", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "3", + "name": "read_write", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "null", + "name": "params", + "optional": true, + "type": "mixed" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "stream_wrapper_restore", - "sub_area": "IO" + "slug": "stream_filter_append", + "sub_area": "Streams" }, { - "area": "IO", - "canonical_name": "stream_socket_enable_crypto", - "description": "Lowers `stream_socket_enable_crypto(stream, enable, method?, session_stream?)`.", + "area": "Streams", + "canonical_name": "stream_filter_prepend", + "description": "Attaches a filter to a stream (prepend).", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_socket_enable_crypto", - "codegen_line": 2328, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_filter_attach", + "codegen_line": 1550, "notes": [ - "Lowers `stream_socket_enable_crypto(stream, enable, method?, session_stream?)`." + "Lowers `stream_filter_append` and `stream_filter_prepend`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_filter_prepend.rs", "sig_line": null }, - "name": "stream_socket_enable_crypto", + "name": "stream_filter_prepend", "sig": { "params": [ { @@ -15294,599 +16149,590 @@ { "by_ref": false, "default": null, - "name": "enable", + "name": "filtername", "optional": false, - "type": "bool" + "type": "string" }, { "by_ref": false, - "default": null, - "name": "crypto_method", + "default": "3", + "name": "read_write", "optional": true, "type": "int" }, { "by_ref": false, - "default": null, - "name": "session_stream", + "default": "null", + "name": "params", "optional": true, - "type": "resource" + "type": "mixed" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "stream_socket_enable_crypto", - "sub_area": "IO" + "slug": "stream_filter_prepend", + "sub_area": "Streams" }, { "area": "IO", - "canonical_name": "stream_context_create", - "description": "Lowers `stream_context_create(options?, params?)`.", + "canonical_name": "stream_filter_register", + "description": "Registers a user-defined stream filter.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_context_create", - "codegen_line": 951, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_filter_register", + "codegen_line": 1521, "notes": [ - "Lowers `stream_context_create(options?, params?)`." + "Lowers `stream_filter_register(filter_name, class)` into the user-filter registry helper." + ], + "runtime_helpers": [ + "__rt_stream_filter_register" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_filter_register.rs", "sig_line": null }, - "name": "stream_context_create", + "name": "stream_filter_register", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "options", - "optional": true, - "type": "array" + "name": "filter_name", + "optional": false, + "type": "string" }, { "by_ref": false, "default": null, - "name": "params", - "optional": true, - "type": "array" + "name": "class", + "optional": false, + "type": "string" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "stream_context_create", + "slug": "stream_filter_register", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_context_get_default", - "description": "Lowers `stream_context_get_default(options?)`.", + "canonical_name": "stream_filter_remove", + "description": "Removes a filter from a stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_context_get_default", - "codegen_line": 965, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_filter_remove", + "codegen_line": 1939, "notes": [ - "Lowers `stream_context_get_default(options?)`." + "Lowers `stream_filter_remove(filter)` and clears both direction tables for the fd." + ], + "runtime_helpers": [ + "__rt_user_filter_release_fd" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_filter_remove.rs", "sig_line": null }, - "name": "stream_context_get_default", + "name": "stream_filter_remove", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "options", - "optional": true, - "type": "array" + "name": "stream_filter", + "optional": false, + "type": "resource" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "stream_context_get_default", + "slug": "stream_filter_remove", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_context_set_default", - "description": "Lowers `stream_context_set_default(options)`.", + "canonical_name": "stream_get_contents", + "description": "Reads remainder of a stream into a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_context_set_default", - "codegen_line": 975, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_get_contents", + "codegen_line": 1301, "notes": [ - "Lowers `stream_context_set_default(options)`." + "Lowers `stream_get_contents(stream, length?, offset?)` to `string|false`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_get_contents.rs", "sig_line": null }, - "name": "stream_context_set_default", + "name": "stream_get_contents", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "options", + "name": "stream", "optional": false, - "type": "array" + "type": "resource" + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "-1", + "name": "offset", + "optional": true, + "type": "int" } ], "return_type": "mixed", "variadic": null }, - "slug": "stream_context_set_default", + "slug": "stream_get_contents", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "stream_get_filters", + "description": "Retrieves list of registered filters.", + "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_stream_get_filters", + "codegen_line": 1493, + "notes": [ + "Lowers `stream_get_filters()` to the static built-in filter list." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/io/stream_get_filters.rs", + "sig_line": null + }, + "name": "stream_get_filters", + "sig": { + "params": [], + "return_type": "array", + "variadic": null + }, + "slug": "stream_get_filters", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_context_set_option", - "description": "Lowers `stream_context_set_option(context, options)` and the four-argument form.", + "canonical_name": "stream_get_line", + "description": "Gets line from stream resource up to a given delimiter.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_context_set_option", - "codegen_line": 985, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_get_line", + "codegen_line": 1393, "notes": [ - "Lowers `stream_context_set_option(context, options)` and the four-argument form." + "Lowers `stream_get_line(stream, length, ending?)`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_get_line.rs", "sig_line": null }, - "name": "stream_context_set_option", + "name": "stream_get_line", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "context", + "name": "stream", "optional": false, "type": "resource" }, { "by_ref": false, "default": null, - "name": "wrapper_or_options", + "name": "length", "optional": false, - "type": "string" + "type": "int" }, { "by_ref": false, - "default": null, - "name": "option_name", + "default": "''", + "name": "ending", "optional": true, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "value", - "optional": true, - "type": "mixed" } ], - "return_type": "bool", + "return_type": "string", "variadic": null }, - "slug": "stream_context_set_option", + "slug": "stream_get_line", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_context_set_params", - "description": "Lowers `stream_context_set_params(context, params)` as an accepted parameter update.", + "canonical_name": "stream_get_meta_data", + "description": "Retrieves metadata from streams/file pointers.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_context_set_params", - "codegen_line": 1005, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_get_meta_data", + "codegen_line": 1446, "notes": [ - "Lowers `stream_context_set_params(context, params)` as an accepted parameter update." + "Lowers `stream_get_meta_data(stream)` through the metadata runtime helper." + ], + "runtime_helpers": [ + "__rt_stream_get_meta_data" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_get_meta_data.rs", "sig_line": null }, - "name": "stream_context_set_params", + "name": "stream_get_meta_data", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "context", + "name": "stream", "optional": false, "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "params", - "optional": false, - "type": "array" } ], - "return_type": "bool", + "return_type": "array", "variadic": null }, - "slug": "stream_context_set_params", + "slug": "stream_get_meta_data", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_context_get_options", - "description": "Lowers `stream_context_get_options(context)`.", + "canonical_name": "stream_get_transports", + "description": "Retrieves list of registered socket transports.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_context_get_options", - "codegen_line": 1139, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_get_transports", + "codegen_line": 1477, "notes": [ - "Lowers `stream_context_get_options(context)`." - ], - "runtime_helpers": [ - "__rt_hash_new", - "__rt_incref" + "Lowers `stream_get_transports()` to the static transport list." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_get_transports.rs", "sig_line": null }, - "name": "stream_context_get_options", + "name": "stream_get_transports", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "stream_or_context", - "optional": false, - "type": "resource" - } - ], + "params": [], "return_type": "array", "variadic": null }, - "slug": "stream_context_get_options", + "slug": "stream_get_transports", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_context_get_params", - "description": "Lowers `stream_context_get_params(context)` to an empty associative hash.", + "canonical_name": "stream_get_wrappers", + "description": "Retrieves list of registered streams.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_context_get_params", - "codegen_line": 1178, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_get_wrappers", + "codegen_line": 1461, "notes": [ - "Lowers `stream_context_get_params(context)` to an empty associative hash." + "Lowers `stream_get_wrappers()` to the static built-in wrapper list." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_get_wrappers.rs", "sig_line": null }, - "name": "stream_context_get_params", + "name": "stream_get_wrappers", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "context", - "optional": false, - "type": "resource" - } - ], + "params": [], "return_type": "array", "variadic": null }, - "slug": "stream_context_get_params", + "slug": "stream_get_wrappers", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_resolve_include_path", - "description": "Lowers `stream_resolve_include_path(filename)` as realpath-backed `string|false`.", + "canonical_name": "stream_is_local", + "description": "Checks if a stream is a local stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_resolve_include_path", - "codegen_line": 2142, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_is_local", + "codegen_line": 2103, "notes": [ - "Lowers `stream_resolve_include_path(filename)` as realpath-backed `string|false`." - ], - "runtime_helpers": [ - "__rt_realpath", - "__rt_stream_socket_server" + "Lowers `stream_is_local(stream)` as a true predicate after evaluating its argument." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_is_local.rs", "sig_line": null }, - "name": "stream_resolve_include_path", + "name": "stream_is_local", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filename", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "stream_resolve_include_path", + "slug": "stream_is_local", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_filter_register", - "description": "Lowers `stream_filter_register(filter_name, class)` into the user-filter registry helper.", + "canonical_name": "stream_isatty", + "description": "Checks if a stream is a TTY.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_filter_register", - "codegen_line": 1408, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_isatty", + "codegen_line": 2127, "notes": [ - "Lowers `stream_filter_register(filter_name, class)` into the user-filter registry helper." + "Lowers `stream_isatty(stream)`." ], "runtime_helpers": [ - "__rt_stream_filter_register" + "__rt_stream_isatty" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_isatty.rs", "sig_line": null }, - "name": "stream_filter_register", + "name": "stream_isatty", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "filter_name", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "class", + "name": "stream", "optional": false, - "type": "string" + "type": "resource" } ], "return_type": "bool", "variadic": null }, - "slug": "stream_filter_register", + "slug": "stream_isatty", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_bucket_make_writeable", - "description": "Lowers `stream_bucket_make_writeable(brigade)` by popping the brigade head.", + "canonical_name": "stream_resolve_include_path", + "description": "Resolves filename against the include path.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_bucket_make_writeable", - "codegen_line": 1768, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_resolve_include_path", + "codegen_line": 2361, "notes": [ - "Lowers `stream_bucket_make_writeable(brigade)` by popping the brigade head." + "Lowers `stream_resolve_include_path(filename)` as realpath-backed `string|false`." ], "runtime_helpers": [ - "__rt_stdclass_get" + "__rt_realpath", + "__rt_stream_socket_server" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_resolve_include_path.rs", "sig_line": null }, - "name": "stream_bucket_make_writeable", + "name": "stream_resolve_include_path", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "brigade", + "name": "filename", "optional": false, - "type": "mixed" + "type": "string" } ], "return_type": "mixed", "variadic": null }, - "slug": "stream_bucket_make_writeable", + "slug": "stream_resolve_include_path", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_bucket_new", - "description": "Lowers `stream_bucket_new(stream, data)` into a stdClass-backed bucket object.", + "canonical_name": "stream_select", + "description": "Runs the equivalent of the select() system call on the given arrays of streams.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_bucket_new", - "codegen_line": 1751, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_select", + "codegen_line": 2316, "notes": [ - "Lowers `stream_bucket_new(stream, data)` into a stdClass-backed bucket object." + "Lowers `stream_select(read, write, except, seconds, microseconds?)`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_select.rs", "sig_line": null }, - "name": "stream_bucket_new", + "name": "stream_select", "sig": { "params": [ { - "by_ref": false, + "by_ref": true, "default": null, - "name": "stream", + "name": "read", "optional": false, - "type": "resource" + "type": "array" }, { - "by_ref": false, + "by_ref": true, "default": null, - "name": "buffer", + "name": "write", "optional": false, - "type": "string" - } - ], - "return_type": "mixed", - "variadic": null - }, - "slug": "stream_bucket_new", - "sub_area": "IO" - }, - { - "area": "Streams", - "canonical_name": "stream_bucket_append", - "description": "", - "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": "src/types/signatures.rs", - "sig_line": null - }, - "name": "stream_bucket_append", - "sig": { - "params": [ + "type": "array" + }, { - "by_ref": false, + "by_ref": true, "default": null, - "name": "brigade", + "name": "except", "optional": false, - "type": "mixed" + "type": "array" }, { "by_ref": false, "default": null, - "name": "bucket", + "name": "seconds", "optional": false, - "type": "mixed" + "type": "int" + }, + { + "by_ref": false, + "default": "0", + "name": "microseconds", + "optional": true, + "type": "int" } ], - "return_type": "void", + "return_type": "int", "variadic": null }, - "slug": "stream_bucket_append", - "sub_area": "Streams" + "slug": "stream_select", + "sub_area": "IO" }, { - "area": "Streams", - "canonical_name": "stream_bucket_prepend", - "description": "", + "area": "IO", + "canonical_name": "stream_set_blocking", + "description": "Sets blocking/non-blocking mode on a stream.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_set_blocking", + "codegen_line": 2142, + "notes": [ + "Lowers `stream_set_blocking(stream, enable)`." + ], + "runtime_helpers": [ + "__rt_stream_set_blocking", + "__rt_user_wrapper_set_option" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/stream_set_blocking.rs", "sig_line": null }, - "name": "stream_bucket_prepend", + "name": "stream_set_blocking", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "brigade", + "name": "stream", "optional": false, - "type": "mixed" + "type": "resource" }, { "by_ref": false, "default": null, - "name": "bucket", + "name": "enable", "optional": false, - "type": "mixed" + "type": "bool" } ], - "return_type": "void", + "return_type": "bool", "variadic": null }, - "slug": "stream_bucket_prepend", - "sub_area": "Streams" + "slug": "stream_set_blocking", + "sub_area": "IO" }, { "area": "IO", "canonical_name": "stream_set_chunk_size", - "description": "Lowers `stream_set_chunk_size(stream, size)` and returns the previous size.", + "description": "Sets the read chunk size on a stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_stream_set_chunk_size", - "codegen_line": 1975, + "codegen_line": 2194, "notes": [ "Lowers `stream_set_chunk_size(stream, size)` and returns the previous size." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_set_chunk_size.rs", "sig_line": null }, "name": "stream_set_chunk_size", @@ -15916,21 +16762,21 @@ { "area": "IO", "canonical_name": "stream_set_read_buffer", - "description": "Lowers stream read/write buffer setters as successful no-ops.", + "description": "Sets the read file buffering on a stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_stream_set_buffer", - "codegen_line": 2035, + "codegen_line": 2254, "notes": [ "Lowers stream read/write buffer setters as successful no-ops." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_set_read_buffer.rs", "sig_line": null }, "name": "stream_set_read_buffer", @@ -15959,25 +16805,25 @@ }, { "area": "IO", - "canonical_name": "stream_set_write_buffer", - "description": "Lowers stream read/write buffer setters as successful no-ops.", + "canonical_name": "stream_set_timeout", + "description": "Sets timeout period on a stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_set_buffer", - "codegen_line": 2035, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_set_timeout", + "codegen_line": 2267, "notes": [ - "Lowers stream read/write buffer setters as successful no-ops." + "Lowers `stream_set_timeout(stream, seconds, microseconds?)`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_set_timeout.rs", "sig_line": null }, - "name": "stream_set_write_buffer", + "name": "stream_set_timeout", "sig": { "params": [ { @@ -15990,38 +16836,45 @@ { "by_ref": false, "default": null, - "name": "size", + "name": "seconds", "optional": false, "type": "int" + }, + { + "by_ref": false, + "default": "0", + "name": "microseconds", + "optional": true, + "type": "int" } ], - "return_type": "int", + "return_type": "bool", "variadic": null }, - "slug": "stream_set_write_buffer", + "slug": "stream_set_timeout", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_get_contents", - "description": "Lowers `stream_get_contents(stream, length?, offset?)` to `string|false`.", + "canonical_name": "stream_set_write_buffer", + "description": "Sets the write file buffering on a stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_get_contents", - "codegen_line": 1188, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_set_buffer", + "codegen_line": 2254, "notes": [ - "Lowers `stream_get_contents(stream, length?, offset?)` to `string|false`." + "Lowers stream read/write buffer setters as successful no-ops." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_set_write_buffer.rs", "sig_line": null }, - "name": "stream_get_contents", + "name": "stream_set_write_buffer", "sig": { "params": [ { @@ -16034,138 +16887,131 @@ { "by_ref": false, "default": null, - "name": "length", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "offset", - "optional": true, + "name": "size", + "optional": false, "type": "int" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "stream_get_contents", + "slug": "stream_set_write_buffer", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_get_line", - "description": "Lowers `stream_get_line(stream, length, ending?)`.", + "canonical_name": "stream_socket_accept", + "description": "Accept a connection on a socket created by stream_socket_server().", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_get_line", - "codegen_line": 1280, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_socket_accept", + "codegen_line": 2436, "notes": [ - "Lowers `stream_get_line(stream, length, ending?)`." + "Lowers `stream_socket_accept(server, timeout?, peer_name?)`." + ], + "runtime_helpers": [ + "__rt_stream_socket_accept" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_accept.rs", "sig_line": null }, - "name": "stream_get_line", + "name": "stream_socket_accept", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "socket", "optional": false, "type": "resource" }, { "by_ref": false, - "default": null, - "name": "length", - "optional": false, - "type": "int" + "default": "null", + "name": "timeout", + "optional": true, + "type": "float" }, { - "by_ref": false, - "default": null, - "name": "ending", + "by_ref": true, + "default": "null", + "name": "peer_name", "optional": true, "type": "string" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "stream_get_line", + "slug": "stream_socket_accept", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_get_meta_data", - "description": "Lowers `stream_get_meta_data(stream)` through the metadata runtime helper.", + "canonical_name": "stream_socket_client", + "description": "Open Internet or Unix domain socket connection.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_get_meta_data", - "codegen_line": 1333, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_socket_client", + "codegen_line": 2397, "notes": [ - "Lowers `stream_get_meta_data(stream)` through the metadata runtime helper." + "Lowers `stream_socket_client(address)` and records the connected host for TLS defaults." ], "runtime_helpers": [ - "__rt_stream_get_meta_data" + "__rt_stash_connect_host", + "__rt_stream_socket_client" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_client.rs", "sig_line": null }, - "name": "stream_get_meta_data", + "name": "stream_socket_client", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "address", "optional": false, - "type": "resource" + "type": "string" } ], - "return_type": "array", + "return_type": "mixed", "variadic": null }, - "slug": "stream_get_meta_data", + "slug": "stream_socket_client", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_set_blocking", - "description": "Lowers `stream_set_blocking(stream, enable)`.", + "canonical_name": "stream_socket_enable_crypto", + "description": "Turns encryption on/off on an already connected socket.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_set_blocking", - "codegen_line": 1923, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_socket_enable_crypto", + "codegen_line": 2547, "notes": [ - "Lowers `stream_set_blocking(stream, enable)`." - ], - "runtime_helpers": [ - "__rt_stream_set_blocking", - "__rt_user_wrapper_set_option" + "Lowers `stream_socket_enable_crypto(stream, enable, method?, session_stream?)`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_enable_crypto.rs", "sig_line": null }, - "name": "stream_set_blocking", + "name": "stream_socket_enable_crypto", "sig": { "params": [ { @@ -16181,194 +17027,203 @@ "name": "enable", "optional": false, "type": "bool" + }, + { + "by_ref": false, + "default": "null", + "name": "crypto_method", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "null", + "name": "session_stream", + "optional": true, + "type": "resource" } ], "return_type": "bool", "variadic": null }, - "slug": "stream_set_blocking", + "slug": "stream_socket_enable_crypto", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_set_timeout", - "description": "Lowers `stream_set_timeout(stream, seconds, microseconds?)`.", + "canonical_name": "stream_socket_get_name", + "description": "Retrieve the name of the local or remote sockets.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_set_timeout", - "codegen_line": 2048, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_socket_get_name", + "codegen_line": 2496, "notes": [ - "Lowers `stream_set_timeout(stream, seconds, microseconds?)`." + "Lowers `stream_socket_get_name(socket, remote)` and boxes `string|false`." + ], + "runtime_helpers": [ + "__rt_stream_socket_get_name" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_get_name.rs", "sig_line": null }, - "name": "stream_set_timeout", + "name": "stream_socket_get_name", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "socket", "optional": false, "type": "resource" }, { "by_ref": false, "default": null, - "name": "seconds", + "name": "remote", "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "microseconds", - "optional": true, - "type": "int" + "type": "bool" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "stream_set_timeout", + "slug": "stream_socket_get_name", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_select", - "description": "Lowers `stream_select(read, write, except, seconds, microseconds?)`.", + "canonical_name": "stream_socket_pair", + "description": "Creates a pair of connected, indistinguishable socket streams.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_select", - "codegen_line": 2097, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_socket_pair", + "codegen_line": 2465, "notes": [ - "Lowers `stream_select(read, write, except, seconds, microseconds?)`." + "Lowers `stream_socket_pair(domain, type, protocol)` and boxes `array|false`." + ], + "runtime_helpers": [ + "__rt_stream_socket_pair" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_pair.rs", "sig_line": null }, - "name": "stream_select", + "name": "stream_socket_pair", "sig": { "params": [ { - "by_ref": true, - "default": null, - "name": "read", - "optional": false, - "type": "array" - }, - { - "by_ref": true, - "default": null, - "name": "write", - "optional": false, - "type": "array" - }, - { - "by_ref": true, + "by_ref": false, "default": null, - "name": "except", + "name": "domain", "optional": false, - "type": "array" + "type": "int" }, { "by_ref": false, "default": null, - "name": "seconds", + "name": "type", "optional": false, "type": "int" }, { "by_ref": false, "default": null, - "name": "microseconds", - "optional": true, + "name": "protocol", + "optional": false, "type": "int" } ], - "return_type": "int", + "return_type": "mixed", "variadic": null }, - "slug": "stream_select", + "slug": "stream_socket_pair", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_socket_shutdown", - "description": "Lowers `stream_socket_shutdown(stream, mode)`.", + "canonical_name": "stream_socket_recvfrom", + "description": "Receives data from a socket, connected or not.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_socket_shutdown", - "codegen_line": 2303, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_socket_recvfrom", + "codegen_line": 2599, "notes": [ - "Lowers `stream_socket_shutdown(stream, mode)`." - ], - "runtime_helpers": [ - "__rt_stream_socket_shutdown" + "Lowers `stream_socket_recvfrom(socket, length, flags?, address?)`." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_recvfrom.rs", "sig_line": null }, - "name": "stream_socket_shutdown", + "name": "stream_socket_recvfrom", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "socket", "optional": false, "type": "resource" }, { - "by_ref": false, - "default": null, - "name": "mode", - "optional": false, - "type": "int" + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true, + "type": "int" + }, + { + "by_ref": true, + "default": "''", + "name": "address", + "optional": true, + "type": "string" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "stream_socket_shutdown", + "slug": "stream_socket_recvfrom", "sub_area": "IO" }, { "area": "IO", "canonical_name": "stream_socket_sendto", - "description": "Lowers `stream_socket_sendto(socket, data, flags?, address?)` and boxes `int|false`.", + "description": "Sends a message to a socket, whether it is connected or not.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_stream_socket_sendto", - "codegen_line": 2422, + "codegen_line": 2641, "notes": [ "Lowers `stream_socket_sendto(socket, data, flags?, address?)` and boxes `int|false`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_sendto.rs", "sig_line": null }, "name": "stream_socket_sendto", @@ -16390,14 +17245,14 @@ }, { "by_ref": false, - "default": null, + "default": "0", "name": "flags", "optional": true, "type": "int" }, { "by_ref": false, - "default": null, + "default": "''", "name": "address", "optional": true, "type": "string" @@ -16411,1076 +17266,1185 @@ }, { "area": "IO", - "canonical_name": "stream_socket_recvfrom", - "description": "Lowers `stream_socket_recvfrom(socket, length, flags?, address?)`.", + "canonical_name": "stream_socket_server", + "description": "Create an Internet or Unix domain server socket.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_socket_recvfrom", - "codegen_line": 2380, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_socket_server", + "codegen_line": 2374, "notes": [ - "Lowers `stream_socket_recvfrom(socket, length, flags?, address?)`." + "Lowers `stream_socket_server(address)` and boxes `resource|false`." + ], + "runtime_helpers": [ + "__rt_stream_socket_server" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_server.rs", "sig_line": null }, - "name": "stream_socket_recvfrom", + "name": "stream_socket_server", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "socket", - "optional": false, - "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "length", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "flags", - "optional": true, - "type": "int" - }, - { - "by_ref": true, - "default": null, "name": "address", - "optional": true, + "optional": false, "type": "string" } ], "return_type": "mixed", "variadic": null }, - "slug": "stream_socket_recvfrom", + "slug": "stream_socket_server", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_socket_get_name", - "description": "Lowers `stream_socket_get_name(socket, remote)` and boxes `string|false`.", + "canonical_name": "stream_socket_shutdown", + "description": "Shutdown a full-duplex connection.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_socket_get_name", - "codegen_line": 2277, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_socket_shutdown", + "codegen_line": 2522, "notes": [ - "Lowers `stream_socket_get_name(socket, remote)` and boxes `string|false`." + "Lowers `stream_socket_shutdown(stream, mode)`." ], "runtime_helpers": [ - "__rt_stream_socket_get_name" + "__rt_stream_socket_shutdown" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_socket_shutdown.rs", "sig_line": null }, - "name": "stream_socket_get_name", + "name": "stream_socket_shutdown", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "socket", + "name": "stream", "optional": false, "type": "resource" }, { "by_ref": false, "default": null, - "name": "remote", + "name": "mode", "optional": false, - "type": "bool" + "type": "int" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "stream_socket_get_name", + "slug": "stream_socket_shutdown", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "stream_socket_pair", - "description": "Lowers `stream_socket_pair(domain, type, protocol)` and boxes `array|false`.", + "canonical_name": "stream_supports_lock", + "description": "Tells whether the stream supports locking.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_socket_pair", - "codegen_line": 2246, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_supports_lock", + "codegen_line": 2115, "notes": [ - "Lowers `stream_socket_pair(domain, type, protocol)` and boxes `array|false`." + "Lowers `stream_supports_lock(stream)` as true after resource unboxing." ], "runtime_helpers": [ - "__rt_stream_socket_pair" + "__rt_stream_isatty" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_supports_lock.rs", "sig_line": null }, - "name": "stream_socket_pair", + "name": "stream_supports_lock", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "domain", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "type", - "optional": false, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "protocol", + "name": "stream", "optional": false, - "type": "int" + "type": "resource" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "stream_socket_pair", + "slug": "stream_supports_lock", "sub_area": "IO" }, { - "area": "Process", - "canonical_name": "popen", - "description": "Lowers `popen(command, mode)` and boxes the process pipe as `resource|false`.", + "area": "IO", + "canonical_name": "stream_wrapper_register", + "description": "Registers a URL wrapper implemented as a PHP class.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_popen", - "codegen_line": 3379, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_wrapper_register", + "codegen_line": 1000, "notes": [ - "Lowers `popen(command, mode)` and boxes the process pipe as `resource|false`." + "Lowers `stream_wrapper_register(protocol, class, flags?)`." ], "runtime_helpers": [ - "__rt_popen" + "__rt_stream_wrapper_register" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_wrapper_register.rs", "sig_line": null }, - "name": "popen", + "name": "stream_wrapper_register", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "command", + "name": "protocol", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "mode", + "name": "class", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true, + "type": "int" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "popen", - "sub_area": "Process" + "slug": "stream_wrapper_register", + "sub_area": "IO" }, { - "area": "Process", - "canonical_name": "pclose", - "description": "Lowers `pclose(handle)` and returns the child process status.", + "area": "IO", + "canonical_name": "stream_wrapper_restore", + "description": "Restores a previously unregistered built-in wrapper.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_pclose", - "codegen_line": 3407, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_wrapper_restore", + "codegen_line": 1052, "notes": [ - "Lowers `pclose(handle)` and returns the child process status." - ], - "runtime_helpers": [ - "__rt_pclose" + "Lowers `stream_wrapper_restore(protocol)` as a successful no-op." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_wrapper_restore.rs", "sig_line": null }, - "name": "pclose", + "name": "stream_wrapper_restore", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "handle", + "name": "protocol", "optional": false, - "type": "resource" + "type": "string" } ], - "return_type": "int", + "return_type": "bool", "variadic": null }, - "slug": "pclose", - "sub_area": "Process" + "slug": "stream_wrapper_restore", + "sub_area": "IO" }, { "area": "IO", - "canonical_name": "opendir", - "description": "Lowers `opendir(path)` and boxes the directory stream as `resource|false`.", + "canonical_name": "stream_wrapper_unregister", + "description": "Unregisters a previously registered URL wrapper.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_opendir", - "codegen_line": 3326, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_stream_wrapper_unregister", + "codegen_line": 1030, "notes": [ - "Lowers `opendir(path)` and boxes the directory stream as `resource|false`." + "Lowers `stream_wrapper_unregister(protocol)`." ], "runtime_helpers": [ - "__rt_opendir", - "__rt_readdir", - "__rt_user_wrapper_dir_readdir" + "__rt_stream_wrapper_unregister" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/stream_wrapper_unregister.rs", "sig_line": null }, - "name": "opendir", + "name": "stream_wrapper_unregister", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "directory", + "name": "protocol", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "opendir", + "slug": "stream_wrapper_unregister", "sub_area": "IO" }, { - "area": "IO", - "canonical_name": "readdir", - "description": "Lowers `readdir(dir_handle)` for libc, glob, and userspace-wrapper handles.", + "area": "String", + "canonical_name": "stripslashes", + "description": "Removes backslashes from a string previously escaped by addslashes.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_readdir", - "codegen_line": 3336, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `readdir(dir_handle)` for libc, glob, and userspace-wrapper handles." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_closedir", - "__rt_readdir", - "__rt_user_wrapper_dir_closedir", - "__rt_user_wrapper_dir_readdir" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/stripslashes.rs", "sig_line": null }, - "name": "readdir", + "name": "stripslashes", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "dir_handle", + "name": "string", "optional": false, - "type": "resource" + "type": "string" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "readdir", - "sub_area": "IO" + "slug": "stripslashes", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "closedir", - "description": "Lowers `closedir(dir_handle)` for libc, glob, and userspace-wrapper handles.", + "area": "String", + "canonical_name": "strlen", + "description": "Returns the length of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_closedir", - "codegen_line": 3351, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_strlen", + "codegen_line": 493, "notes": [ - "Lowers `closedir(dir_handle)` for libc, glob, and userspace-wrapper handles." + "Lowers `strlen()` by coercing string-like values and returning the byte length." ], "runtime_helpers": [ - "__rt_closedir", - "__rt_rewinddir", - "__rt_user_wrapper_dir_closedir", - "__rt_user_wrapper_dir_rewinddir" + "__rt_mixed_cast_string" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/strlen.rs", "sig_line": null }, - "name": "closedir", + "name": "strlen", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "dir_handle", + "name": "string", "optional": false, - "type": "resource" + "type": "string" } ], - "return_type": "void", + "return_type": "int", "variadic": null }, - "slug": "closedir", - "sub_area": "IO" + "slug": "strlen", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "rewinddir", - "description": "Lowers `rewinddir(dir_handle)` for libc, glob, and userspace-wrapper handles.", + "area": "String", + "canonical_name": "strpos", + "description": "Finds the numeric position of the first occurrence of a substring.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_rewinddir", - "codegen_line": 3365, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_string_position", + "codegen_line": 700, "notes": [ - "Lowers `rewinddir(dir_handle)` for libc, glob, and userspace-wrapper handles." - ], - "runtime_helpers": [ - "__rt_rewinddir", - "__rt_user_wrapper_dir_rewinddir" + "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/strpos.rs", "sig_line": null }, - "name": "rewinddir", + "name": "strpos", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "dir_handle", + "name": "haystack", "optional": false, - "type": "resource" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true, + "type": "int" } ], - "return_type": "void", + "return_type": "mixed", "variadic": null }, - "slug": "rewinddir", - "sub_area": "IO" + "slug": "strpos", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "gethostname", - "description": "Lowers `gethostname()` through the shared runtime helper.", + "area": "String", + "canonical_name": "strrev", + "description": "Reverses a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_gethostname", - "codegen_line": 3188, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `gethostname()` through the shared runtime helper." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_gethostbyaddr", - "__rt_gethostbyname", - "__rt_gethostname" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/strrev.rs", "sig_line": null }, - "name": "gethostname", + "name": "strrev", "sig": { - "params": [], + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "string" + } + ], "return_type": "string", "variadic": null }, - "slug": "gethostname", - "sub_area": "IO" + "slug": "strrev", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "gethostbyname", - "description": "Lowers `gethostbyname(hostname)` through the shared runtime resolver.", + "area": "String", + "canonical_name": "strrpos", + "description": "Finds the numeric position of the last occurrence of a substring.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_gethostbyname", - "codegen_line": 3198, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_string_position", + "codegen_line": 700, "notes": [ - "Lowers `gethostbyname(hostname)` through the shared runtime resolver." - ], - "runtime_helpers": [ - "__rt_gethostbyaddr", - "__rt_gethostbyname" + "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/strrpos.rs", "sig_line": null }, - "name": "gethostbyname", + "name": "strrpos", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "hostname", + "name": "haystack", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "needle", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true, + "type": "int" } ], - "return_type": "string", + "return_type": "mixed", "variadic": null }, - "slug": "gethostbyname", - "sub_area": "IO" + "slug": "strrpos", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "gethostbyaddr", - "description": "Lowers `gethostbyaddr(address)` and boxes malformed addresses as PHP `false`.", + "area": "String", + "canonical_name": "strstr", + "description": "Returns the portion of a string starting at the first occurrence of a substring.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_gethostbyaddr", - "codegen_line": 3210, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_strstr", + "codegen_line": 762, "notes": [ - "Lowers `gethostbyaddr(address)` and boxes malformed addresses as PHP `false`." - ], - "runtime_helpers": [ - "__rt_gethostbyaddr", - "__rt_getprotobyname" + "Lowers `strstr(haystack, needle)` by searching and returning the matching suffix." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/strstr.rs", "sig_line": null }, - "name": "gethostbyaddr", + "name": "strstr", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "ip", + "name": "haystack", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "needle", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "false", + "name": "before_needle", + "optional": true, + "type": "bool" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "gethostbyaddr", - "sub_area": "IO" + "slug": "strstr", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "getprotobyname", - "description": "Lowers `getprotobyname(protocol)` and boxes a missing entry as PHP `false`.", + "area": "String", + "canonical_name": "strtolower", + "description": "Converts a string to lowercase.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_getprotobyname", - "codegen_line": 3223, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `getprotobyname(protocol)` and boxes a missing entry as PHP `false`." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_getprotobyname" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/strtolower.rs", "sig_line": null }, - "name": "getprotobyname", + "name": "strtolower", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "protocol", + "name": "string", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "getprotobyname", - "sub_area": "IO" + "slug": "strtolower", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "getprotobynumber", - "description": "Lowers `getprotobynumber(number)` and boxes a missing entry as PHP `false`.", + "area": "Date", + "canonical_name": "strtotime", + "description": "Parses an English textual datetime description into a Unix timestamp.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_getprotobynumber", - "codegen_line": 3246, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_strtotime", + "codegen_line": 487, "notes": [ - "Lowers `getprotobynumber(number)` and boxes a missing entry as PHP `false`." + "Lowers `strtotime(datetime[, baseTimestamp])` through the shared parser runtime helper.", + "Returns PHP's `int|false`: the `__rt_strtotime` `i64::MIN` parse-failure sentinel is boxed as", + "`Mixed` `false`, and every other value (including a real `-1` pre-epoch timestamp) is boxed as", + "a `Mixed` integer, so `=== false`, `=== -1`, and `echo` all observe the distinct results.", + "Supports PHP's optional `$baseTimestamp`. (The `__elephc_strtotime_raw` alias keeps the plain", + "`-1` integer shape for the synthetic `DateTime` internals.)" ], "runtime_helpers": [ - "__rt_getprotobynumber" + "__rt_mixed_from_value", + "__rt_strtotime" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/strtotime.rs", "sig_line": null }, - "name": "getprotobynumber", + "name": "strtotime", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "protocol", + "name": "datetime", "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "null", + "name": "baseTimestamp", + "optional": true, "type": "int" } ], "return_type": "mixed", "variadic": null }, - "slug": "getprotobynumber", - "sub_area": "IO" + "slug": "strtotime", + "sub_area": "Date" }, { - "area": "IO", - "canonical_name": "getservbyname", - "description": "Lowers `getservbyname(service, protocol)` and boxes a missing entry as PHP `false`.", + "area": "String", + "canonical_name": "strtoupper", + "description": "Converts a string to uppercase.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_getservbyname", - "codegen_line": 3265, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `getservbyname(service, protocol)` and boxes a missing entry as PHP `false`." + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_getservbyname" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/strtoupper.rs", "sig_line": null }, - "name": "getservbyname", + "name": "strtoupper", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "service", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "protocol", + "name": "string", "optional": false, "type": "string" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "getservbyname", - "sub_area": "IO" + "slug": "strtoupper", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "getservbyport", - "description": "Lowers `getservbyport(port, protocol)` and boxes a missing entry as PHP `false`.", + "area": "String", + "canonical_name": "substr", + "description": "Returns a portion of a string specified by the offset and length.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_getservbyport", - "codegen_line": 3296, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_substr", + "codegen_line": 713, "notes": [ - "Lowers `getservbyport(port, protocol)` and boxes a missing entry as PHP `false`." + "Lowers `substr(string, offset, length?)` with target-local pointer arithmetic." ], "runtime_helpers": [ - "__rt_getservbyport" + "__rt_substr_replace" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/substr.rs", "sig_line": null }, - "name": "getservbyport", + "name": "substr", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "port", + "name": "string", "optional": false, - "type": "int" + "type": "string" }, { "by_ref": false, "default": null, - "name": "protocol", + "name": "offset", "optional": false, - "type": "string" + "type": "int" + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true, + "type": "int" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "getservbyport", - "sub_area": "IO" + "slug": "substr", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "stream_copy_to_stream", - "description": "Lowers `stream_copy_to_stream(from, to, length?, offset?)` through wrapper-aware read/write loops.", + "area": "String", + "canonical_name": "substr_replace", + "description": "Replaces text within a portion of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_copy_to_stream", - "codegen_line": 1247, + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", + "codegen_function": "lower_substr_replace", + "codegen_line": 730, "notes": [ - "Lowers `stream_copy_to_stream(from, to, length?, offset?)` through wrapper-aware read/write loops." + "Lowers `substr_replace(string, replacement, start, length?)`." + ], + "runtime_helpers": [ + "__rt_str_repeat", + "__rt_substr_replace" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/substr_replace.rs", "sig_line": null }, - "name": "stream_copy_to_stream", + "name": "substr_replace", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "from", + "name": "string", "optional": false, - "type": "resource" + "type": "string" }, { "by_ref": false, "default": null, - "name": "to", + "name": "replace", "optional": false, - "type": "resource" + "type": "string" }, { "by_ref": false, "default": null, - "name": "length", - "optional": true, + "name": "offset", + "optional": false, "type": "int" }, { "by_ref": false, - "default": null, - "name": "offset", + "default": "null", + "name": "length", "optional": true, "type": "int" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "stream_copy_to_stream", - "sub_area": "IO" + "slug": "substr_replace", + "sub_area": "String" }, { - "area": "IO", - "canonical_name": "stream_is_local", - "description": "Lowers `stream_is_local(stream)` as a true predicate after evaluating its argument.", + "area": "Filesystem", + "canonical_name": "symlink", + "description": "Creates a symbolic link.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_is_local", - "codegen_line": 1884, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_symlink", + "codegen_line": 5448, "notes": [ - "Lowers `stream_is_local(stream)` as a true predicate after evaluating its argument." + "Lowers `symlink(target, link)` through the target-aware libc wrapper." + ], + "runtime_helpers": [ + "__rt_fileatime", + "__rt_link", + "__rt_readlink", + "__rt_symlink" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/symlink.rs", "sig_line": null }, - "name": "stream_is_local", + "name": "symlink", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "target", "optional": false, - "type": "resource" + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "link", + "optional": false, + "type": "string" } ], "return_type": "bool", "variadic": null }, - "slug": "stream_is_local", - "sub_area": "IO" + "slug": "symlink", + "sub_area": "Filesystem" }, { - "area": "IO", - "canonical_name": "stream_supports_lock", - "description": "Lowers `stream_supports_lock(stream)` as true after resource unboxing.", + "area": "Filesystem", + "canonical_name": "sys_get_temp_dir", + "description": "Returns the directory path used for temporary files.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_supports_lock", - "codegen_line": 1896, + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_sys_get_temp_dir", + "codegen_line": 5403, "notes": [ - "Lowers `stream_supports_lock(stream)` as true after resource unboxing." + "Lowers `sys_get_temp_dir()` as the project's hardcoded `/tmp` string." ], "runtime_helpers": [ - "__rt_stream_isatty" + "__rt_tmpfile" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/sys_get_temp_dir.rs", "sig_line": null }, - "name": "stream_supports_lock", + "name": "sys_get_temp_dir", "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "stream", - "optional": false, - "type": "resource" - } - ], - "return_type": "bool", + "params": [], + "return_type": "string", "variadic": null }, - "slug": "stream_supports_lock", - "sub_area": "IO" + "slug": "sys_get_temp_dir", + "sub_area": "Filesystem" }, { - "area": "IO", - "canonical_name": "stream_get_transports", - "description": "Lowers `stream_get_transports()` to the static transport list.", + "area": "Process", + "canonical_name": "system", + "description": "Executes an external program and displays the output.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_get_transports", - "codegen_line": 1364, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_system", + "codegen_line": 706, "notes": [ - "Lowers `stream_get_transports()` to the static transport list." + "Lowers `system(command)` through libc `system()` and returns the compiler's empty string result." + ], + "runtime_helpers": [ + "__rt_shell_exec" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/system.rs", "sig_line": null }, - "name": "stream_get_transports", + "name": "system", "sig": { - "params": [], - "return_type": "array", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false, + "type": "string" + } + ], + "return_type": "string", "variadic": null }, - "slug": "stream_get_transports", - "sub_area": "IO" + "slug": "system", + "sub_area": "Process" }, { - "area": "IO", - "canonical_name": "stream_get_wrappers", - "description": "Lowers `stream_get_wrappers()` to the static built-in wrapper list.", + "area": "Math", + "canonical_name": "tan", + "description": "Returns the tangent of a number (radians).", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_get_wrappers", - "codegen_line": 1348, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers `stream_get_wrappers()` to the static built-in wrapper list." + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/tan.rs", "sig_line": null }, - "name": "stream_get_wrappers", + "name": "tan", "sig": { - "params": [], - "return_type": "array", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "float" + } + ], + "return_type": "float", "variadic": null }, - "slug": "stream_get_wrappers", - "sub_area": "IO" + "slug": "tan", + "sub_area": "Math" }, { - "area": "IO", - "canonical_name": "stream_get_filters", - "description": "Lowers `stream_get_filters()` to the static built-in filter list.", + "area": "Math", + "canonical_name": "tanh", + "description": "Returns the hyperbolic tangent of a number.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_get_filters", - "codegen_line": 1380, + "codegen_file": "src/codegen/lower_inst/builtins/math/libm.rs", + "codegen_function": "lower_unary_libm", + "codegen_line": 22, "notes": [ - "Lowers `stream_get_filters()` to the static built-in filter list." + "Lowers a one-argument libm builtin such as `sin()`, `cos()`, or `exp()`." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/math/tanh.rs", "sig_line": null }, - "name": "stream_get_filters", + "name": "tanh", "sig": { - "params": [], - "return_type": "array", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "float" + } + ], + "return_type": "float", "variadic": null }, - "slug": "stream_get_filters", - "sub_area": "IO" + "slug": "tanh", + "sub_area": "Math" }, { - "area": "Streams", - "canonical_name": "stream_filter_append", - "description": "", + "area": "Filesystem", + "canonical_name": "tempnam", + "description": "Creates a file with a unique filename.", "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": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_tempnam", + "codegen_line": 4453, + "notes": [ + "Lowers `tempnam(directory, prefix)` through the target-aware runtime helper." + ], + "runtime_helpers": [ + "__rt_glob", + "__rt_scandir", + "__rt_tempnam" + ], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/tempnam.rs", "sig_line": null }, - "name": "stream_filter_append", + "name": "tempnam", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", + "name": "directory", "optional": false, - "type": "resource" + "type": "string" }, { "by_ref": false, "default": null, - "name": "filter_name", + "name": "prefix", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "mode", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": null, - "name": "params", - "optional": true, - "type": "mixed" } ], + "return_type": "string", + "variadic": null + }, + "slug": "tempnam", + "sub_area": "Filesystem" + }, + { + "area": "Date", + "canonical_name": "time", + "description": "Returns the current Unix timestamp.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", + "codegen_function": "lower_time", + "codegen_line": 615, + "notes": [ + "Lowers `time()` through the shared wall-clock runtime helper." + ], + "runtime_helpers": [ + "__rt_time" + ], + "sig_arm": null, + "sig_file": "src/builtins/system/time.rs", + "sig_line": null + }, + "name": "time", + "sig": { + "params": [], + "return_type": "int", + "variadic": null + }, + "slug": "time", + "sub_area": "Date" + }, + { + "area": "Filesystem", + "canonical_name": "tmpfile", + "description": "Creates a temporary 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_tmpfile", + "codegen_line": 5416, + "notes": [ + "Lowers `tmpfile()` and boxes the anonymous stream descriptor or PHP false." + ], + "runtime_helpers": [ + "__rt_filemtime", + "__rt_linkinfo", + "__rt_tmpfile" + ], + "sig_arm": null, + "sig_file": "src/builtins/io/tmpfile.rs", + "sig_line": null + }, + "name": "tmpfile", + "sig": { + "params": [], "return_type": "mixed", "variadic": null }, - "slug": "stream_filter_append", - "sub_area": "Streams" + "slug": "tmpfile", + "sub_area": "Filesystem" }, { - "area": "Streams", - "canonical_name": "stream_filter_prepend", - "description": "", + "area": "Filesystem", + "canonical_name": "touch", + "description": "Sets access and modification time of a file.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": null, - "codegen_function": null, - "codegen_line": null, - "notes": [], + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", + "codegen_function": "lower_touch", + "codegen_line": 4523, + "notes": [ + "Lowers `touch(path, mtime?, atime?)` through the target-aware runtime helper." + ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/types/signatures.rs", + "sig_file": "src/builtins/io/touch.rs", "sig_line": null }, - "name": "stream_filter_prepend", + "name": "touch", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream", - "optional": false, - "type": "resource" - }, - { - "by_ref": false, - "default": null, - "name": "filter_name", + "name": "filename", "optional": false, "type": "string" }, { "by_ref": false, - "default": null, - "name": "mode", + "default": "null", + "name": "mtime", "optional": true, "type": "int" }, { "by_ref": false, - "default": null, - "name": "params", + "default": "null", + "name": "atime", "optional": true, - "type": "mixed" + "type": "int" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "stream_filter_prepend", - "sub_area": "Streams" + "slug": "touch", + "sub_area": "Filesystem" }, { - "area": "IO", - "canonical_name": "stream_filter_remove", - "description": "Lowers `stream_filter_remove(filter)` and clears both direction tables for the fd.", + "area": "Class", + "canonical_name": "trait_exists", + "description": "Checks whether the trait exists.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_stream_filter_remove", - "codegen_line": 1720, + "codegen_file": "src/codegen/lower_inst/builtins.rs", + "codegen_function": "lower_class_like_exists", + "codegen_line": 293, "notes": [ - "Lowers `stream_filter_remove(filter)` and clears both direction tables for the fd." - ], - "runtime_helpers": [ - "__rt_user_filter_release_fd" + "Lowers AOT class/interface/enum existence checks for literal names." ], + "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/callables/trait_exists.rs", "sig_line": null }, - "name": "stream_filter_remove", + "name": "trait_exists", "sig": { "params": [ { "by_ref": false, "default": null, - "name": "stream_filter", + "name": "trait", "optional": false, - "type": "resource" + "type": "string" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" } ], "return_type": "bool", "variadic": null }, - "slug": "stream_filter_remove", - "sub_area": "IO" + "slug": "trait_exists", + "sub_area": "Class" }, { "area": "String", "canonical_name": "trim", - "description": "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks.", + "description": "Strips whitespace (or other characters) from the beginning and end of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", "codegen_line": 112, "notes": [ @@ -17488,7 +18452,7 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/trim.rs", "sig_line": null }, "name": "trim", @@ -17503,7 +18467,7 @@ }, { "by_ref": false, - "default": null, + "default": "' \\n\\r\\t\\x0b\\x0c\\x00'", "name": "characters", "optional": true, "type": "string" @@ -17518,21 +18482,23 @@ { "area": "Array", "canonical_name": "uasort", - "description": "Lowers `uasort()` through the legacy user-sort helper for static comparators.", + "description": "Sorts an array with a user-defined comparison function and maintains index association.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_uasort", - "codegen_line": 1131, + "codegen_line": 1134, "notes": [ - "Lowers `uasort()` through the legacy user-sort helper for static comparators." + "Lowers `uasort()` through the user-sort helper for static comparators." + ], + "runtime_helpers": [ + "__rt_array_is_list" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/uasort.rs", "sig_line": null }, "name": "uasort", @@ -17562,13 +18528,13 @@ { "area": "String", "canonical_name": "ucfirst", - "description": "Lowers `ucfirst()` by copying the string and uppercasing the first ASCII byte.", + "description": "Uppercases the first character of a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ucfirst", "codegen_line": 96, "notes": [ @@ -17578,7 +18544,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ucfirst.rs", "sig_line": null }, "name": "ucfirst", @@ -17601,13 +18567,13 @@ { "area": "String", "canonical_name": "ucwords", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "description": "Uppercases the first character of each word in a string.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_unary_string_runtime", "codegen_line": 76, "notes": [ @@ -17618,7 +18584,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/ucwords.rs", "sig_line": null }, "name": "ucwords", @@ -17633,7 +18599,7 @@ }, { "by_ref": false, - "default": null, + "default": "' \\t\\r\\n\\x0c\\x0b'", "name": "separators", "optional": true, "type": "string" @@ -17648,21 +18614,23 @@ { "area": "Array", "canonical_name": "uksort", - "description": "Lowers `uksort()` through the legacy user-sort helper for static comparators.", + "description": "Sorts an array by keys using a user-defined comparison function.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_uksort", - "codegen_line": 1126, + "codegen_line": 1129, "notes": [ - "Lowers `uksort()` through the legacy user-sort helper for static comparators." + "Lowers `uksort()` through the user-sort helper for static comparators." + ], + "runtime_helpers": [ + "__rt_array_is_list" ], - "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/uksort.rs", "sig_line": null }, "name": "uksort", @@ -17692,15 +18660,15 @@ { "area": "Filesystem", "canonical_name": "umask", - "description": "Lowers `umask(mask?)` through the target-aware runtime helper.", + "description": "Changes the current umask.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_umask", - "codegen_line": 3850, + "codegen_line": 4493, "notes": [ "Lowers `umask(mask?)` through the target-aware runtime helper." ], @@ -17708,7 +18676,7 @@ "__rt_umask" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/umask.rs", "sig_line": null }, "name": "umask", @@ -17716,7 +18684,7 @@ "params": [ { "by_ref": false, - "default": null, + "default": "null", "name": "mask", "optional": true, "type": "int" @@ -17731,15 +18699,15 @@ { "area": "Filesystem", "canonical_name": "unlink", - "description": "Lowers `unlink(path)` through the target-aware runtime helper.", + "description": "Deletes a file.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_unlink", - "codegen_line": 3764, + "codegen_line": 4407, "notes": [ "Lowers `unlink(path)` through the target-aware runtime helper." ], @@ -17749,7 +18717,7 @@ "__rt_unlink" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/unlink.rs", "sig_line": null }, "name": "unlink", @@ -17769,6 +18737,57 @@ "slug": "unlink", "sub_area": "Filesystem" }, + { + "area": "Misc", + "canonical_name": "unserialize", + "description": "Creates a PHP value from a stored representation.", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/serialize.rs", + "codegen_function": "lower_unserialize", + "codegen_line": 164, + "notes": [ + "Lowers `unserialize($data, $options?)` into the shared unserialize runtime helper.", + "The source string is parsed by `__rt_unserialize_mixed`; a null result pointer", + "(parse error or unsupported wire form) is boxed as PHP `false`. The optional", + "`$options` argument is accepted but currently ignored." + ], + "runtime_helpers": [ + "__rt_mixed_cast_string", + "__rt_unserialize_begin", + "__rt_unserialize_mixed" + ], + "sig_arm": null, + "sig_file": "src/builtins/system/unserialize.rs", + "sig_line": null + }, + "name": "unserialize", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "[]", + "name": "options", + "optional": true, + "type": "mixed" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "unserialize", + "sub_area": "Misc" + }, { "area": "Misc", "canonical_name": "unset", @@ -17778,7 +18797,7 @@ "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/types.rs", + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_unset_builtin", "codegen_line": 48, "notes": [ @@ -17809,13 +18828,13 @@ { "area": "String", "canonical_name": "urldecode", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "description": "Decodes a URL-encoded string, including '+' as a space.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_unary_string_runtime", "codegen_line": 76, "notes": [ @@ -17826,7 +18845,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/urldecode.rs", "sig_line": null }, "name": "urldecode", @@ -17849,13 +18868,13 @@ { "area": "String", "canonical_name": "urlencode", - "description": "Lowers a one-argument string builtin that directly delegates to a runtime helper.", + "description": "URL-encodes a string using application/x-www-form-urlencoded rules.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_unary_string_runtime", "codegen_line": 76, "notes": [ @@ -17866,7 +18885,7 @@ "__rt_strcopy" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/urlencode.rs", "sig_line": null }, "name": "urlencode", @@ -17889,13 +18908,13 @@ { "area": "Process", "canonical_name": "usleep", - "description": "Lowers `usleep(microseconds)` through the target's C library symbol.", + "description": "Delays execution for a number of microseconds.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/system.rs", + "codegen_file": "src/codegen/lower_inst/builtins/system.rs", "codegen_function": "lower_usleep", "codegen_line": 625, "notes": [ @@ -17905,7 +18924,7 @@ "__rt_getenv" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/system/usleep.rs", "sig_line": null }, "name": "usleep", @@ -17928,21 +18947,23 @@ { "area": "Array", "canonical_name": "usort", - "description": "Lowers `usort()` for indexed integer arrays with a static user comparator.", + "description": "Sorts an array by values using a user-defined comparison function.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/arrays.rs", + "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_usort", - "codegen_line": 1121, + "codegen_line": 1124, "notes": [ "Lowers `usort()` for indexed integer arrays with a static user comparator." ], - "runtime_helpers": [], + "runtime_helpers": [ + "__rt_array_is_list" + ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/array/usort.rs", "sig_line": null }, "name": "usort", @@ -17978,7 +18999,7 @@ "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/debug.rs", + "codegen_file": "src/codegen/lower_inst/builtins/debug.rs", "codegen_function": "lower_var_dump", "codegen_line": 35, "notes": [ @@ -17986,14 +19007,22 @@ ], "runtime_helpers": [], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/var_dump.rs", "sig_line": null }, "name": "var_dump", "sig": { - "params": [], + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + } + ], "return_type": "void", - "variadic": "values" + "variadic": null }, "slug": "var_dump", "sub_area": "Variable" @@ -18001,15 +19030,15 @@ { "area": "IO", "canonical_name": "vfprintf", - "description": "Lowers `vfprintf(stream, format, values)` through `__rt_vsprintf` then fwrite.", + "description": "Write a formatted string to a stream.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", + "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_vfprintf", - "codegen_line": 2677, + "codegen_line": 2898, "notes": [ "Lowers `vfprintf(stream, format, values)` through `__rt_vsprintf` then fwrite." ], @@ -18018,7 +19047,7 @@ "__rt_vsprintf" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/io/vfprintf.rs", "sig_line": null }, "name": "vfprintf", @@ -18055,13 +19084,13 @@ { "area": "String", "canonical_name": "vprintf", - "description": "Lowers `vprintf(format, values)` as `vsprintf()` followed by stdout emission.", + "description": "Outputs a formatted string using an array of values.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_vprintf", "codegen_line": 530, "notes": [ @@ -18071,7 +19100,7 @@ "__rt_sprintf" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/vprintf.rs", "sig_line": null }, "name": "vprintf", @@ -18101,13 +19130,13 @@ { "area": "String", "canonical_name": "vsprintf", - "description": "Lowers `vsprintf(format, values)` through the array-to-sprintf runtime bridge.", + "description": "Returns a formatted string using an array of values.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_vsprintf", "codegen_line": 524, "notes": [ @@ -18117,7 +19146,7 @@ "__rt_sprintf" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/vsprintf.rs", "sig_line": null }, "name": "vsprintf", @@ -18147,13 +19176,13 @@ { "area": "String", "canonical_name": "wordwrap", - "description": "Lowers `wordwrap(string, width?, break?, cut?)` through the shared runtime helper.", + "description": "Wraps a string to a given number of characters.", "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/strings.rs", + "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_wordwrap", "codegen_line": 802, "notes": [ @@ -18164,7 +19193,7 @@ "__rt_wordwrap" ], "sig_arm": null, - "sig_file": null, + "sig_file": "src/builtins/string/wordwrap.rs", "sig_line": null }, "name": "wordwrap", @@ -18179,21 +19208,21 @@ }, { "by_ref": false, - "default": null, + "default": "75", "name": "width", "optional": true, "type": "int" }, { "by_ref": false, - "default": null, + "default": "'\\n'", "name": "break", "optional": true, "type": "string" }, { "by_ref": false, - "default": null, + "default": "false", "name": "cut_long_words", "optional": true, "type": "bool" @@ -18204,88 +19233,5 @@ }, "slug": "wordwrap", "sub_area": "String" - }, - { - "area": "IO", - "canonical_name": "__elephc_phar_list_entries", - "description": "Internal helper used by the built-in Phar / PharData support to enumerate archive entries.", - "in_catalog": false, - "is_internal": true, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_elephc_phar_list_entries", - "codegen_line": 3634, - "notes": [ - "Internal helper used by the built-in Phar / PharData support to enumerate archive entries.", - "Calls the native PHAR listing bridge and returns the entries as an array." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "__elephc_phar_list_entries", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "filename", - "optional": false, - "type": "mixed" - } - ], - "return_type": "array", - "variadic": null - }, - "slug": "__elephc_phar_list_entries", - "sub_area": "IO" - }, - { - "area": "IO", - "canonical_name": "__elephc_phar_set_compression", - "description": "Internal helper used by the built-in Phar / PharData support to change archive compression.", - "in_catalog": false, - "is_internal": true, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen_ir/lower_inst/builtins/io.rs", - "codegen_function": "lower_elephc_phar_set_compression", - "codegen_line": 3576, - "notes": [ - "Internal helper used by the built-in Phar / PharData support to change archive compression.", - "Calls the native PHAR compression-control bridge and returns whether the update succeeded." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": null, - "sig_line": null - }, - "name": "__elephc_phar_set_compression", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "filename", - "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "compression", - "optional": false, - "type": "mixed" - } - ], - "return_type": "bool", - "variadic": null - }, - "slug": "__elephc_phar_set_compression", - "sub_area": "IO" } ] \ No newline at end of file diff --git a/scripts/docs/elephc_builtins/extract.py b/scripts/docs/elephc_builtins/extract.py index 2c03ef8875..3f5dc2535e 100644 --- a/scripts/docs/elephc_builtins/extract.py +++ b/scripts/docs/elephc_builtins/extract.py @@ -1,17 +1,27 @@ -"""Extract builtin metadata from the Elephc source tree. - -We parse three layers: - -1. ``src/types/checker/builtins/catalog.rs`` — the canonical list of - PHP-visible builtins. This is our *set* of builtins. -2. ``src/types/signatures.rs`` — per-builtin canonical call signatures - (param names, variadic, by-ref, first-class return type). -3. ``src/codegen_ir/lower_inst/builtins.rs`` and the per-area submodules - — for each builtin we capture the lowering function name, the runtime - helpers it calls, and the leading /// doc comment of that function. - -The output is a list of :class:`registry.Builtin` written to a JSON file -in ``scripts/docs/builtin_registry.json``. +"""Extract builtin metadata from the Elephc `builtin!` registry. + +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 +--include-internal``), NOT by regex-scraping ``catalog.rs`` / ``signatures.rs`` +(which the migration emptied). + +For each builtin we enrich the registry data with: + +1. its lowering location — the emitter its home-file ``lower`` hook dispatches to, + plus that emitter's ``__rt_*`` runtime helpers and leading ``///`` doc notes, +2. its documentation area (derived from the lowering file path, as before), +3. optional type-precision refinements for non-scalar params/returns that the + registry represents coarsely as ``Mixed`` (``PARAM_TYPES`` / ``RETURN_TYPE_OVERRIDES``). + +The 8 PHP language constructs that intentionally stay checker-resident +(``isset``/``unset``/``empty``/``exit``/``die``/``buffer_len``/``buffer_free``/ +``buffer_new``) are not in the registry; they are added from a small hand-curated +table so their documentation pages are preserved. + +The output is a list of :class:`registry.Builtin` written to a JSON file in +``scripts/docs/builtin_registry.json``. """ from __future__ import annotations @@ -19,6 +29,7 @@ import argparse import json import re +import subprocess import sys from pathlib import Path from typing import Optional @@ -31,437 +42,127 @@ AREA_BY_LOWERING_FN, AREA_BY_MODULE, AREA_BY_NAME, - AREAS, Builtin, BuiltinSig, DESCRIPTION_OVERRIDES, INTERNAL_NOTES, LoweringInfo, - OPTIONAL_PARAM_OVERRIDES, - PARAM_NAME_OVERRIDES, PARAM_TYPES, - REF_PARAM_OVERRIDES, Parameter, RETURN_TYPE_OVERRIDES, - VARIADIC_OVERRIDES, slug, ) # --------------------------------------------------------------------------- -# catalog.rs -# --------------------------------------------------------------------------- - -# We pull SUPPORTED_BUILTIN_FUNCTIONS and INTERNAL_BUILTIN_FUNCTIONS straight -# out of catalog.rs. The file is a simple list-of-string-literals, so a small -# state machine is enough — no need for a full Rust parser. - -def parse_catalog(path: Path) -> tuple[list[str], list[str]]: - """Return (supported_names, internal_names) from catalog.rs.""" - src = path.read_text(encoding="utf-8") - return _extract_string_list(src, "SUPPORTED_BUILTIN_FUNCTIONS"), _extract_string_list( - src, "INTERNAL_BUILTIN_FUNCTIONS" - ) - - -def _extract_string_list(src: str, const_name: str) -> list[str]: - pattern = re.compile( - r"const\s+" + re.escape(const_name) + r"\s*:\s*&?\[?&?str\]\s*=\s*&?\[(.*?)\];", - re.DOTALL, - ) - match = pattern.search(src) - if not match: - return [] - body = match.group(1) - return re.findall(r'"([^"]+)"', body) - - -# --------------------------------------------------------------------------- -# signatures.rs +# Registry source of truth: the `gen_builtins` binary # --------------------------------------------------------------------------- -# Each arm of `builtin_call_sig` looks like: -# "name" | "alias" | ... => Some(fixed(&["a", "b"])), -# or -# "name" => Some(optional(&["a", "b"], 2, vec![int_lit(0)])), -# or -# "name" => Some(variadic(&["a"], "rest")), -# or -# "name" => { -# let mut sig = first_param_ref(fixed(&["a", "b"])); -# sig.ref_params[2] = true; -# Some(sig) -# } -# -# And `first_class_callable_builtin_sig` / `general_first_class_callable_builtin_sig` -# carry the canonical return type: -# "name" => Some(typed_first_class_builtin_sig(name, &[PhpType::Str], PhpType::Str)) -# or -# "name" => Some(FunctionSig { return_type: PhpType::Int, ... }) - -_FN_CALL_SIG_RE = re.compile( - r'"([^"]+)"\s*(?:\|\s*"[^"]+"\s*)*=>\s*Some\(([a-z_]+)\(([^)]*)\)\)', - re.DOTALL, -) - - -def _split_name_list(arm: str) -> list[str]: - return re.findall(r'"([^"]+)"', arm) - - -def _split_args(args: str) -> list[str]: - # split top-level commas only — args of variadic(&["a"], "b") must stay grouped - depth = 0 - parts: list[str] = [] - buf: list[str] = [] - for ch in args: - if ch in "([{": - depth += 1 - elif ch in ")]}": - depth -= 1 - if ch == "," and depth == 0: - parts.append("".join(buf).strip()) - buf = [] - else: - buf.append(ch) - if buf: - parts.append("".join(buf).strip()) - return parts - - -def _parse_param_list(s: str) -> list[str]: - """`&["a", "b"]` → ['a', 'b']""" - return re.findall(r'"([^"]+)"', s) - - -def _parse_optional_defaults(s: str) -> dict[int, str]: - """`vec![int_lit(0), string_lit(" ")]` → {0: '0', 1: "' '"}""" - out: dict[int, str] = {} - for idx, expr in enumerate(_split_args(s)): - m = re.match(r"(int|bool|string|null)_lit\((.*)\)$", expr.strip()) - if m: - kind, raw = m.group(1), m.group(2).strip() - if kind == "int": - out[idx] = raw - elif kind == "bool": - out[idx] = raw - elif kind == "string": - out[idx] = repr(raw) # render as PHP string - elif kind == "null": - out[idx] = "null" - return out - - -def _default_expr_renderer(expr_kind: str, raw: str) -> str: - """Render a few special default expressions from signatures.rs.""" - expr_kind = expr_kind.strip() - raw = raw.strip() - if expr_kind == "FloatLiteral": - # default for log() base is e — render as M_E - return "M_E" - if expr_kind == "ArrayLiteral": - return "[]" - if expr_kind in ("int", "Int"): - return raw - if expr_kind in ("bool", "Bool"): - return raw - if expr_kind in ("string", "Str"): - return repr(raw) - if expr_kind in ("null", "Null"): - return "null" - return raw - - -def _extract_function_body(src: str, fn_name: str) -> str: - """Return the body of `pub(crate) fn (...)` (between matching braces), or ''.""" - for prefix in ("pub(crate) ", "pub(super) ", "pub ", ""): - marker = f"{prefix}fn {fn_name}(" - start = src.find(marker) - if start >= 0: - break - else: - return "" - brace = src.find("{", start) - if brace < 0: - return "" - depth = 0 - for i in range(brace, len(src)): - if src[i] == "{": - depth += 1 - elif src[i] == "}": - depth -= 1 - if depth == 0: - return src[brace : i + 1] - return "" - - -def _split_match_arms(body: str) -> list[str]: - """Split a `match` body into individual arms. +def run_gen_builtins(repo: Path) -> list[dict]: + """Return the registry as a list of dicts by invoking the `gen_builtins` binary. - Each arm ends either at a top-level `,` (direct-expression arm) or at the - closing `}` of an arm-block. The final closing `}` of the match itself - (which has no leading `,`) terminates the walk. + 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``. """ - arms: list[str] = [] - i = 0 - while i < len(body): - m = re.search(r'"([^"]+)"(?:\s*\|\s*"([^"]+)")*\s*=>\s*', body[i:]) - if not m: + cmd: list[str] + for profile in ("release", "debug"): + exe = repo / "target" / profile / "gen_builtins" + if exe.exists(): + cmd = [str(exe), "--include-internal"] break - arm_start = i + m.start() - rhs_start = i + m.end() - # If the arm body starts with `{` (after optional whitespace/newlines), - # the matching `}` ends the arm. - scan = rhs_start - while scan < len(body) and body[scan] in " \t\n\r": - scan += 1 - is_block = scan < len(body) and body[scan] == "{" - depth = 0 - j = rhs_start - in_str = False - ended_on = None # "," or "}" once we terminate - while j < len(body): - ch = body[j] - if ch == "\\" and j + 1 < len(body): - j += 2 - continue - if ch == '"': - in_str = not in_str - elif not in_str: - if ch in "([{": - depth += 1 - elif ch in ")]}": - if depth == 0: - # Closing `}` of the whole match (not inside any arm). - ended_on = "}" - break - depth -= 1 - # After decrement, depth may have hit 0: a block arm's - # closing `}` ends the arm here. - if is_block and depth == 0 and ch == "}": - ended_on = "}" - break - elif ch == "," and depth == 0: - ended_on = "," - j += 1 - break - j += 1 - # Include the closing `}` for block arms so `_parse_rhs` sees a - # balanced `{ ... }` (it relies on rhs.endswith("}")). - end = j + 1 if (is_block and ended_on == "}") else j - arm_text = body[arm_start:end].rstrip().rstrip(",").rstrip() - arms.append(arm_text) - if ended_on == "}" and not is_block: - # closing of the whole match - break - i = j - return arms - - -def parse_builtin_call_sigs(path: Path) -> dict[str, dict]: - """Return {name: {params, variadic, ref_params, required}} from signatures.rs::builtin_call_sig.""" - src = path.read_text(encoding="utf-8") - body = _extract_function_body(src, "builtin_call_sig") - if not body: - return {} - out: dict[str, dict] = {} - for arm in _split_match_arms(body): - # names: everything before " =>" - head, _, rhs = arm.partition("=>") - names = _split_name_list(head) - info = _parse_rhs(rhs) - for n in names: - out[n] = info - return out - - -def _parse_rhs(rhs: str) -> dict: - """Parse a single arm RHS — either a direct `Some(builder(...))` or a block with `let mut sig = ...`.""" - rhs = rhs.strip().rstrip(",") - # Strip leading/trailing braces if it's a block. - block: Optional[str] = None - if rhs.startswith("{") and rhs.endswith("}"): - block = rhs[1:-1] - # Case A: `let mut sig = ...` (custom builder with overrides). - m_let = re.search(r"let\s+mut\s+sig\s*=\s*([^;]+);", block) - if m_let: - builder_expr = m_let.group(1).strip() - ref_overrides = { - int(m.group(1)): m.group(2).strip() == "true" - for m in re.finditer(r"sig\.ref_params\[(\d+)\]\s*=\s*(true|false)", block) - } - else: - # Case B: block with a single `Some(builder(...))` expression. - m_some = re.search(r"Some\s*\(\s*(.+?)\s*\)\s*(?:;|$)", block, re.DOTALL) - if not m_some: - return {} - builder_expr = m_some.group(1).strip() - ref_overrides = { - int(m.group(1)): m.group(2).strip() == "true" - for m in re.finditer(r"sig\.ref_params\[(\d+)\]\s*=\s*(true|false)", block) - } else: - # form: Some(builder(args)) - m = re.match(r"Some\s*\(\s*(.+?)\s*\)\s*$", rhs, re.DOTALL) - if not m: - return {} - builder_expr = m.group(1).strip() - ref_overrides = {} - - return _parse_builder(builder_expr, ref_overrides) - - -def _parse_builder(expr: str, ref_overrides: dict[int, bool]) -> dict: - """Parse `fixed(&["a", "b"])` / `optional(...)` / `variadic(...)` / `first_param_ref(...)`. - - All regexes use `re.DOTALL` and tolerate whitespace between tokens, because - the Rust source often spans multiple lines and inserts newlines + indentation. - """ - # unwrap first_param_ref(...) if present - m = re.match(r"first_param_ref\s*\(\s*(.+?)\s*\)\s*$", expr, re.DOTALL) - if m: - inner = m.group(1).strip() - ref_overrides = {0: True, **ref_overrides} - return _parse_builder(inner, ref_overrides) - - m = re.match(r"fixed\s*\(\s*&\[(.*?)\]\s*\)", expr, re.DOTALL) - if m: - params = _parse_param_list(m.group(1)) - return { - "params": [{"name": p, "by_ref": ref_overrides.get(i, False), "default": None, "optional": False} for i, p in enumerate(params)], - "variadic": None, - "required": len(params), - } - m = re.match(r"optional\s*\(\s*&\[(.*?)\]\s*,\s*(\d+)\s*,\s*(.+?)\s*\)\s*$", expr, re.DOTALL) - if m: - params = _parse_param_list(m.group(1)) - required = int(m.group(2)) - defaults = _parse_optional_defaults(m.group(3)) - result = [] - for i, p in enumerate(params): - opt = i >= required - default = defaults.get(i - required) - result.append({"name": p, "by_ref": ref_overrides.get(i, False), "default": default, "optional": opt}) - return {"params": result, "variadic": None, "required": required} - m = re.match(r"variadic\s*\(\s*&\[(.*?)\]\s*,\s*\"([^\"]+)\"\s*\)\s*$", expr, re.DOTALL) - if m: - params = _parse_param_list(m.group(1)) - variadic = m.group(2) - result = [{"name": p, "by_ref": ref_overrides.get(i, False), "default": None, "optional": False} for i, p in enumerate(params)] - return {"params": result, "variadic": variadic, "required": len(params)} - return {"params": [], "variadic": None, "required": 0} - - -def parse_first_class_return_types(path: Path) -> dict[str, str]: - """Return {name: return_type_string} from signatures.rs::general_first_class_callable_builtin_sig.""" - src = path.read_text(encoding="utf-8") - out: dict[str, str] = {} - - # explicit FunctionSig blocks (strlen, count, buffer_len, ...) - for m in re.finditer( - r'"([^"]+)"\s*=>\s*Some\(FunctionSig\s*\{[^}]*?return_type:\s*(PhpType::[A-Za-z0-9_]+(?:::[A-Za-z0-9_]+)?(?:\([^)]*\))?)[^}]*?\}', - src, - re.DOTALL, - ): - out[m.group(1)] = _render_phptype(m.group(2)) - - # typed_first_class_builtin_sig(name, &[PhpType::X, ...], PhpType::Y) - for m in re.finditer( - r'typed_first_class_builtin_sig\(\s*name\s*,\s*&\[([^\]]*)\]\s*,\s*(PhpType::[A-Za-z0-9_]+(?:::[A-Za-z0-9_]+)?(?:\([^)]*\))?)\s*\)', - src, - ): - # we don't know the names here without re-parsing the match arm; resolve by walking backward to find the most recent arm header - # simpler: just use the previous match in src order - pass - - # General "name | "name" => Some(typed_first_class_builtin_sig(...))" pattern - arm_re = re.compile( - r'"([^"]+)"(?:\s*\|\s*"[^"]+")*\s*=>\s*Some\(typed_first_class_builtin_sig\(\s*name\s*,\s*&\[([^\]]*)\]\s*,\s*(PhpType::[A-Za-z0-9_]+(?:::[A-Za-z0-9_]+)?(?:\([^)]*\))?)\)\)', - ) - for m in arm_re.finditer(src): - for name in _split_name_list(m.group(0).split("=>", 1)[0]): - out[name] = _render_phptype(m.group(3)) - - # return_typed_first_class_builtin_sig(name, PhpType::X) - arm_re2 = re.compile( - r'"([^"]+)"(?:\s*\|\s*"[^"]+")*\s*=>\s*return_typed_first_class_builtin_sig\(\s*name\s*,\s*(PhpType::[A-Za-z0-9_]+(?:::[A-Za-z0-9_]+)?(?:\([^)]*\))?)\s*\)', - ) - for m in arm_re2.finditer(src): - for name in _split_name_list(m.group(0).split("=>", 1)[0]): - out[name] = _render_phptype(m.group(2)) - - return out - - -def _render_phptype(ty: str) -> str: - """Render a PhpType expression as a short user-facing string.""" - ty = ty.strip() - mapping = { - "PhpType::Int": "int", - "PhpType::Float": "float", - "PhpType::Bool": "bool", - "PhpType::Str": "string", - "PhpType::Void": "void", - "PhpType::Null": "null", - "PhpType::Mixed": "mixed", - "PhpType::Never": "never", - } - if ty in mapping: - return mapping[ty] - if ty.startswith("PhpType::Array"): - return "array" - if ty.startswith("PhpType::Buffer"): - return "buffer" - if ty.startswith("PhpType::Union"): - return "mixed" - if ty.startswith("PhpType::AssocArray"): - return "array" - return "mixed" + cmd = ["cargo", "run", "--quiet", "--bin", "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" + + proc.stderr + ) + try: + return json.loads(proc.stdout) + except json.JSONDecodeError as exc: # pragma: no cover - defensive + sys.exit(f"gen_builtins produced invalid JSON: {exc}") # --------------------------------------------------------------------------- -# lowering: builtins.rs and per-area submodules +# Home-file lowering map: name -> the emitter its `lower` hook dispatches to # --------------------------------------------------------------------------- -# Each dispatch arm in `lower_builtin_call` matches either a single name -# or a `|`-separated list. e.g. `"strlen" => lower_strlen(ctx, inst),` - -DISPATCH_ARM_RE = re.compile( - r'"([^"]+)"(?:\s*\|\s*"[^"]+")*\s*=>\s*([A-Za-z_][A-Za-z0-9_]*)::([A-Za-z_][A-Za-z0-9_]*|lower_[A-Za-z0-9_]+|lower_unary_libm)\(', -) - - -def parse_lowering_dispatch(path: Path) -> dict[str, tuple[str, str, int]]: - """Return {name: (module, codegen_function, line)} for arms in builtins.rs - that dispatch a *single* builtin name to a dedicated lowering function. - - Multi-name arms (e.g. catch-all dispatchers that handle a whole family) are - skipped, so the per-name entry wins when both exist. +# Core registry-machinery files under src/builtins/ that are NOT builtin homes. +_NON_HOME_FILES = { + "spec.rs", + "registry.rs", + "macros.rs", + "convert.rs", + "docs.rs", + "mod.rs", + "parity_tests.rs", +} + +_NAME_RE = re.compile(r'name:\s*"([^"]+)"') +# The `lower` hook dispatches to the real emitter via a fully-qualified path, +# e.g. `crate::codegen::lower_inst::builtins::math::lower_abs(ctx, inst)` +# (the `(ctx` may be on the following line — `\s*` spans newlines). +_EMITTER_RE = re.compile(r"lower_inst::builtins::([A-Za-z0-9_:]+)\s*\(\s*ctx\b") + + +def build_home_lowering_map(repo: Path) -> dict[str, tuple[str, str, str]]: + """Map each registry builtin name (lowercased) to ``(emitter_fn, module, home_rel)``. + + Scans every builtin home file under ``src/builtins/`` (skipping the registry + machinery files), reads its ``builtin!`` name and the emitter path its + ``lower`` hook dispatches to. ``module`` is the last path segment before the + emitter function (used for the AREA_BY_MODULE area fallback); ``home_rel`` is + the home file path relative to the repo root. """ - src_lines = path.read_text(encoding="utf-8").splitlines() - out: dict[str, tuple[str, str, int]] = {} - for lineno, line in enumerate(src_lines, start=1): - m = DISPATCH_ARM_RE.search(line) - if not m: + out: dict[str, tuple[str, str, str]] = {} + builtins_root = repo / "src" / "builtins" + for path in builtins_root.rglob("*.rs"): + if path.name in _NON_HOME_FILES: + continue + text = path.read_text(encoding="utf-8") + if "builtin!" not in text: continue - names_in_arm = re.findall(r'"([^"]+)"', line.split("=>", 1)[0]) - if len(names_in_arm) != 1: + name_match = _NAME_RE.search(text) + if not name_match: continue - n = names_in_arm[0] - out[n] = (m.group(2), m.group(3), lineno) + canonical = name_match.group(1).lower() + emitter_fn = "" + module = "" + emit_match = _EMITTER_RE.search(text) + if emit_match: + segments = emit_match.group(1).split("::") + emitter_fn = segments[-1] + module = segments[-2] if len(segments) >= 2 else "" + out[canonical] = (emitter_fn, module, str(path.relative_to(repo))) return out +# --------------------------------------------------------------------------- +# Emitter resolution: find the emitter fn definition, its doc + runtime helpers +# --------------------------------------------------------------------------- + DOC_COMMENT_RE = re.compile(r"^///\s?(.*)$") +def find_lowering_function_def(src: str, fn_name: str) -> Optional[tuple[str, int]]: + """Find the (line_text, line_number) of ``fn (`` in ``src``.""" + lines = src.splitlines() + for i, line in enumerate(lines, start=1): + if re.match(rf"\s*(pub(?:\([^)]*\))?\s+)?fn\s+{re.escape(fn_name)}\s*\(", line): + return (line, i) + return None + + def _leading_doc_comment(src: str, line: int) -> str: - """Return the /// doc comment block immediately above a function definition at `line`.""" + """Return the ``///`` doc-comment block immediately above the function at ``line``.""" lines = src.splitlines() - i = line - 2 # 1-based + i = line - 2 # 1-based → index above the def out: list[str] = [] while i >= 0 and lines[i].lstrip().startswith("///"): - m = DOC_COMMENT_RE.match(lines[i]) + m = DOC_COMMENT_RE.match(lines[i].lstrip()) if m: out.append(m.group(1).strip()) i -= 1 @@ -469,19 +170,8 @@ def _leading_doc_comment(src: str, line: int) -> str: return "\n".join(out) -def find_lowering_function_def( - src: str, fn_name: str -) -> Optional[tuple[str, int]]: - """Find the (path, line) of `fn (` in `src`.""" - lines = src.splitlines() - for i, line in enumerate(lines, start=1): - if re.match(rf"\s*(pub(?:\([^)]*\))?\s+)?fn\s+{re.escape(fn_name)}\s*\(", line): - return (line, i) - return None - - def collect_runtime_helpers(notes: str, body: str) -> list[str]: - """Find `__rt_*` symbols in the doc comment and the lowering body.""" + """Return the sorted set of ``__rt_*`` symbols mentioned in the doc + lowering body.""" found = set(re.findall(r"\b__rt_[A-Za-z0-9_]+", notes)) | set( re.findall(r"\b__rt_[A-Za-z0-9_]+", body) ) @@ -489,195 +179,292 @@ def collect_runtime_helpers(notes: str, body: str) -> list[str]: def parse_area_for_file(rel_path: str) -> tuple[Optional[str], str]: - """Look up the (area, sub_area) for a given relative file path. + """Look up the ``(area, sub_area)`` for a lowering file path under ``builtins/``. - Returns (None, "") as a sentinel when the file is the root dispatcher - (builtins/builtins.rs) and the area should be inferred from the - dispatch module/function instead. + Returns ``(None, "")`` as a sentinel when the file is the root dispatcher and + the area should be inferred from the module/function instead. """ key = rel_path.replace("builtins/", "").replace("builtins\\", "") if key in AREA_BY_FILE: val = AREA_BY_FILE[key] - if val is None: - return (None, "") # sentinel - return val - # try the basename if no submodule match + return (None, "") if val is None else val base = Path(key).name if base in AREA_BY_FILE: val = AREA_BY_FILE[base] - if val is None: - return (None, "") - return val + return (None, "") if val is None else val return ("Misc", "Misc") -def parse_check_builtin_returns(path: Path) -> dict[str, str]: - """Return {name: return_type} extracted from check_builtin() in a checker file. +def resolve_lowering( + repo: Path, + read, + dispatch: Path, + lowering_dir: Path, + emitter_fn: str, + sig_file: Optional[str], +) -> LoweringInfo: + """Resolve an emitter function name to its definition, doc notes, and helpers. + + Searches ``builtins.rs`` (root dispatcher) and every per-area submodule for + ``fn (``. Returns a populated :class:`LoweringInfo` (with + ``codegen_file``/``codegen_line``/``notes``/``runtime_helpers``) when found, or + a bare one carrying only ``sig_file`` when not. + """ + lowering = LoweringInfo(sig_file=sig_file) + if not emitter_fn: + return lowering + for candidate in [dispatch, *sorted(lowering_dir.rglob("*.rs"))]: + src_text = read(candidate) + defn = find_lowering_function_def(src_text, emitter_fn) + if defn is None: + continue + _, def_line = defn + doc = _leading_doc_comment(src_text, def_line) + body = "\n".join(src_text.splitlines()[def_line - 1 : def_line + 30]) + helpers = collect_runtime_helpers(doc, body) + notes = [line for line in doc.splitlines() if line.strip()] + return LoweringInfo( + sig_file=sig_file, + codegen_file=str(candidate.relative_to(repo)), + codegen_line=def_line, + codegen_function=emitter_fn, + runtime_helpers=helpers, + notes=notes, + ) + return lowering + - We scan the body line-by-line, tracking: - - the set of names that make up the current arm (multi-name and multi-line - headers are handled by accumulating names until we see `=>` or a `}`), - - and the return type as the *last* `Ok(Some(PhpType::))` we observe - before the arm closes. +def resolve_area( + canonical: str, lowering: LoweringInfo, emitter_fn: str, module: str +) -> tuple[str, str]: + """Resolve a builtin's documentation ``(area, sub_area)``. - Conditional arms like `min`/`max` keep the *last* such return, which is the - more specific (Float) branch. + Priority (most specific first): per-name override → the lowering file's path → + the generic libm/lowering-fn mapping → the dispatch module → ``Misc``. """ - text = path.read_text(encoding="utf-8") - out: dict[str, str] = {} - re_phptype = re.compile( - r"Ok\s*\(\s*Some\s*\(\s*(PhpType::[A-Za-z0-9_]+(?:::[A-Za-z0-9_]+)?(?:\([^)]*\))?)\s*\)\s*\)" - ) - re_name = re.compile(r'"([^"]+)"') - in_match = False - current_names: list[str] = [] - pending_names: list[str] = [] # names found on a line that didn't have `=>` yet - arm_indent: int | None = None - for line in text.splitlines(): - # Skip Rust string literals when scanning for tokens. - # We do this by re-tokenising the line on quotes. - if not in_match: - if "match name" in line or re.search(r"match\s+\w+\s*\{", line): - in_match = True - continue - # Detect a "names" line: a line with one or more `"name"` segments. - names = re_name.findall(line) - if "=>" in line: - # commit pending names + this line's names - current_names = list(dict.fromkeys(current_names + pending_names + names)) - pending_names = [] - arm_indent = len(line) - len(line.lstrip()) - continue - if names: - pending_names.extend(names) - # Look for the return type on any line in the arm - if current_names: - m = re_phptype.search(line) - if m: - rt = _render_phptype(m.group(1)) - for n in current_names: - out[n] = rt - current_names = [] - # arm ends at a `}` at the same indent as the header - if arm_indent is not None and line.strip() == "}": - cur_indent = len(line) - len(line.lstrip()) - if cur_indent == arm_indent: - current_names = [] - pending_names = [] - arm_indent = None - return out + area = AREA_BY_NAME.get(canonical, ("Misc", "Misc")) + if area == ("Misc", "Misc") and lowering.codegen_file: + cf = lowering.codegen_file + prefix = "src/codegen/lower_inst/builtins" + rel_under = cf[len(prefix) + 1 :] if cf.startswith(prefix + "/") else cf + file_area = parse_area_for_file(rel_under) + if file_area[0] is not None and (file_area[0] != "Misc" or file_area[1] != "Misc"): + area = file_area + if area == ("Misc", "Misc"): + fn_area = AREA_BY_LOWERING_FN.get(emitter_fn) if emitter_fn else None + if fn_area is not None: + area = fn_area + elif module: + mod_area = AREA_BY_MODULE.get(module) + if mod_area is not None: + area = mod_area + return area -def parse_check_builtin_param_types(path: Path) -> dict[str, list[tuple[int, str]]]: - """Return {name: [(arg_index, type_str), ...]} extracted from check_builtin(). +# --------------------------------------------------------------------------- +# Type + default rendering (registry data → doc vocabulary) +# --------------------------------------------------------------------------- - We scan the arm body and watch for the pattern: - let = checker.infer_type(&args[N], env)?; - if !matches!(, PhpType::X | PhpType::Y) { ... } # X|Y - if != PhpType::X { ... } # X - match { PhpType::X => ..., _ => Err } # X +def _normalize_type(reg_type: str) -> str: + """Map a registry type string to the doc's simple type vocabulary. - For each such pair we record (N, type). For the common "must be array" / - "must be string" / "must be int" patterns this gives us precise param - types. The renderer picks the *first* declared type per arg index. + The registry renders `TypeSpec::ArrayOf`/`AssocOf` as ``array<...>`` and + unions as ``a|b``; the docs collapse those to ``array`` / ``mixed``. Scalars + (``int``/``float``/``string``/``bool``/``mixed``/``null``/``void``) pass through. """ - text = path.read_text(encoding="utf-8") - out: dict[str, list[tuple[int, str]]] = {} - in_match = False - current_names: list[str] = [] - pending_names: list[str] = [] - arm_indent: int | None = None - # Map from inferred-var name -> (arg_index, type_str) — collected while - # scanning the current arm. - inferred: dict[str, tuple[int, str]] = {} - for line in text.splitlines(): - if not in_match: - if "match name" in line or re.search(r"match\s+\w+\s*\{", line): - in_match = True - continue - names = re.findall(r'"([^"]+)"', line) - if "=>" in line: - current_names = list(dict.fromkeys(current_names + pending_names + names)) - pending_names = [] - arm_indent = len(line) - len(line.lstrip()) - inferred = {} # reset for new arm - continue - if names: - pending_names.extend(names) - # Pattern 1: `let = checker.infer_type(&args[N], env)?;` - m_infer = re.search( - r"let\s+(\w+)\s*=\s*checker\.infer_type\(&args\[(\d+)\]\s*,\s*env\)?\s*;?", - line, - ) - if m_infer: - var_name = m_infer.group(1) - arg_idx = int(m_infer.group(2)) - # If we already saw a constraint on this var, use it now. - if var_name in inferred: - idx, ty = inferred[var_name] - inferred[var_name] = (idx, ty) - else: - inferred[var_name] = (arg_idx, "mixed") - continue - # Pattern 2: `if !matches!(, PhpType::X | PhpType::Y) {` - m_match = re.search( - r"if\s+!matches!\(\s*(\w+)\s*,\s*(PhpType::[A-Za-z0-9_]+(?:\([^)]*\))?(?:\s*\|\s*PhpType::[A-Za-z0-9_]+(?:\([^)]*\))?)*)\s*\)", - line, - ) - if m_match: - var_name = m_match.group(1) - type_pattern = m_match.group(2) - # Take the FIRST PhpType from the alternation. - first = re.match(r"(PhpType::[A-Za-z0-9_]+)", type_pattern) - if first and var_name in inferred: - idx, _ = inferred[var_name] - inferred[var_name] = (idx, _render_phptype(first.group(1))) - continue - # Pattern 3: `if != PhpType::X {` - m_neq = re.search( - r"if\s+(\w+)\s*!=\s*(PhpType::[A-Za-z0-9_]+(?:\([^)]*\))?)\s*\{", - line, - ) - if m_neq: - var_name = m_neq.group(1) - type_str = _render_phptype(m_neq.group(2)) - if var_name in inferred: - idx, _ = inferred[var_name] - inferred[var_name] = (idx, type_str) - continue - # Pattern 4: `match { PhpType::X => ..., _ => Err }` — the first - # match arm header gives the type. We look for lines that start with - # `PhpType::X =>` and pair them with the var that was just matched. - m_match_arm = re.match( - r"^\s*(PhpType::[A-Za-z0-9_]+(?:\([^)]*\))?)\s*=>", - line, - ) - if m_match_arm and inferred: - # We can't easily tell which var this `match` belongs to without - # a brace counter. Cheap heuristic: take the most recently assigned - # inferred var. The patterns above usually appear right after - # `let = ...`. - type_str = _render_phptype(m_match_arm.group(1)) - for var, (idx, _) in list(inferred.items()): - if inferred[var][1] == "mixed": - inferred[var] = (idx, type_str) - break - # When the arm closes, commit the inferred types to the current names. - if arm_indent is not None and line.strip() == "}": - cur_indent = len(line) - len(line.lstrip()) - if cur_indent == arm_indent and current_names: - # Group by arg index, keep the first non-mixed type per index. - by_idx: dict[int, str] = {} - for _, (idx, ty) in inferred.items(): - if idx not in by_idx or ty != "mixed": - by_idx[idx] = ty - for n in current_names: - out.setdefault(n, []).extend([(i, t) for i, t in sorted(by_idx.items())]) - current_names = [] - pending_names = [] - inferred = {} - arm_indent = None - return out + reg_type = reg_type.strip() + if "|" in reg_type: + return "mixed" + if reg_type.startswith("array"): + return "array" + return reg_type + + +def _param_refine_type(entry) -> Optional[str]: + """Extract the display type from a `PARAM_TYPES` entry (``str`` or ``(type, name)``).""" + if entry is None: + return None + if isinstance(entry, str): + return entry or None + ty = entry[0] + return ty or None + + +# Maps a Rust `PhpType::` to the doc's display type. +_PHPTYPE_DISPLAY = { + "Str": "string", + "Int": "int", + "Bool": "bool", + "Float": "float", + "Void": "void", + "Null": "null", + "Mixed": "mixed", + "Never": "never", + "Array": "array", + "AssocArray": "array", + "Union": "mixed", + "Buffer": "buffer", +} + + +def _extract_fn_body(text: str, fn_name: str) -> str: + """Return the brace-matched body of ``fn (`` in ``text`` (or '').""" + for prefix in ("pub(crate) ", "pub(super) ", "pub ", ""): + start = text.find(f"{prefix}fn {fn_name}(") + if start >= 0: + break + else: + return "" + brace = text.find("{", start) + if brace < 0: + return "" + depth = 0 + for i in range(brace, len(text)): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + return text[brace : i + 1] + return "" + + +def parse_home_check_return(home_text: str, resolve_body) -> Optional[str]: + """Recover a precise return type from a home file's ``check`` hook, or ``None``. + + The registry types non-scalar returns coarsely as ``Mixed`` (arrays are + declared ``Mixed`` + a check hook that returns the precise type). We locate the + hook's body — a local ``fn check`` or, when ``check:`` points to a distinctively + named shared fn (e.g. ``support::check_declared_names``), that fn resolved via + ``resolve_body`` — then scan its ``Ok(PhpType::)`` returns. When they + agree on a single non-``mixed`` display type (or an array type dominates), we + return it; otherwise ``None``. + """ + m = re.search(r"\bcheck:\s*([A-Za-z0-9_:]+)", home_text) + if not m: + return None + target = m.group(1) + fn_name = target.split("::")[-1] + if "::" in target and fn_name != "check": + body = resolve_body(fn_name) or _extract_fn_body(home_text, fn_name) + else: + body = _extract_fn_body(home_text, "check") + if not body: + return None + variants = re.findall(r"Ok\(\s*PhpType::([A-Za-z0-9_]+)", body) + displays = {_PHPTYPE_DISPLAY.get(v, "mixed") for v in variants} + # Array-passthrough pattern: the hook validates the argument is an array and + # returns it unchanged (`Ok(ty)`), so the literal PhpType is never written. + if re.search(r"Ok\(\s*[a-z_]\w*\s*\)", body) and "PhpType::Array" in body: + displays.add("array") + non_mixed = displays - {"mixed"} + if len(non_mixed) == 1: + return next(iter(non_mixed)) + if "array" in non_mixed: + return "array" + return None + + +def _render_default(value, optional: bool) -> Optional[str]: + """Render a registry default value as a PHP-literal display string. + + Required params (``optional`` false) have no default (``None``). Optional + params render their default: ``null``, ``true``/``false``, integers/floats + verbatim, strings single-quoted, the ``PHP_INT_MAX``/``PHP_INT_MIN`` sentinels + as constants, and the empty-array sentinel as ``[]``. + """ + if not optional: + return None + if value is None: + return "null" + # bool must precede int: bool is a subclass of int in Python. + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, list): + return "[]" + if isinstance(value, str): + if value in ("PHP_INT_MAX", "PHP_INT_MIN"): + return value + return repr(value) + return str(value) + + +# --------------------------------------------------------------------------- +# PHP language constructs (checker-resident, NOT in the registry) +# --------------------------------------------------------------------------- + +# These stay in the type checker (they operate on l-values / are lazy constructs) +# and are absent from the `builtin!` registry. We add them by hand so their doc +# pages are preserved. Each: params [(name, type, by_ref, default, optional)], +# variadic, return_type, (area, sub_area), description, emitter_fn (or None). +LANGUAGE_CONSTRUCTS: dict[str, dict] = { + "isset": { + "params": [("var", "mixed", False, None, False)], + "variadic": "vars", + "return_type": "bool", + "area": ("Misc", "Variable"), + "description": "Determines whether a variable is set and is not null.", + "emitter_fn": "lower_isset", + }, + "unset": { + "params": [("var", "mixed", False, None, False)], + "variadic": "vars", + "return_type": "void", + "area": ("Misc", "Variable"), + "description": "Unsets the given variables.", + "emitter_fn": "lower_unset_builtin", + }, + "empty": { + "params": [("value", "mixed", False, None, False)], + "variadic": None, + "return_type": "bool", + "area": ("Misc", "Variable"), + "description": "Determines whether a variable is considered empty.", + "emitter_fn": "lower_empty", + }, + "exit": { + "params": [("status", "int", False, None, True)], + "variadic": None, + "return_type": "void", + "area": ("Process", "Process"), + "description": "", + "emitter_fn": None, + }, + "die": { + "params": [("status", "int", False, None, True)], + "variadic": None, + "return_type": "void", + "area": ("Process", "Process"), + "description": "", + "emitter_fn": None, + }, + "buffer_len": { + "params": [("buffer", "buffer", False, None, False)], + "variadic": None, + "return_type": "int", + "area": ("Buffer", "Buffer"), + "description": "Lowers `buffer_len()` through the direct buffer opcode helper.", + "emitter_fn": "lower_buffer_len", + }, + "buffer_free": { + "params": [("buffer", "buffer", False, None, False)], + "variadic": None, + "return_type": "mixed", + "area": ("Buffer", "Buffer"), + "description": "Lowers `buffer_free()` through the direct buffer opcode helper.", + "emitter_fn": "lower_buffer_free", + }, + "buffer_new": { + "params": [("length", "int", False, None, False)], + "variadic": None, + "return_type": "mixed", + "area": ("Misc", "Misc"), + "description": "", + "emitter_fn": None, + }, +} # --------------------------------------------------------------------------- @@ -685,29 +472,14 @@ def parse_check_builtin_param_types(path: Path) -> dict[str, list[tuple[int, str # --------------------------------------------------------------------------- def build_registry(repo: Path) -> list[Builtin]: - """Build the full list of builtins from the given repo root.""" + """Build the full list of builtins from the registry + language constructs.""" src = repo / "src" + dispatch = src / "codegen" / "lower_inst" / "builtins.rs" + lowering_dir = src / "codegen" / "lower_inst" / "builtins" + + gen = run_gen_builtins(repo) + home_map = build_home_lowering_map(repo) - catalog = src / "types" / "checker" / "builtins" / "catalog.rs" - sigs = src / "types" / "signatures.rs" - dispatch = src / "codegen_ir" / "lower_inst" / "builtins.rs" - lowering_dir = src / "codegen_ir" / "lower_inst" / "builtins" - - if not catalog.exists(): - sys.exit(f"catalog.rs not found: {catalog}") - if not sigs.exists(): - sys.exit(f"signatures.rs not found: {sigs}") - if not dispatch.exists(): - sys.exit(f"builtins.rs not found: {dispatch}") - - supported, internal = parse_catalog(catalog) - internal_names = {name.lower() for name in internal} - catalog_names = list(dict.fromkeys([*supported, *internal])) - call_sigs = parse_builtin_call_sigs(sigs) - first_class_returns = parse_first_class_return_types(sigs) - dispatch_map = parse_lowering_dispatch(dispatch) - - # Cache file contents to avoid re-reading. file_cache: dict[Path, str] = {} def read(p: Path) -> str: @@ -715,269 +487,129 @@ def read(p: Path) -> str: file_cache[p] = p.read_text(encoding="utf-8") return file_cache[p] + builtins_root = src / "builtins" + + def resolve_check_body(fn_name: str) -> str: + """Return the body of a shared check fn ``fn (`` defined under src/builtins/.""" + for path in sorted(builtins_root.rglob("*.rs")): + text = read(path) + if f"fn {fn_name}(" in text: + body = _extract_fn_body(text, fn_name) + if body: + return body + return "" + builtins: list[Builtin] = [] - # Pre-parse checker return/param types once. Re-parsing every checker file - # inside the per-builtin loop is O(N*M); doing it once keeps extraction fast. - checker_dir = src / "types" / "checker" / "builtins" - check_builtin_returns: dict[str, str] = {} - check_builtin_params: dict[str, list[tuple[int, str]]] = {} - for cb_path in checker_dir.rglob("*.rs"): - for n, rt in parse_check_builtin_returns(cb_path).items(): - check_builtin_returns[n] = rt - for n, inferred_params in parse_check_builtin_param_types(cb_path).items(): - check_builtin_params.setdefault(n, []).extend(inferred_params) - - for name in catalog_names: + # --- registry builtins (PHP-visible + internal helpers) --- + for entry in gen: + name = entry["name"] canonical = name.lower() - in_catalog = canonical not in internal_names - # Any function whose canonical name starts with __elephc_ is a compiler - # internal helper; it gets an internals page but no user-facing page. - is_internal = canonical.startswith("__elephc_") or canonical in internal_names - - # Fallback: if signatures.rs didn't yield a result, use the call_sigs - # entry directly (may be empty) so the builtin still renders. - sig_info = call_sigs.get(canonical) - if sig_info is None: - # No match at all in signatures.rs — use an empty stub. - sig_info = {"params": [], "variadic": None, "required": 0} - params = [ - Parameter( - name=p["name"], - php_type="mixed", - by_ref=p["by_ref"], - default=p["default"], - optional=p["optional"], + is_internal = bool(entry.get("internal")) + in_catalog = not is_internal + + refine = PARAM_TYPES.get(canonical) + params: list[Parameter] = [] + for i, p in enumerate(entry.get("params", [])): + php_type = _normalize_type(p["type"]) + if php_type == "mixed" and refine and i < len(refine): + better = _param_refine_type(refine[i]) + if better: + php_type = better + params.append( + Parameter( + name=p["name"], + php_type=php_type, + 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 sig_info.get("params", []) - ] - return_type = first_class_returns.get(canonical, "mixed") - # Second-pass return type from check_builtin() in src/types/checker/builtins/*.rs. - # This gives precise types for things like `floor -> float`, `is_array -> bool`, etc. - # It overrides the first_class_returns value when more specific. - if canonical in check_builtin_returns and check_builtin_returns[canonical] != "mixed": - return_type = check_builtin_returns[canonical] - # Apply hand-curated return-type and variadic overrides. These win - # over anything parsed from check_builtin() / signatures.rs. + + emitter_fn, module, home_rel = home_map.get(canonical, ("", "", None)) + + return_type = _normalize_type(entry.get("returns", "mixed")) + # The registry types non-scalar returns as `Mixed`; recover the precise + # type from the home file's `check` hook when possible. + if return_type == "mixed" and home_rel: + precise = parse_home_check_return(read(repo / home_rel), resolve_check_body) + if precise: + return_type = precise if canonical in RETURN_TYPE_OVERRIDES: return_type = RETURN_TYPE_OVERRIDES[canonical] - if canonical in VARIADIC_OVERRIDES: - # Force variadic even when signatures.rs says fixed. - # Re-shape params: drop the fixed stub, switch to variadic. - vname = VARIADIC_OVERRIDES[canonical] - params = [] - sig_info["variadic"] = vname - sig_info.pop("__stub", None) - # Apply PARAM_TYPES to refine parameter names/types. For each known - # (arg_index, type) pair, override the parameter's `php_type` when it - # is currently `mixed` and the inferred type is more specific. - if canonical in PARAM_TYPES: - table = PARAM_TYPES[canonical] - # Normalize entries: accept either `str` or `(type, name)` tuple. - norm: list[tuple[str, str]] = [] - for entry in table: - if entry is None: - continue - if isinstance(entry, str): - norm.append((entry or "mixed", "value")) - else: - # tuple (type, name) - ty, entry_name = entry - norm.append((ty or "mixed", entry_name or "value")) - - # Apply name overrides on top of PARAM_TYPES. - name_overrides = PARAM_NAME_OVERRIDES.get(canonical, []) - for idx, override_name in enumerate(name_overrides): - if override_name is not None and idx < len(norm): - norm[idx] = (norm[idx][0], override_name) - - # Case A: signatures.rs gave us nothing — materialize all params - # from PARAM_TYPES (skip when signatures.rs says variadic, the - # variadic info is the authoritative shape). - if not params and not sig_info.get("variadic"): - for ty, entry_name in norm: - params.append(Parameter(name=entry_name, php_type=ty)) - else: - is_single_stub = ( - len(params) == 1 and params[0].name == "…" - ) - is_named = all(p.name not in (None, "…") for p in params) - if is_single_stub or is_named: - # Refine existing params in place; PARAM_TYPES can - # override the name when it's a tuple. - target_count = len(params) if is_named else len(norm) - for idx, (ty, entry_name) in enumerate(norm[:target_count]): - if 0 <= idx < len(params) and ty and params[idx].php_type == "mixed": - new_name = params[idx].name if is_named else entry_name - if is_named and entry_name != "value": - new_name = entry_name # explicit override - params[idx] = Parameter( - name=new_name, - php_type=ty, - by_ref=params[idx].by_ref, - default=params[idx].default, - optional=params[idx].optional, - ) - # Append any extra params that signatures.rs missed (e.g. - # `preg_match_all` declares 2 params in signatures.rs but - # PHP takes a 3rd `&$matches` array). Skip when variadic. - if not sig_info.get("variadic") and len(norm) > len(params): - for ty, entry_name in norm[len(params):]: - params.append(Parameter(name=entry_name, php_type=ty)) - # Apply by-reference overrides after PARAM_TYPES/checker refinement. - if canonical in REF_PARAM_OVERRIDES: - for idx, by_ref in enumerate(REF_PARAM_OVERRIDES[canonical]): - if 0 <= idx < len(params): - params[idx] = Parameter( - name=params[idx].name, - php_type=params[idx].php_type, - by_ref=by_ref, - default=params[idx].default, - optional=params[idx].optional, - ) - # Apply optional/default overrides. - if canonical in OPTIONAL_PARAM_OVERRIDES: - for idx, default in enumerate(OPTIONAL_PARAM_OVERRIDES[canonical]): - if default is not None and 0 <= idx < len(params): - params[idx] = Parameter( - name=params[idx].name, - php_type=params[idx].php_type, - by_ref=params[idx].by_ref, - default=default, - optional=True, - ) - # Then refine further with what the check_builtin() arms tell us. - # We only apply this when the entry is unambiguous (one type per - # arg_index, and the inferred type is not `mixed`). - if canonical in check_builtin_params and any( - p.name not in (None, "…") for p in params - ): - # Group by arg index, keep the first non-mixed type per index. - by_idx: dict[int, str] = {} - for idx, ty in check_builtin_params[canonical]: - if ty == "mixed": - continue - if idx not in by_idx: - by_idx[idx] = ty - for idx, ty in by_idx.items(): - if 0 <= idx < len(params) and params[idx].php_type == "mixed": - params[idx] = Parameter( - name=params[idx].name, - php_type=ty, - by_ref=params[idx].by_ref, - default=params[idx].default, - optional=params[idx].optional, - ) - # If still no signature at all (signatures.rs had no entry AND - # check_builtin() didn't yield anything useful), mark the builtin - # with a TODO stub so the renderer can show a placeholder. - # Skip when signatures.rs already gave us info (even an empty list - # means "the builtin takes no arguments", which is a real signature). - if ( - not params - and not sig_info.get("variadic") - and canonical not in call_sigs - ): - params = [Parameter(name="…", php_type="mixed", optional=False)] - sig_info["__stub"] = True - - lowering = LoweringInfo( - sig_file=str(sigs.relative_to(repo)), - sig_line=None, - sig_arm=None, + lowering = resolve_lowering( + repo, read, dispatch, lowering_dir, emitter_fn, home_rel ) - fn_name = "" - if canonical in dispatch_map: - module, fn_name, line = dispatch_map[canonical] - else: - # No dedicated dispatch arm. Try a heuristic: if the builtin name - # matches a `lower_` function defined in builtins.rs root, use it. - guessed_fn = f"lower_{canonical}" - guessed = find_lowering_function_def(read(dispatch), guessed_fn) - if guessed is not None: - module, fn_name, line = "", guessed_fn, guessed[1] - else: - module, fn_name, line = "", "", 0 - if fn_name: - # search in builtins.rs first, then submodules - for candidate in [dispatch, *sorted(lowering_dir.rglob("*.rs"))]: - src_text = read(candidate) - defn = find_lowering_function_def(src_text, fn_name) - if defn is None: - continue - _, def_line = defn - doc = _leading_doc_comment(src_text, def_line) - # pull the next 30 lines to look for runtime helpers - body = "\n".join(src_text.splitlines()[def_line - 1 : def_line + 30]) - helpers = collect_runtime_helpers(doc, body) - notes = [l for l in doc.splitlines() if l.strip()] - lowering = LoweringInfo( - codegen_file=str(candidate.relative_to(repo)), - codegen_line=def_line, - codegen_function=fn_name, - runtime_helpers=helpers, - notes=notes, - ) - break + description = DESCRIPTION_OVERRIDES.get(canonical, "") + if not description: + description = entry.get("summary", "") or "" + if not description and lowering.notes: + description = lowering.notes[0] - # Attach hand-curated notes for compiler-internal helpers. if is_internal and canonical in INTERNAL_NOTES: lowering.notes = INTERNAL_NOTES[canonical] - # Area resolution priority (most specific first): - # 1. AREA_BY_NAME — hand-curated per-name overrides (e.g. sin→Math). - # 2. Lowering-fn location (the file the lowering lives in). - # 3. AREA_BY_LOWERING_FN — generic libm dispatcher mapping. - # 4. AREA_BY_MODULE — based on the dispatch arm's module prefix. - # 5. Misc (last resort). - area = AREA_BY_NAME.get(canonical, ("Misc", "Misc")) - if area == ("Misc", "Misc") and lowering.codegen_file: - cf = lowering.codegen_file - prefix = "src/codegen_ir/lower_inst/builtins" - if cf.startswith(prefix + "/"): - rel_under = cf[len(prefix) + 1:] - else: - rel_under = cf # legacy safety - file_area = parse_area_for_file(rel_under) - if file_area[0] is not None and (file_area[0] != "Misc" or file_area[1] != "Misc"): - area = file_area - if area == ("Misc", "Misc"): - fn_area = AREA_BY_LOWERING_FN.get(fn_name) if fn_name else None - if fn_area is not None: - area = fn_area - elif module: - mod_area = AREA_BY_MODULE.get(module) - if mod_area is not None: - area = mod_area - - # Derive a one-line description from hand-curated overrides or from - # the first line of the lowering function's doc comment. - description = DESCRIPTION_OVERRIDES.get(canonical, "") - if not description and lowering.notes: - description = lowering.notes[0] + area = resolve_area(canonical, lowering, emitter_fn, module) + + builtins.append( + Builtin( + name=name, + canonical_name=canonical, + in_catalog=in_catalog, + is_internal=is_internal, + area=area[0], + sub_area=area[1], + sig=BuiltinSig( + params=params, + variadic=entry.get("variadic"), + return_type=return_type, + ), + lowering=lowering, + description=description, + ) + ) - b = Builtin( - name=name, - canonical_name=canonical, - in_catalog=in_catalog, - is_internal=is_internal, - area=area[0], - sub_area=area[1], - sig=BuiltinSig( - params=params, - variadic=sig_info.get("variadic"), - return_type=return_type, - ), - lowering=lowering, - description=description, + # --- language constructs (checker-resident, hand-curated) --- + for canonical, spec in LANGUAGE_CONSTRUCTS.items(): + params = [ + Parameter( + name=pname, + php_type=ptype, + by_ref=by_ref, + default=default, + optional=optional, + ) + for (pname, ptype, by_ref, default, optional) in spec["params"] + ] + emitter_fn = spec.get("emitter_fn") or "" + lowering = resolve_lowering(repo, read, dispatch, lowering_dir, emitter_fn, None) + description = DESCRIPTION_OVERRIDES.get(canonical, spec.get("description", "")) + builtins.append( + Builtin( + name=canonical, + canonical_name=canonical, + in_catalog=True, + is_internal=False, + area=spec["area"][0], + sub_area=spec["area"][1], + sig=BuiltinSig( + params=params, + variadic=spec.get("variadic"), + return_type=spec["return_type"], + ), + lowering=lowering, + description=description, + ) ) - builtins.append(b) + # Deterministic order for reproducible JSON. + builtins.sort(key=lambda b: b.canonical_name) return builtins def main_with(repo_root: Path, out: Path) -> int: + """Build the registry from ``repo_root`` and write the JSON registry to ``out``.""" builtins = build_registry(repo_root) out.parent.mkdir(parents=True, exist_ok=True) out.write_text( @@ -989,6 +621,7 @@ def main_with(repo_root: Path, out: Path) -> int: def main() -> int: + """CLI entry point: parse the registry and write ``builtin_registry.json``.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[3]) parser.add_argument("--out", type=Path, default=None) @@ -999,6 +632,7 @@ def main() -> int: def _builtin_to_dict(b: Builtin) -> dict: + """Serialize a :class:`Builtin` to the JSON schema consumed by the renderer.""" return { "name": b.name, "canonical_name": b.canonical_name, @@ -1039,5 +673,3 @@ def _builtin_to_dict(b: Builtin) -> dict: if __name__ == "__main__": sys.exit(main()) - - diff --git a/scripts/docs/elephc_builtins/registry.py b/scripts/docs/elephc_builtins/registry.py index fe3ca78e6c..d95e11156c 100644 --- a/scripts/docs/elephc_builtins/registry.py +++ b/scripts/docs/elephc_builtins/registry.py @@ -35,7 +35,7 @@ ] -# Sub-area mapping: file path under src/codegen_ir/lower_inst/builtins/ or +# Sub-area mapping: file path under src/codegen/lower_inst/builtins/ or # src/types/checker/builtins/ → (area, sub_area). When multiple files match # the same key, the first one wins. AREA_BY_FILE: Dict[str, Optional[Tuple[str, str]]] = { @@ -508,6 +508,7 @@ "rand": ["int", "int"], "mt_rand": ["int", "int"], "random_int": ["int", "int"], + "random_bytes": ["int"], "is_nan": ["float"], "is_finite": ["float"], "is_infinite": ["float"], @@ -1213,8 +1214,6 @@ def slug(name: str) -> str: "next": "mixed", "prev": "mixed", "reset": "mixed", - # in_array returns the value if found, false otherwise → mixed. - "in_array": "mixed", # String functions with a concrete string return type. "addslashes": "string", "bin2hex": "string", @@ -1268,98 +1267,6 @@ def slug(name: str) -> str: } -# Hand-curated by-reference parameter overrides. signatures.rs does not always -# mark parameters that PHP passes by reference; these overrides make the -# generated docs match the actual PHP calling convention. -REF_PARAM_OVERRIDES: Dict[str, List[bool]] = { - "exec": [False, True, True], - "is_callable": [False, False, True], - "passthru": [False, True], - "preg_match_all": [False, False, True], - "preg_replace": [False, False, False, False, True], - "preg_replace_callback": [False, False, False, False, True, False], - "str_ireplace": [False, False, False, True], - "str_replace": [False, False, False, True], - "system": [False, True], - "stream_socket_client": [False, True, True, False, False], - "stream_socket_server": [False, True, True], -} - - -# Hand-curated optional-parameter overrides. Each entry is a list parallel to -# the parameter list: None = required, string = optional with that PHP default. -OPTIONAL_PARAM_OVERRIDES: Dict[str, List[Optional[str]]] = { - "file_put_contents": [None, None, "0", "null"], - "fputcsv": [None, None, "','", "'\"'", "'\\\\'", "'\\n'"], - "hash": [None, None, "false", "[]"], - "hash_file": [None, None, "false", "[]"], - "hash_init": [None, "0", "''", "[]"], - "is_callable": [None, "false", "null"], - "phpversion": ["null"], - "preg_replace": [None, None, None, "-1", "null"], - "preg_replace_callback": [None, None, None, "-1", "null", "0"], - "print_r": [None, "false"], - "rmdir": [None, "null"], -} - - -# Hand-curated parameter-name overrides. signatures.rs sometimes uses generic -# names like `value`; these match the PHP manual names. -PARAM_NAME_OVERRIDES: Dict[str, List[Optional[str]]] = { - "array_chunk": [None, None, "preserve_keys"], - "array_column": [None, None, "index_key"], - "array_keys": [None, "filter_value", "strict"], - "array_rand": [None, "num"], - "array_reverse": [None, "preserve_keys"], - "array_slice": [None, None, None, "preserve_keys"], - "array_splice": [None, None, None, "replacement"], - "array_unique": [None, "flags"], - "array_walk": [None, None, "arg"], - "arsort": [None, "flags"], - "asort": [None, "flags"], - "base64_decode": [None, "strict"], - "exec": [None, "output", "result_code"], - "fgetcsv": [None, None, None, "enclosure", "escape"], - "fgets": [None, "length"], - "fwrite": [None, None, "length"], - "getenv": [None, "local_only"], - "glob": [None, "flags"], - "html_entity_decode": [None, "flags", "encoding"], - "htmlentities": [None, "flags", "encoding", "double_encode"], - "htmlspecialchars": [None, "flags", "encoding", "double_encode"], - "is_callable": [None, "syntax_only"], - "krsort": [None, "flags"], - "ksort": [None, "flags"], - "mkdir": [None, "permissions", "recursive", "context"], - "nl2br": [None, "use_xhtml"], - "passthru": [None, "result_code"], - "preg_replace": [None, None, None, "limit"], - "preg_replace_callback": [None, None, None, "limit"], - "range": [None, None, "step"], - "rename": [None, None, "context"], - "rsort": [None, "flags"], - "scandir": [None, "sorting_order", "context"], - "sort": [None, "flags"], - "stream_context_get_options": ["stream_or_context"], - "stream_filter_append": [None, "filter_name", "mode"], - "stream_filter_prepend": [None, "filter_name", "mode"], - "stream_socket_client": [None, "error_code", "error_message", "timeout", "flags"], - "stream_socket_server": [None, "error_code", "error_message"], - "system": [None, "result_code"], - "fputcsv": [None, None, None, None, "escape", "eol"], - "gzcompress": [None, None, "encoding"], - "gzdeflate": [None, None, "encoding"], - "hash": [None, None, None, "options"], - "hash_file": [None, None, None, "options"], - "hash_init": [None, None, None, "options"], - "is_callable": [None, "syntax_only", "callable_name"], - "phpversion": ["extension"], - "preg_replace": [None, None, None, "limit", "count"], - "preg_replace_callback": [None, None, None, "limit", "count", "flags"], - "rmdir": [None, "context"], -} - - # Hand-curated one-line descriptions for the user-facing pages. When a # builtin has no override here, the renderer falls back to the first line of # the lowering function's `///` doc comment, if available. @@ -1402,13 +1309,3 @@ def slug(name: str) -> str: "Calls the native PHAR compression-control bridge and returns whether the update succeeded.", ], } - - -# Hand-curated variadic overrides. signatures.rs says `fixed(&["value"])` -# for these but PHP actually accepts any number of arguments. -# Note: printf/sprintf/fprintf are already variadic in signatures.rs, -# so they do not need an override. -VARIADIC_OVERRIDES: Dict[str, str] = { - "var_dump": "values", - "print_r": "values", -} diff --git a/scripts/docs/elephc_builtins/render.py b/scripts/docs/elephc_builtins/render.py index 8677b7b06b..71792fe8be 100644 --- a/scripts/docs/elephc_builtins/render.py +++ b/scripts/docs/elephc_builtins/render.py @@ -521,7 +521,8 @@ def area_dir(base: Path, b: dict) -> Path: if internals_path.exists() and not force: skipped += 1 else: - internals_path.write_text(render_internals(b, idx, repo), encoding="utf-8") + content = render_internals(b, idx, repo).rstrip() + "\n" + internals_path.write_text(content, encoding="utf-8") written += 1 # Compiler-only builtin entries are tracked outside the PHP-visible catalog. diff --git a/scripts/gen_windows_codegen_allowlist.py b/scripts/gen_windows_codegen_allowlist.py new file mode 100755 index 0000000000..33e8b7ea4c --- /dev/null +++ b/scripts/gen_windows_codegen_allowlist.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +"""Manage the Windows (windows-x86_64) codegen no-regression parity gate. + +This single tool owns every piece of the Windows codegen parity gate that is +expressed in data rather than YAML: + + * ``generate`` — (re)builds the two in-repo source-of-truth lists from a + ``cargo nextest list`` JSON dump (the *runnable* set) and a set of failing + test names (JUnit reports and/or plain-text lists): + + allow_list = runnable_ci_tests - known_windows_failures + known_failures = the failing set (sorted) + + * ``gate`` — the CI post-step. Given the allow-list and the *actual* failing + tests of a Windows-under-wine run (per-shard JUnit reports), it computes + ``regressions = actual_failures ∩ allow_list`` and exits non-zero iff that + intersection is non-empty. Tests that are NOT in the allow-list (the known + failures and any brand-new / native-only tests) can never fail the gate. + +Both subcommands share the same JUnit / plain-text failure parsing, so the +"what counts as a failing test name" definition lives in exactly one place. + +Determinism: every emitted list is sorted with Python's default (Unicode +code-point) ordering, independent of the host locale, so regenerating from the +same inputs always produces byte-identical files. + +Usage examples:: + + # Regenerate both lists from a fresh nextest list + the failures dump. + python3 scripts/gen_windows_codegen_allowlist.py generate \\ + --list-json nextest_list.json \\ + --failures win_failing.txt + + # Regenerate from the 16 per-shard JUnit artifacts of a parity CI run. + python3 scripts/gen_windows_codegen_allowlist.py generate \\ + --list-json nextest_list.json \\ + --junit artifacts/*/junit.xml + + # CI gate step for one shard. + python3 scripts/gen_windows_codegen_allowlist.py gate \\ + --allowlist tests/codegen/support/windows_codegen_allowlist.txt \\ + --junit target/nextest/ci/junit.xml \\ + --shard 3/16 +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +# Default in-repo locations, resolved relative to the repository root (this +# script lives in `/scripts/`). +REPO_ROOT = Path(__file__).resolve().parent.parent +SUPPORT_DIR = REPO_ROOT / "tests" / "codegen" / "support" +DEFAULT_ALLOWLIST = SUPPORT_DIR / "windows_codegen_allowlist.txt" +DEFAULT_KNOWN_FAILURES = SUPPORT_DIR / "windows_codegen_known_failures.txt" + +# The nextest test binary whose fixtures make up the parity suite. +CODEGEN_SUITE_ID = "elephc::codegen_tests" + + +def _local_tag(tag: str) -> str: + """Return an XML element tag without its ``{namespace}`` prefix, if any.""" + return tag.rsplit("}", 1)[-1] + + +def parse_junit_failures(path: Path) -> tuple[set[str], int]: + """Parse a nextest JUnit report into (failing_test_names, tests_run). + + A ```` counts as failing iff it has a direct child element named + ``failure`` or ``error`` (nextest emits ```` for panics, non-zero + exits, and slow-timeout terminations). Passing tests carry no such child; + skipped/ignored tests are not emitted at all. The returned name is the + testcase ``name`` attribute, which nextest sets to the full test path + (e.g. ``codegen::arrays::callbacks::test_array_all``) — the exact form used + throughout these lists. + """ + root = ET.parse(path).getroot() + failures: set[str] = set() + ran = 0 + for testcase in root.iter("testcase"): + name = testcase.attrib.get("name") + if name is None: + continue + ran += 1 + if any(_local_tag(child.tag) in ("failure", "error") for child in testcase): + failures.add(name) + return failures, ran + + +def read_name_list(path: Path) -> set[str]: + """Read a plain-text list of test names, ignoring blank and ``#`` lines.""" + names: set[str] = set() + for raw in Path(path).read_text().splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + names.add(line) + return names + + +def collect_failures(junit_paths: list[str], failure_paths: list[str]) -> tuple[set[str], int | None]: + """Union the failing test names from JUnit reports and plain-text lists. + + Returns (failures, tests_run). ``tests_run`` is the total number of executed + testcases seen across the JUnit reports (used for the parity summary), or + ``None`` when only plain-text failure lists were supplied (a bare name list + carries no notion of how many tests ran). + """ + failures: set[str] = set() + ran_total = 0 + saw_junit = False + for jp in junit_paths: + saw_junit = True + f, ran = parse_junit_failures(Path(jp)) + failures |= f + ran_total += ran + for fp in failure_paths: + failures |= read_name_list(Path(fp)) + return failures, (ran_total if saw_junit else None) + + +def load_runnable(list_json_path: Path) -> set[str]: + """Extract the CI-profile *runnable* codegen tests from a nextest list dump. + + Reads ``cargo nextest list --profile ci --test codegen_tests + --message-format json`` output. A test is runnable when it is not + ``#[ignore]``d and matches the profile's filter (``filter-match.status == + "matches"``). Returns the set of full test names. + """ + data = json.loads(Path(list_json_path).read_text()) + suites = data.get("rust-suites", {}) + suite = suites.get(CODEGEN_SUITE_ID) + if suite is None: + raise SystemExit( + f"error: nextest list JSON has no '{CODEGEN_SUITE_ID}' suite " + f"(found: {sorted(suites)})" + ) + runnable: set[str] = set() + for name, tc in suite.get("testcases", {}).items(): + if tc.get("ignored"): + continue + fm = tc.get("filter-match") + matched = fm.get("status") == "matches" if isinstance(fm, dict) else bool(fm) + if matched: + runnable.add(name) + return runnable + + +def _write_list(path: Path, header: list[str], names: list[str]) -> None: + """Write a sorted name list with a ``#`` comment header to ``path``.""" + path.parent.mkdir(parents=True, exist_ok=True) + lines = [f"# {h}" if h else "#" for h in header] + lines.extend(names) + path.write_text("\n".join(lines) + "\n") + + +ALLOWLIST_HEADER = [ + "Windows (windows-x86_64) codegen no-regression allow-list.", + "", + "One full nextest test name per line (sorted, comments start with '#').", + "These codegen fixtures currently PASS when cross-compiled to", + "windows-x86_64 and run under wine64. The CI gate fails iff any test in", + "THIS list regresses (starts failing) on Windows; tests NOT listed here", + "(the known failures and any brand-new / native-only fixtures) never fail", + "the gate, so Windows parity can only improve, never regress.", + "", + "allow_list = (ci-profile runnable codegen tests) - (known failures)", + "", + "Regenerate with:", + " cargo nextest list --profile ci --test codegen_tests \\", + " --message-format json > nextest_list.json", + " python3 scripts/gen_windows_codegen_allowlist.py generate \\", + " --list-json nextest_list.json \\", + " --junit ", + "See docs/compiling/targets.md ('Windows codegen parity gate').", + "DO NOT hand-edit; regenerate so it stays in sync with the suite.", +] + +KNOWN_FAILURES_HEADER = [ + "Windows (windows-x86_64) codegen KNOWN-FAILURES list (companion to the", + "allow-list). One full nextest test name per line (sorted, '#' comments).", + "These codegen fixtures currently FAIL under windows-x86_64 + wine64. They", + "are the complement of the allow-list within the ci-profile runnable set:", + "", + " known_failures = (ci-profile runnable codegen tests) - allow_list", + "", + "Kept in-repo as the source of truth for how the allow-list was derived and", + "to track Windows parity progress. As failures get fixed, refresh both files", + "together with scripts/gen_windows_codegen_allowlist.py (a fixed test moves", + "from here into the allow-list). DO NOT hand-edit; regenerate.", +] + + +def cmd_generate(args: argparse.Namespace) -> int: + """Regenerate the allow-list and known-failures files from source inputs.""" + runnable = load_runnable(Path(args.list_json)) + failures, _ = collect_failures(args.junit, args.failures) + if not failures: + raise SystemExit( + "error: no failing test names were supplied " + "(pass --junit and/or --failures)." + ) + + stale = sorted(failures - runnable) + if stale: + preview = "\n ".join(stale[:20]) + more = "" if len(stale) <= 20 else f"\n ... and {len(stale) - 20} more" + msg = ( + f"error: {len(stale)} failing test name(s) are not in the runnable " + f"ci set (stale/renamed inputs?):\n {preview}{more}\n" + "The failure inputs and the nextest list JSON must come from the " + "same revision. Re-run `cargo nextest list` on the same commit." + ) + if args.allow_stale_failures: + print("WARNING: " + msg, file=sys.stderr) + failures &= runnable + else: + raise SystemExit(msg + "\n(Use --allow-stale-failures to drop them.)") + + allow = sorted(runnable - failures) + known = sorted(failures) + + _write_list(Path(args.out_allowlist), ALLOWLIST_HEADER, allow) + _write_list(Path(args.out_known_failures), KNOWN_FAILURES_HEADER, known) + + print(f"runnable ci codegen tests : {len(runnable)}") + print(f"known Windows failures : {len(known)}") + print(f"allow-list (known-good) : {len(allow)}") + print(f"wrote {args.out_allowlist}") + print(f"wrote {args.out_known_failures}") + assert not (set(allow) & set(known)), "allow-list and known-failures overlap" + return 0 + + +def _emit_summary(shard: str, ran: int | None, failed_count: int, regressions: list[str]) -> None: + """Append the per-shard parity picture to $GITHUB_STEP_SUMMARY, if set.""" + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + return + lines = [f"### Windows codegen parity — shard {shard}", ""] + if ran is not None: + passed = ran - failed_count + parity = (passed / ran * 100.0) if ran else 100.0 + lines += [ + "| metric | value |", + "| --- | --- |", + f"| tests run | {ran} |", + f"| passed | {passed} |", + f"| failed | {failed_count} |", + f"| parity | {parity:.1f}% |", + f"| allow-list regressions | {len(regressions)} |", + "", + ] + else: + lines += [f"failed tests: {failed_count}, allow-list regressions: {len(regressions)}", ""] + if regressions: + lines.append("**Regressed (allow-listed) tests:**") + lines.append("") + lines += [f"- `{name}`" for name in regressions] + lines.append("") + with open(summary_path, "a") as fh: + fh.write("\n".join(lines) + "\n") + + +def cmd_gate(args: argparse.Namespace) -> int: + """Fail iff any allow-listed test failed in this Windows-under-wine run. + + Loads the allow-list and the actual failing tests (JUnit and/or plain-text), + intersects them, emits the parity summary, and returns 1 when the + intersection is non-empty (printing the regressed names) or 0 otherwise. + """ + allow = read_name_list(Path(args.allowlist)) + if not allow: + raise SystemExit(f"error: allow-list '{args.allowlist}' is empty — refusing to run a no-op gate.") + failures, ran = collect_failures(args.junit, args.failures) + + regressions = sorted(failures & allow) + _emit_summary(args.shard, ran, len(failures), regressions) + + if regressions: + print( + f"WINDOWS CODEGEN REGRESSION: {len(regressions)} allow-listed " + f"test(s) failed on windows-x86_64 (shard {args.shard}):", + file=sys.stderr, + ) + for name in regressions: + print(f" {name}", file=sys.stderr) + print( + "\nThese tests are in the known-good allow-list " + f"({args.allowlist}); a Windows failure here is a regression.\n" + "If a test legitimately can no longer pass on Windows, refresh the " + "allow-list with scripts/gen_windows_codegen_allowlist.py.", + file=sys.stderr, + ) + return 1 + + print(f"OK: no allow-listed Windows codegen regressions (shard {args.shard}); {len(failures)} non-gated failure(s).") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + """Construct the argparse CLI with the ``generate`` and ``gate`` subcommands.""" + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = p.add_subparsers(dest="command", required=True) + + g = sub.add_parser("generate", help="regenerate the allow-list and known-failures files") + g.add_argument("--list-json", required=True, help="cargo nextest list --message-format json output") + g.add_argument("--junit", action="append", default=[], help="nextest JUnit report(s) with the failing tests") + g.add_argument("--failures", action="append", default=[], help="plain-text failing-test list(s), one name per line") + g.add_argument("--out-allowlist", default=str(DEFAULT_ALLOWLIST)) + g.add_argument("--out-known-failures", default=str(DEFAULT_KNOWN_FAILURES)) + g.add_argument("--allow-stale-failures", action="store_true", help="drop (don't error on) failures missing from the runnable set") + g.set_defaults(func=cmd_generate) + + t = sub.add_parser("gate", help="fail iff an allow-listed test regressed on Windows") + t.add_argument("--allowlist", default=str(DEFAULT_ALLOWLIST)) + t.add_argument("--junit", action="append", default=[], help="this run's nextest JUnit report(s)") + t.add_argument("--failures", action="append", default=[], help="plain-text failing-test list(s), one name per line") + t.add_argument("--shard", default="?", help="shard label for logs/summary, e.g. 3/16") + t.set_defaults(func=cmd_gate) + return p + + +def main(argv: list[str]) -> int: + """CLI entry point: parse arguments and dispatch to the chosen subcommand.""" + args = build_parser().parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index c897bfd12e..151f3928e7 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -44,6 +44,7 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "DomainException", "EmptyIterator", "Error", + "ArithmeticError", "Exception", "Fiber", "FiberError", diff --git a/src/bin/gen_builtins.rs b/src/bin/gen_builtins.rs new file mode 100644 index 0000000000..c40890ceec --- /dev/null +++ b/src/bin/gen_builtins.rs @@ -0,0 +1,27 @@ +//! 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/builtins/array/array_all.rs b/src/builtins/array/array_all.rs new file mode 100644 index 0000000000..1085840cc2 --- /dev/null +++ b/src/builtins/array/array_all.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Home of the PHP `array_all` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `fixed(&["array","callback"])` (exactly 2 required params). +//! The legacy CHECK arm also required exactly 2 arguments; no arity override is needed. +//! - `check` validates the first argument is an indexed array and validates the predicate +//! callback with a dummy element argument. Returns `PhpType::Bool`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_all` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_all", + area: Array, + params: [array: Mixed, callback: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Returns true when every array element satisfies the predicate callback.", + php_manual: "https://www.php.net/manual/en/function.array-all.php", +} + +/// Validates the predicate callback for an `array_all` call and returns `PhpType::Bool`. +/// +/// The first argument must be an indexed array. The callback is validated with a single +/// dummy element argument derived from the array element type. Arity (exactly 2 args) is +/// pre-validated by `check_arity`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_)) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + let dummy_args = vec![ + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem( + &arr_ty, cx.span, + ), + ]; + let label = format!("{}() callback", cx.name); + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + &label, + )?; + Ok(PhpType::Bool) +} + +/// Lowers an `array_all` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_all(ctx, inst) +} diff --git a/src/builtins/array/array_any.rs b/src/builtins/array/array_any.rs new file mode 100644 index 0000000000..1910def53e --- /dev/null +++ b/src/builtins/array/array_any.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Home of the PHP `array_any` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `fixed(&["array","callback"])` (exactly 2 required params). +//! The legacy CHECK arm also required exactly 2 arguments; no arity override is needed. +//! - `check` validates the first argument is an indexed array and validates the predicate +//! callback with a dummy element argument. Returns `PhpType::Bool`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_any` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_any", + area: Array, + params: [array: Mixed, callback: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Returns true when at least one array element satisfies the predicate callback.", + php_manual: "https://www.php.net/manual/en/function.array-any.php", +} + +/// Validates the predicate callback for an `array_any` call and returns `PhpType::Bool`. +/// +/// The first argument must be an indexed array. The callback is validated with a single +/// dummy element argument derived from the array element type. Arity (exactly 2 args) is +/// pre-validated by `check_arity`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_)) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + let dummy_args = vec![ + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem( + &arr_ty, cx.span, + ), + ]; + let label = format!("{}() callback", cx.name); + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + &label, + )?; + Ok(PhpType::Bool) +} + +/// Lowers an `array_any` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_any(ctx, inst) +} diff --git a/src/builtins/array/array_chunk.rs b/src/builtins/array/array_chunk.rs new file mode 100644 index 0000000000..6e00f1eba0 --- /dev/null +++ b/src/builtins/array/array_chunk.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Home of the PHP `array_chunk` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy rule: chunking an indexed `Array` yields a +//! nested `Array>`. Associative inputs are rejected (the lowering only +//! supports indexed arrays), and non-array inputs are rejected too. A check hook is +//! required because the return type depends on the inferred argument type. +//! - Arity (exactly 2 arguments) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_chunk` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_chunk", + area: Array, + params: [array: Mixed, length: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Splits an array into chunks of the given size.", + php_manual: "https://www.php.net/manual/en/function.array-chunk.php", +} + +/// Returns the nested chunk-array type for an `array_chunk` call. +/// +/// An indexed `Array` chunks into `Array>`. Associative arrays are +/// rejected (only indexed arrays are supported), and non-array arguments are rejected. +/// The argument is re-inferred here to drive the return type; the registry already +/// inferred every argument once for side effects, and arity (exactly 2) is pre-validated. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(elem_ty) => Ok(PhpType::Array(Box::new(PhpType::Array(elem_ty)))), + PhpType::AssocArray { .. } => Err(CompileError::new( + cx.span, + "array_chunk() argument must be indexed array", + )), + _ => Err(CompileError::new( + cx.span, + "array_chunk() first argument must be array", + )), + } +} + +/// Lowers an `array_chunk` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_chunk(ctx, inst) +} diff --git a/src/builtins/array/array_column.rs b/src/builtins/array/array_column.rs new file mode 100644 index 0000000000..58918b08d8 --- /dev/null +++ b/src/builtins/array/array_column.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Home of the PHP `array_column` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy rule: the first argument must be an `Array` of +//! associative arrays; the result is an indexed `Array` of the associative value +//! type. Other shapes are rejected. A check hook is required because the return type +//! depends on the inferred argument type. +//! - Arity (exactly 2 arguments) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! Note elephc only supports the 2-argument form (`array`, `column_key`). +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_column` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_column", + area: Array, + params: [array: Mixed, column_key: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns the values from a single column of an array of arrays.", + php_manual: "https://www.php.net/manual/en/function.array-column.php", +} + +/// Returns the extracted-column array type for an `array_column` call. +/// +/// The first argument must be an `Array` of associative arrays; the result is an +/// indexed `Array` of the associative value type. Other shapes are rejected. The +/// argument is re-inferred here to drive the return type; the registry already +/// inferred every argument once for side effects, and arity (exactly 2) is pre-validated. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(inner) => match *inner { + PhpType::AssocArray { value, .. } => Ok(PhpType::Array(value)), + _ => Err(CompileError::new( + cx.span, + "array_column() requires an array of associative arrays", + )), + }, + _ => Err(CompileError::new( + cx.span, + "array_column() first argument must be array", + )), + } +} + +/// Lowers an `array_column` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_column(ctx, inst) +} diff --git a/src/builtins/array/array_combine.rs b/src/builtins/array/array_combine.rs new file mode 100644 index 0000000000..83ee51027a --- /dev/null +++ b/src/builtins/array/array_combine.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Home of the PHP `array_combine` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy rule: the result is an associative array whose key +//! type is derived from the keys-array element type (via +//! `array_key_type_from_value_type`) and whose value type is the values-array element +//! type. Both arguments must be indexed arrays. A check hook is required because the +//! return type depends on the two inferred argument types. +//! - Arity (exactly 2 arguments) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_combine` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::{array_key_type_from_value_type, PhpType}; + +builtin! { + name: "array_combine", + area: Array, + params: [keys: Mixed, values: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Creates an array by using one array for keys and another for values.", + php_manual: "https://www.php.net/manual/en/function.array-combine.php", +} + +/// Returns the combined associative-array type for an `array_combine` call. +/// +/// The key type is derived from the keys-array element type via +/// `array_key_type_from_value_type`, and the value type is the values-array element +/// type. Both arguments must be indexed arrays. They are re-inferred here to drive the +/// return type; the registry already inferred them once for side effects, and arity +/// (exactly 2) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let keys_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let vals_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + let key_elem = match keys_ty { + PhpType::Array(elem) => *elem, + _ => { + return Err(CompileError::new( + cx.span, + "array_combine() first argument must be array", + )); + } + }; + let val_elem = match vals_ty { + PhpType::Array(elem) => *elem, + _ => { + return Err(CompileError::new( + cx.span, + "array_combine() second argument must be array", + )); + } + }; + Ok(PhpType::AssocArray { + key: Box::new(array_key_type_from_value_type(key_elem)), + value: Box::new(val_elem), + }) +} + +/// Lowers an `array_combine` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_combine(ctx, inst) +} diff --git a/src/builtins/array/array_diff.rs b/src/builtins/array/array_diff.rs new file mode 100644 index 0000000000..33ab2e5b33 --- /dev/null +++ b/src/builtins/array/array_diff.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Home of the PHP `array_diff` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&["array"], "arrays")` (one regular `array` +//! param plus a variadic `arrays`). The legacy CHECK arm required exactly 2 arguments, +//! so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` only; +//! `function_sig` and the parity gate keep the variadic shape from the golden. +//! - `check` reproduces the legacy rule: the first argument must be an indexed or +//! associative array, and the result preserves that first-operand type. A check hook +//! is required because the return type depends on the inferred first-argument type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_diff` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_diff", + area: Array, + params: [array: Mixed], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Computes the difference of arrays.", + php_manual: "https://www.php.net/manual/en/function.array-diff.php", +} + +/// Validates the first argument is an array and returns its (preserved) type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. The first argument is +/// re-inferred here to drive the return type; the registry already inferred every +/// argument once for side effects. The result preserves the first-operand array shape. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty1, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + Ok(ty1) +} + +/// Lowers an `array_diff` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_diff(ctx, inst) +} diff --git a/src/builtins/array/array_diff_assoc.rs b/src/builtins/array/array_diff_assoc.rs new file mode 100644 index 0000000000..74a8edf821 --- /dev/null +++ b/src/builtins/array/array_diff_assoc.rs @@ -0,0 +1,66 @@ +//! Purpose: +//! Home of the PHP `array_diff_assoc` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&["array"], "arrays")` (one regular `array` +//! param plus a variadic `arrays`). The legacy CHECK arm required exactly 2 arguments, +//! so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` only; +//! `function_sig` and the parity gate keep the variadic shape from the golden. +//! - `check` reproduces the legacy rule: both arguments must be associative arrays or +//! indexed arrays of scalars, and the result is the two-input hash result type. A +//! check hook is required because the return type depends on the inferred arguments. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_diff_assoc` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_diff_assoc", + area: Array, + params: [array: Mixed], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Computes the difference of arrays with additional index check.", + php_manual: "https://www.php.net/manual/en/function.array-diff-assoc.php", +} + +/// Validates both arguments are hash-compatible arrays and returns the merged hash type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. Both arguments are +/// re-inferred here to drive the return type; the registry already inferred every +/// argument once for side effects. Each operand must be an associative array or an +/// indexed array of scalars; the result widens key/value to `Mixed` when the operands +/// disagree, via `PhpType::two_input_hash_result`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + let ty2 = cx.checker.infer_type(&cx.args[1], cx.env)?; + let accepted = + |t: &PhpType| matches!(t, PhpType::AssocArray { .. }) || t.is_scalar_indexed_array(); + if !accepted(&ty1) || !accepted(&ty2) { + return Err(CompileError::new( + cx.span, + &format!( + "{}() arguments must be associative arrays or indexed arrays of scalars", + cx.name + ), + )); + } + Ok(PhpType::two_input_hash_result(&ty1, &ty2)) +} + +/// Lowers an `array_diff_assoc` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_diff_assoc(ctx, inst) +} diff --git a/src/builtins/array/array_diff_key.rs b/src/builtins/array/array_diff_key.rs new file mode 100644 index 0000000000..fa382287b9 --- /dev/null +++ b/src/builtins/array/array_diff_key.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Home of the PHP `array_diff_key` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&["array"], "arrays")` (one regular `array` +//! param plus a variadic `arrays`). The legacy CHECK arm required exactly 2 arguments, +//! so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` only; +//! `function_sig` and the parity gate keep the variadic shape from the golden. +//! - `check` reproduces the legacy rule: the first argument must be an indexed or +//! associative array, and the result preserves that first-operand type. A check hook +//! is required because the return type depends on the inferred first-argument type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_diff_key` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_diff_key", + area: Array, + params: [array: Mixed], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Computes the difference of arrays using keys for comparison.", + php_manual: "https://www.php.net/manual/en/function.array-diff-key.php", +} + +/// Validates the first argument is an array and returns its (preserved) type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. The first argument is +/// re-inferred here to drive the return type; the registry already inferred every +/// argument once for side effects. The result preserves the first-operand array shape. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty1, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + Ok(ty1) +} + +/// Lowers an `array_diff_key` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_diff_key(ctx, inst) +} diff --git a/src/builtins/array/array_fill.rs b/src/builtins/array/array_fill.rs new file mode 100644 index 0000000000..5efec2275a --- /dev/null +++ b/src/builtins/array/array_fill.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Home of the PHP `array_fill` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` computes the actual return type based on the `start_index` argument: +//! a literal-zero start produces an indexed array; any other start produces an +//! associative array with Int keys and Mixed values. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_fill` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_fill", + area: Array, + params: [start_index: Mixed, count: Mixed, value: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Fill an array with values.", + php_manual: "https://www.php.net/manual/en/function.array-fill.php", +} + +/// Computes the return array type based on whether `start_index` is a literal zero. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 3 arguments). +/// A non-literal-zero start builds a keyed assoc array (Int → Mixed); a literal-zero +/// start builds an indexed array preserving the value type. This mirrors the codegen +/// emitter's branch logic so static types stay consistent with runtime behavior. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.infer_type(&cx.args[1], cx.env)?; + let val_ty = cx.checker.infer_type(&cx.args[2], cx.env)?; + let start_is_literal_zero = + matches!(cx.args[0].kind, crate::parser::ast::ExprKind::IntLiteral(0)); + if !start_is_literal_zero { + Ok(PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(PhpType::Mixed), + }) + } else { + Ok(PhpType::Array(Box::new(val_ty))) + } +} + +/// Lowers an `array_fill` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_fill(ctx, inst) +} diff --git a/src/builtins/array/array_fill_keys.rs b/src/builtins/array/array_fill_keys.rs new file mode 100644 index 0000000000..a3dbbde56a --- /dev/null +++ b/src/builtins/array/array_fill_keys.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Home of the PHP `array_fill_keys` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the first argument is an indexed array and returns an +//! associative array whose key type is derived from the element type of `keys` +//! and whose value type matches `value`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_fill_keys` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_fill_keys", + area: Array, + params: [keys: Mixed, value: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Fill an array with values, specifying keys.", + php_manual: "https://www.php.net/manual/en/function.array-fill-keys.php", +} + +/// Validates `keys` is an indexed array and returns the resulting assoc-array type. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// The key type of the resulting assoc array is derived via `array_key_type_from_value_type` +/// from the element type of `keys`; the value type is the inferred type of `value`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let keys_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let val_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + let key_elem = match keys_ty { + PhpType::Array(elem) => *elem, + _ => { + return Err(CompileError::new( + cx.span, + "array_fill_keys() first argument must be array", + )); + } + }; + Ok(PhpType::AssocArray { + key: Box::new(crate::types::array_key_type_from_value_type(key_elem)), + value: Box::new(val_ty), + }) +} + +/// Lowers an `array_fill_keys` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_fill_keys(ctx, inst) +} diff --git a/src/builtins/array/array_filter.rs b/src/builtins/array/array_filter.rs new file mode 100644 index 0000000000..2e2c67a6bd --- /dev/null +++ b/src/builtins/array/array_filter.rs @@ -0,0 +1,76 @@ +//! Purpose: +//! Home of the PHP `array_filter` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `optional(&["array","callback","mode"], 1, &[null, 0])`. +//! The legacy CHECK arm required 2 or 3 arguments (`args.len() < 2 || args.len() > 3`), +//! so `min_args: 2` reproduces that enforcement in `check_arity`; the derived max of 3 +//! from the optional signature already matches. +//! - `check` validates the first argument is an indexed array, builds callback dummy args +//! based on the static mode value, and validates the callback signature. The return type +//! preserves the input array element type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_filter` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_filter", + area: Array, + params: [array: Mixed, callback: Mixed = DefaultSpec::Null, mode: Mixed = DefaultSpec::Int(0)], + min_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Filters elements of an array using a callback function.", + php_manual: "https://www.php.net/manual/en/function.array-filter.php", +} + +/// Returns the filtered array type for an `array_filter` call. +/// +/// Validates the first argument is an indexed array, builds the callback dummy args +/// based on the optional mode argument, and validates the callback. Arity (2 or 3 args) +/// is pre-validated by `check_arity`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match arr_ty { + PhpType::Array(elem_ty) => { + let arr_ty = PhpType::Array(elem_ty.clone()); + let dummy_args = + crate::types::checker::builtins::array_filter_callback_dummy_args( + &arr_ty, + cx.args.get(2), + cx.span, + ); + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + "array_filter() callback", + )?; + Ok(PhpType::Array(elem_ty)) + } + _ => Err(CompileError::new( + cx.span, + "array_filter() first argument must be array", + )), + } +} + +/// Lowers an `array_filter` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_filter(ctx, inst) +} diff --git a/src/builtins/array/array_find.rs b/src/builtins/array/array_find.rs new file mode 100644 index 0000000000..50dbaef94f --- /dev/null +++ b/src/builtins/array/array_find.rs @@ -0,0 +1,70 @@ +//! Purpose: +//! Home of the PHP `array_find` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `fixed(&["array","callback"])` (exactly 2 required params). +//! The legacy CHECK arm also required exactly 2 arguments; no arity override is needed. +//! - `check` validates the first argument is an indexed array and validates the predicate +//! callback with a dummy element argument. Returns `PhpType::Mixed` (the matching element +//! or null). +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_find` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_find", + area: Array, + params: [array: Mixed, callback: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns the first element satisfying a predicate callback, or null.", + php_manual: "https://www.php.net/manual/en/function.array-find.php", +} + +/// Validates the predicate callback for an `array_find` call and returns `PhpType::Mixed`. +/// +/// The first argument must be an indexed array. The callback is validated with a single +/// dummy element argument derived from the array element type. Arity (exactly 2 args) is +/// pre-validated by `check_arity`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_)) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + let dummy_args = vec![ + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem( + &arr_ty, cx.span, + ), + ]; + let label = format!("{}() callback", cx.name); + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + &label, + )?; + Ok(PhpType::Mixed) +} + +/// Lowers an `array_find` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_find(ctx, inst) +} diff --git a/src/builtins/array/array_flip.rs b/src/builtins/array/array_flip.rs new file mode 100644 index 0000000000..1944e87bcd --- /dev/null +++ b/src/builtins/array/array_flip.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Home of the PHP `array_flip` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy rule: flipping swaps keys and values, so the +//! result is an associative array whose key type is derived from the input value +//! type (via `array_key_type_from_value_type`). An indexed array flips to +//! `AssocArray`; an associative array flips to +//! `AssocArray`. A check hook is required because the +//! return type depends on the inferred argument type. +//! - Arity (exactly 1 argument) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_flip` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::{array_key_type_from_value_type, PhpType}; + +builtin! { + name: "array_flip", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Exchanges all keys with their associated values in an array.", + php_manual: "https://www.php.net/manual/en/function.array-flip.php", +} + +/// Returns the flipped associative-array type for an `array_flip` call. +/// +/// Keys and values swap places, so the new key type is derived from the old value +/// type via `array_key_type_from_value_type`. The argument is re-inferred here to +/// drive the return type; the registry already inferred it once for side effects, +/// and arity is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(elem_ty) => Ok(PhpType::AssocArray { + key: Box::new(array_key_type_from_value_type(*elem_ty)), + value: Box::new(PhpType::Int), + }), + PhpType::AssocArray { key, value } => Ok(PhpType::AssocArray { + key: Box::new(array_key_type_from_value_type(*value)), + value: key, + }), + _ => Err(CompileError::new( + cx.span, + "array_flip() argument must be array", + )), + } +} + +/// Lowers an `array_flip` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_flip(ctx, inst) +} diff --git a/src/builtins/array/array_intersect.rs b/src/builtins/array/array_intersect.rs new file mode 100644 index 0000000000..8ed3cc1986 --- /dev/null +++ b/src/builtins/array/array_intersect.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Home of the PHP `array_intersect` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&["array"], "arrays")` (one regular `array` +//! param plus a variadic `arrays`). The legacy CHECK arm required exactly 2 arguments, +//! so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` only; +//! `function_sig` and the parity gate keep the variadic shape from the golden. +//! - `check` reproduces the legacy rule: the first argument must be an indexed or +//! associative array, and the result preserves that first-operand type. A check hook +//! is required because the return type depends on the inferred first-argument type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_intersect` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_intersect", + area: Array, + params: [array: Mixed], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Computes the intersection of arrays.", + php_manual: "https://www.php.net/manual/en/function.array-intersect.php", +} + +/// Validates the first argument is an array and returns its (preserved) type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. The first argument is +/// re-inferred here to drive the return type; the registry already inferred every +/// argument once for side effects. The result preserves the first-operand array shape. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty1, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + Ok(ty1) +} + +/// Lowers an `array_intersect` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_intersect(ctx, inst) +} diff --git a/src/builtins/array/array_intersect_assoc.rs b/src/builtins/array/array_intersect_assoc.rs new file mode 100644 index 0000000000..9cd7721fb5 --- /dev/null +++ b/src/builtins/array/array_intersect_assoc.rs @@ -0,0 +1,66 @@ +//! Purpose: +//! Home of the PHP `array_intersect_assoc` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&["array"], "arrays")` (one regular `array` +//! param plus a variadic `arrays`). The legacy CHECK arm required exactly 2 arguments, +//! so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` only; +//! `function_sig` and the parity gate keep the variadic shape from the golden. +//! - `check` reproduces the legacy rule: both arguments must be associative arrays or +//! indexed arrays of scalars, and the result is the two-input hash result type. A +//! check hook is required because the return type depends on the inferred arguments. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_intersect_assoc` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_intersect_assoc", + area: Array, + params: [array: Mixed], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Computes the intersection of arrays with additional index check.", + php_manual: "https://www.php.net/manual/en/function.array-intersect-assoc.php", +} + +/// Validates both arguments are hash-compatible arrays and returns the merged hash type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. Both arguments are +/// re-inferred here to drive the return type; the registry already inferred every +/// argument once for side effects. Each operand must be an associative array or an +/// indexed array of scalars; the result widens key/value to `Mixed` when the operands +/// disagree, via `PhpType::two_input_hash_result`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + let ty2 = cx.checker.infer_type(&cx.args[1], cx.env)?; + let accepted = + |t: &PhpType| matches!(t, PhpType::AssocArray { .. }) || t.is_scalar_indexed_array(); + if !accepted(&ty1) || !accepted(&ty2) { + return Err(CompileError::new( + cx.span, + &format!( + "{}() arguments must be associative arrays or indexed arrays of scalars", + cx.name + ), + )); + } + Ok(PhpType::two_input_hash_result(&ty1, &ty2)) +} + +/// Lowers an `array_intersect_assoc` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_intersect_assoc(ctx, inst) +} diff --git a/src/builtins/array/array_intersect_key.rs b/src/builtins/array/array_intersect_key.rs new file mode 100644 index 0000000000..e5ee8e7b3c --- /dev/null +++ b/src/builtins/array/array_intersect_key.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Home of the PHP `array_intersect_key` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&["array"], "arrays")` (one regular `array` +//! param plus a variadic `arrays`). The legacy CHECK arm required exactly 2 arguments, +//! so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` only; +//! `function_sig` and the parity gate keep the variadic shape from the golden. +//! - `check` reproduces the legacy rule: the first argument must be an indexed or +//! associative array, and the result preserves that first-operand type. A check hook +//! is required because the return type depends on the inferred first-argument type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_intersect_key` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_intersect_key", + area: Array, + params: [array: Mixed], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Computes the intersection of arrays using keys for comparison.", + php_manual: "https://www.php.net/manual/en/function.array-intersect-key.php", +} + +/// Validates the first argument is an array and returns its (preserved) type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. The first argument is +/// re-inferred here to drive the return type; the registry already inferred every +/// argument once for side effects. The result preserves the first-operand array shape. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty1, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + Ok(ty1) +} + +/// Lowers an `array_intersect_key` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_intersect_key(ctx, inst) +} diff --git a/src/builtins/array/array_is_list.rs b/src/builtins/array/array_is_list.rs new file mode 100644 index 0000000000..0c0ceb7082 --- /dev/null +++ b/src/builtins/array/array_is_list.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Home of the PHP `array_is_list` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The return type is always `Bool`, but a check hook is still required (rather than +//! a pure-data `returns: Bool`) because the legacy arm rejects non-array arguments at +//! type-check time, and that guard is covered by an error test +//! (`array_is_list() argument must be array`). The hook reproduces the guard and +//! then returns `Bool`. +//! - Arity (exactly 1 argument) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_is_list` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_is_list", + area: Array, + params: [array: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Checks whether an array is a list (sequential 0-based integer keys).", + php_manual: "https://www.php.net/manual/en/function.array-is-list.php", +} + +/// Returns `PhpType::Bool` for an `array_is_list` call, rejecting non-array arguments. +/// +/// The return type is always `Bool`, but the argument must be an array-like value +/// (`Array`, `AssocArray`, or boxed `Mixed`); other types are a type error. The +/// argument is re-inferred here to enforce that guard; the registry already inferred +/// it once for side effects, and arity (exactly 1) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!( + ty, + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Mixed + ) { + return Err(CompileError::new( + cx.span, + "array_is_list() argument must be array", + )); + } + Ok(PhpType::Bool) +} + +/// Lowers an `array_is_list` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_is_list(ctx, inst) +} diff --git a/src/builtins/array/array_key_exists.rs b/src/builtins/array/array_key_exists.rs new file mode 100644 index 0000000000..0285cec64d --- /dev/null +++ b/src/builtins/array/array_key_exists.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `array_key_exists` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the second argument is an array and returns `Bool`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_key_exists` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_key_exists", + area: Array, + params: [key: Mixed, array: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Checks if the given key or index exists in the array.", + php_manual: "https://www.php.net/manual/en/function.array-key-exists.php", +} + +/// Validates that the second argument is an array and returns `Bool`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// This hook validates that `array` is an array and returns the `Bool` return type. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let arr_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_key_exists() second argument must be array", + )); + } + Ok(PhpType::Bool) +} + +/// Lowers an `array_key_exists` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_key_exists(ctx, inst) +} diff --git a/src/builtins/array/array_key_first.rs b/src/builtins/array/array_key_first.rs new file mode 100644 index 0000000000..f7e1ffd166 --- /dev/null +++ b/src/builtins/array/array_key_first.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `array_key_first` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is an array (or Mixed) and returns `Mixed`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_key_first` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_key_first", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets the first key of an array.", + php_manual: "https://www.php.net/manual/en/function.array-key-first.php", +} + +/// Validates that the argument is an array or Mixed and returns `Mixed`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +/// Mixed is permitted because heterogeneous arrays are represented as Mixed at compile time. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Mixed) { + return Err(CompileError::new( + cx.span, + "array_key_first() argument must be array", + )); + } + Ok(PhpType::Mixed) +} + +/// Lowers an `array_key_first` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_key_first(ctx, inst) +} diff --git a/src/builtins/array/array_key_last.rs b/src/builtins/array/array_key_last.rs new file mode 100644 index 0000000000..02a2ef1bb5 --- /dev/null +++ b/src/builtins/array/array_key_last.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `array_key_last` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is an array (or Mixed) and returns `Mixed`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_key_last` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_key_last", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets the last key of an array.", + php_manual: "https://www.php.net/manual/en/function.array-key-last.php", +} + +/// Validates that the argument is an array or Mixed and returns `Mixed`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +/// Mixed is permitted because heterogeneous arrays are represented as Mixed at compile time. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Mixed) { + return Err(CompileError::new( + cx.span, + "array_key_last() argument must be array", + )); + } + Ok(PhpType::Mixed) +} + +/// Lowers an `array_key_last` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_key_last(ctx, inst) +} diff --git a/src/builtins/array/array_keys.rs b/src/builtins/array/array_keys.rs new file mode 100644 index 0000000000..01a154a9b6 --- /dev/null +++ b/src/builtins/array/array_keys.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Home of the PHP `array_keys` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy return-type rule: an indexed array yields +//! `Array` (positional keys) while an associative array yields `Array`. +//! A check hook is required because the return type depends on the inferred +//! argument type, which the `builtin!` `returns:` field cannot express. +//! - Arity (exactly 1 argument) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_keys` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_keys", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns all the keys of an array.", + php_manual: "https://www.php.net/manual/en/function.array-keys.php", +} + +/// Returns the key-array type for an `array_keys` call. +/// +/// An indexed array produces `Array`; an associative array produces +/// `Array`. Any other argument type is rejected. The argument is re-inferred +/// here to drive the return type; the registry already inferred it once for side +/// effects, and arity is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(_) => Ok(PhpType::Array(Box::new(PhpType::Int))), + PhpType::AssocArray { key, .. } => Ok(PhpType::Array(key)), + _ => Err(CompileError::new( + cx.span, + "array_keys() argument must be array", + )), + } +} + +/// Lowers an `array_keys` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_keys(ctx, inst) +} diff --git a/src/builtins/array/array_map.rs b/src/builtins/array/array_map.rs new file mode 100644 index 0000000000..13e0e13975 --- /dev/null +++ b/src/builtins/array/array_map.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Home of the PHP `array_map` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&["callback","array"], "arrays")` (two +//! required params plus a variadic `arrays`). The legacy CHECK arm required exactly +//! 2 arguments, so `min_args: 2, max_args: 2` reproduce that enforcement in +//! `check_arity` only; `function_sig` and the parity gate keep the variadic shape. +//! - `check` validates that the second argument is an indexed array and infers the +//! callback return element type; the result preserves the input array element type +//! unless the callback returns Mixed. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_map` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_map", + area: Array, + params: [callback: Mixed, array: Mixed], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Applies a callback to the elements of an array.", + php_manual: "https://www.php.net/manual/en/function.array-map.php", +} + +/// Returns the mapped array type for an `array_map` call. +/// +/// Validates that the second argument is an indexed array, checks the callback +/// with a dummy element argument, and derives the result element type from the +/// callback return type. Arity (exactly 2 args) is pre-validated by `check_arity`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + match arr_ty { + PhpType::Array(elem_ty) => { + let arr_ty = PhpType::Array(elem_ty.clone()); + let dummy_args = vec![ + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem( + &arr_ty, cx.span, + ), + ]; + let callback_ret_ty = + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[0], + &dummy_args, + cx.span, + cx.env, + "array_map() callback", + )?; + let result_elem_ty = if callback_ret_ty == PhpType::Mixed { + Box::new(PhpType::Mixed) + } else { + elem_ty + }; + Ok(PhpType::Array(result_elem_ty)) + } + _ => Err(CompileError::new( + cx.span, + "array_map() second argument must be array", + )), + } +} + +/// Lowers an `array_map` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_map(ctx, inst) +} diff --git a/src/builtins/array/array_merge.rs b/src/builtins/array/array_merge.rs new file mode 100644 index 0000000000..6c78c19cd6 --- /dev/null +++ b/src/builtins/array/array_merge.rs @@ -0,0 +1,92 @@ +//! Purpose: +//! Home of the PHP `array_merge` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&[], "arrays")` (min=0). The legacy CHECK +//! arm requires exactly 2 arguments. `min_args: 2, max_args: 2` reproduce that +//! enforcement in `check_arity` only; `function_sig` and the parity gate keep the +//! variadic shape from the golden. +//! - `check` validates that the first argument is an indexed or associative array and +//! returns the merged result type. The return type logic mirrors the legacy checker: +//! when the first operand is an empty array (element type `Void`), the result adopts +//! the second operand's element type if it is a scalar-merge type. +//! - Arity is pre-validated by `check_arity`; the hook can assume exactly 2 args. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_merge", + area: Array, + params: [], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Merges the elements of two arrays.", + php_manual: "https://www.php.net/manual/en/function.array-merge.php", +} + +/// Validates the first argument is an array and returns the merged result type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. The hook re-infers both +/// argument types to derive the precise result type: when the left operand is an empty +/// indexed array (element type `Void`), the result adopts the right operand's element +/// type if it is a scalar-merge-compatible type. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + let ty2 = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(ty1, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_merge() first argument must be array", + )); + } + Ok(array_merge_return_type(ty1, ty2)) +} + +/// Lowers an `array_merge` call by delegating to the shared array-merge emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_merge(ctx, inst) +} + +/// Infers the return type for `array_merge(first, second)`. +/// +/// When `first` is an empty indexed array (element type `Void`), the merged result +/// adopts `second`'s element type if it is a scalar-merge-compatible type; otherwise +/// the result keeps `first`'s type. For non-empty indexed arrays, the left operand +/// type is returned unchanged (matching legacy checker behavior). +fn array_merge_return_type(first: PhpType, second: PhpType) -> PhpType { + match first { + PhpType::Array(elem) if is_empty_array_element_type(elem.as_ref()) => match second { + PhpType::Array(right) if is_scalar_merge_element_type(right.as_ref()) => { + PhpType::Array(right) + } + _ => PhpType::Array(elem), + }, + other => 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 that the scalar merge runtime helper copies safely. +fn is_scalar_merge_element_type(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Callable | PhpType::Void + ) +} diff --git a/src/builtins/array/array_merge_recursive.rs b/src/builtins/array/array_merge_recursive.rs new file mode 100644 index 0000000000..d1d66b5a70 --- /dev/null +++ b/src/builtins/array/array_merge_recursive.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Home of the PHP `array_merge_recursive` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `variadic(&[], "arrays")` (min=0). The legacy CHECK +//! arm requires exactly 2 arguments. `min_args: 2, max_args: 2` reproduce that +//! enforcement in `check_arity` only; `function_sig` and the parity gate keep the +//! variadic shape from the golden. +//! - `check` validates that both arguments are associative or scalar-indexed arrays and +//! returns an `AssocArray` whose value type is always `Mixed` (scalar collisions +//! combine into lists). The key type widens to `Mixed` when the two input key types +//! disagree. +//! - Arity is pre-validated by `check_arity`; the hook can assume exactly 2 args. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_merge_recursive", + area: Array, + params: [], + variadic: "arrays", + min_args: 2, + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Recursively merges two arrays, combining scalar collisions into lists.", + php_manual: "https://www.php.net/manual/en/function.array-merge-recursive.php", +} + +/// Validates both arguments are compatible arrays and returns the recursively-merged type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. The hook re-infers both +/// argument types. Both must be associative arrays or indexed arrays of scalars. Scalar +/// collisions combine into lists, so the value type of the result is always `Mixed`; the +/// key type widens to `Mixed` when the two input key types disagree. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + let ty2 = cx.checker.infer_type(&cx.args[1], cx.env)?; + let accepted = |t: &PhpType| { + matches!(t, PhpType::AssocArray { .. }) || t.is_scalar_indexed_array() + }; + if !accepted(&ty1) || !accepted(&ty2) { + return Err(CompileError::new( + cx.span, + "array_merge_recursive() arguments must be associative arrays or indexed arrays of scalars", + )); + } + Ok(PhpType::AssocArray { + key: Box::new(PhpType::widen(ty1.hash_key_type(), ty2.hash_key_type())), + value: Box::new(PhpType::Mixed), + }) +} + +/// Lowers an `array_merge_recursive` call by delegating to the shared emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_merge_recursive(ctx, inst) +} diff --git a/src/builtins/array/array_multisort.rs b/src/builtins/array/array_multisort.rs new file mode 100644 index 0000000000..a2f37d81ed --- /dev/null +++ b/src/builtins/array/array_multisort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `array_multisort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `fixed(["array1","array2"])` with `ref_params = [true, true]`: +//! exactly 2 by-ref params. The `ref` markers are mandatory for in-place mutation. +//! - `check` requires BOTH arguments are indexed `Array(_)` types, returning Bool. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_multisort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_multisort", + area: Array, + params: [ref array1: Mixed, ref array2: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Sorts multiple arrays or multi-dimensional arrays.", + php_manual: "https://www.php.net/manual/en/function.array-multisort.php", +} + +/// Validates argument types for an `array_multisort` call. +/// +/// Requires both arguments be indexed arrays (`PhpType::Array(_)`). Arity (exactly 2) is +/// pre-validated by the registry. Returns `Ok(PhpType::Bool)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + let ty2 = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(ty1, PhpType::Array(_)) || !matches!(ty2, PhpType::Array(_)) { + return Err(CompileError::new(cx.span, "array_multisort() arguments must be indexed arrays")); + } + Ok(PhpType::Bool) +} + +/// Lowers an `array_multisort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_multisort(ctx, inst) +} diff --git a/src/builtins/array/array_pad.rs b/src/builtins/array/array_pad.rs new file mode 100644 index 0000000000..6856b0bb24 --- /dev/null +++ b/src/builtins/array/array_pad.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Home of the PHP `array_pad` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy rule: padding preserves the array shape, so the +//! return type is the (array-or-assoc) first-argument type unchanged. A check hook is +//! required both to reject a non-array first argument and to echo its type back. +//! - Arity (exactly 3 arguments) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_pad` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_pad", + area: Array, + params: [array: Mixed, length: Mixed, value: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Pads an array to the specified length with a value.", + php_manual: "https://www.php.net/manual/en/function.array-pad.php", +} + +/// Returns the (shape-preserving) array type for an `array_pad` call. +/// +/// Padding keeps the array shape, so the first-argument array/assoc type is returned +/// unchanged. A non-array first argument is rejected. The first argument is re-inferred +/// here; the registry already inferred every argument once for side effects, and arity +/// (exactly 3) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_pad() first argument must be array", + )); + } + Ok(ty) +} + +/// Lowers an `array_pad` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_pad(ctx, inst) +} diff --git a/src/builtins/array/array_pop.rs b/src/builtins/array/array_pop.rs new file mode 100644 index 0000000000..f34fd9bbea --- /dev/null +++ b/src/builtins/array/array_pop.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Home of the PHP `array_pop` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` reproduces the legacy rule: `Array(elem)` yields the element type, +//! `AssocArray { value, .. }` yields the value type, any other type is an error. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_pop` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_pop", + area: Array, + params: [ref array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Pops the element off the end of array.", + php_manual: "https://www.php.net/manual/en/function.array-pop.php", +} + +/// Returns the element type for an `array_pop` call. +/// +/// The `array` argument is re-inferred to drive the return type. Arity (exactly 1) is +/// pre-validated by the registry. `Array(elem)` yields the element type; `AssocArray` +/// yields the value type; any other type is a compile error. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(elem) => Ok(*elem), + PhpType::AssocArray { value, .. } => Ok(*value), + _ => Err(CompileError::new(cx.span, "array_pop() argument must be array")), + } +} + +/// Lowers an `array_pop` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_pop(ctx, inst) +} diff --git a/src/builtins/array/array_product.rs b/src/builtins/array/array_product.rs new file mode 100644 index 0000000000..6ffd9da7de --- /dev/null +++ b/src/builtins/array/array_product.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Home of the PHP `array_product` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` computes the actual return type (Int or Float) based on the element type +//! of the argument array. The declared `returns: Int` is only used as the FCC type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_product` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_product", + area: Array, + params: [array: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Calculate the product of values in an array.", + php_manual: "https://www.php.net/manual/en/function.array-product.php", +} + +/// Computes the return type (Int or Float) based on the array element type. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +/// A float-element array yields Float; integer or mixed-element arrays yield Int. +/// Non-array arguments are rejected. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(ref elem_ty) if **elem_ty == PhpType::Float => Ok(PhpType::Float), + PhpType::Array(_) => Ok(PhpType::Int), + PhpType::AssocArray { ref value, .. } if **value == PhpType::Float => Ok(PhpType::Float), + PhpType::AssocArray { .. } => Ok(PhpType::Int), + _ => Err(CompileError::new( + cx.span, + "array_product() argument must be array", + )), + } +} + +/// Lowers an `array_product` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_product(ctx, inst) +} diff --git a/src/builtins/array/array_push.rs b/src/builtins/array/array_push.rs new file mode 100644 index 0000000000..57cd4602d4 --- /dev/null +++ b/src/builtins/array/array_push.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Home of the PHP `array_push` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(variadic(["array"], "values"))`: `array` +//! by-ref plus a variadic `values` param. The legacy CHECK arm enforced exactly 2 +//! arguments, so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` +//! only; `function_sig` and the parity gate keep the variadic shape from the golden. +//! - The `ref` marker on `array` is mandatory — it is what makes by-reference mutation +//! lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - Returns `Void` (not PHP's int count) — reproducing the legacy behavior exactly. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_push` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_push", + area: Array, + params: [ref array: Mixed], + variadic: "values", + min_args: 2, + max_args: 2, + returns: Void, + check: check, + lower: lower, + summary: "Pushes one or more elements onto the end of array.", + php_manual: "https://www.php.net/manual/en/function.array-push.php", +} + +/// Validates the first argument is an indexed array for an `array_push` call. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. Both arguments are inferred +/// to produce any side effects; the first must be an indexed array or the call is rejected. +/// Returns `Void` — matching the legacy checker behavior. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let _val_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if let PhpType::Array(_) = arr_ty { + Ok(PhpType::Void) + } else { + Err(CompileError::new(cx.span, "array_push() first argument must be array")) + } +} + +/// Lowers an `array_push` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_push(ctx, inst) +} diff --git a/src/builtins/array/array_rand.rs b/src/builtins/array/array_rand.rs new file mode 100644 index 0000000000..6bbed72fd9 --- /dev/null +++ b/src/builtins/array/array_rand.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `array_rand` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the argument is an array and returns `Int` (the randomly +//! selected integer index). The declared `returns: Mixed` is the FCC type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_rand` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_rand", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Pick one or more random keys out of an array.", + php_manual: "https://www.php.net/manual/en/function.array-rand.php", +} + +/// Validates that the argument is an array and returns `Int`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +/// The runtime always returns a single random integer index from the array. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_rand() argument must be array", + )); + } + Ok(PhpType::Int) +} + +/// Lowers an `array_rand` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_rand(ctx, inst) +} diff --git a/src/builtins/array/array_reduce.rs b/src/builtins/array/array_reduce.rs new file mode 100644 index 0000000000..55b7110ff5 --- /dev/null +++ b/src/builtins/array/array_reduce.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Home of the PHP `array_reduce` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `optional(&["array","callback","initial"], 2, &[null])`. +//! The legacy CHECK arm required exactly 3 arguments, so `min_args: 3, max_args: 3` +//! reproduce that enforcement in `check_arity` only. +//! - `check` validates the callback with a two-element dummy args list (carry=int literal, +//! element=array element dummy). The return type is `PhpType::Int`, matching the legacy arm. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_reduce` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::{Expr, ExprKind}; +use crate::types::PhpType; + +builtin! { + name: "array_reduce", + area: Array, + params: [array: Mixed, callback: Mixed, initial: Mixed = DefaultSpec::Null], + min_args: 3, + max_args: 3, + returns: Mixed, + check: check, + lower: lower, + summary: "Iteratively reduces an array to a single value using a callback function.", + php_manual: "https://www.php.net/manual/en/function.array-reduce.php", +} + +/// Validates the callback for an `array_reduce` call and returns `PhpType::Int`. +/// +/// Builds a two-element dummy args list: an integer literal as the carry placeholder +/// and a scalar element placeholder derived from the first-argument array type. +/// Arity (exactly 3 args) is pre-validated by `check_arity`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let dummy_args = vec![ + Expr::new(ExprKind::IntLiteral(0), cx.span), + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem( + &arr_ty, cx.span, + ), + ]; + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + "array_reduce() callback", + )?; + Ok(PhpType::Int) +} + +/// Lowers an `array_reduce` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_reduce(ctx, inst) +} diff --git a/src/builtins/array/array_replace.rs b/src/builtins/array/array_replace.rs new file mode 100644 index 0000000000..d566992eed --- /dev/null +++ b/src/builtins/array/array_replace.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Home of the PHP `array_replace` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `fixed(&["array", "replacements"])` (two required +//! params, no variadic), matching the legacy `legacy_builtin_call_sig` arm. The +//! param-derived bounds already require exactly 2 arguments, so no `min_args`/ +//! `max_args` override is needed; `check_arity` reproduces the legacy CHECK arity. +//! - `check` reproduces the legacy rule: both arguments must be associative arrays or +//! indexed arrays of scalars, and the result is the two-input hash result type. A +//! check hook is required because the return type depends on the inferred arguments. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_replace` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_replace", + area: Array, + params: [array: Mixed, replacements: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Replaces elements from passed arrays into the first array.", + php_manual: "https://www.php.net/manual/en/function.array-replace.php", +} + +/// Validates both arguments are hash-compatible arrays and returns the merged hash type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. Both arguments are +/// re-inferred here to drive the return type; the registry already inferred every +/// argument once for side effects. Each operand must be an associative array or an +/// indexed array of scalars; the result widens key/value to `Mixed` when the operands +/// disagree, via `PhpType::two_input_hash_result`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + let ty2 = cx.checker.infer_type(&cx.args[1], cx.env)?; + let accepted = + |t: &PhpType| matches!(t, PhpType::AssocArray { .. }) || t.is_scalar_indexed_array(); + if !accepted(&ty1) || !accepted(&ty2) { + return Err(CompileError::new( + cx.span, + &format!( + "{}() arguments must be associative arrays or indexed arrays of scalars", + cx.name + ), + )); + } + Ok(PhpType::two_input_hash_result(&ty1, &ty2)) +} + +/// Lowers an `array_replace` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_replace(ctx, inst) +} diff --git a/src/builtins/array/array_replace_recursive.rs b/src/builtins/array/array_replace_recursive.rs new file mode 100644 index 0000000000..e418030583 --- /dev/null +++ b/src/builtins/array/array_replace_recursive.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Home of the PHP `array_replace_recursive` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `fixed(&["array", "replacements"])` (two required +//! params, no variadic), matching the legacy `legacy_builtin_call_sig` arm. The +//! param-derived bounds already require exactly 2 arguments, so no `min_args`/ +//! `max_args` override is needed; `check_arity` reproduces the legacy CHECK arity. +//! - `check` reproduces the legacy rule: both arguments must be associative arrays or +//! indexed arrays of scalars, and the result is the two-input hash result type. A +//! check hook is required because the return type depends on the inferred arguments. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_replace_recursive` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_replace_recursive", + area: Array, + params: [array: Mixed, replacements: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Replaces elements from passed arrays into the first array recursively.", + php_manual: "https://www.php.net/manual/en/function.array-replace-recursive.php", +} + +/// Validates both arguments are hash-compatible arrays and returns the merged hash type. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. Both arguments are +/// re-inferred here to drive the return type; the registry already inferred every +/// argument once for side effects. Each operand must be an associative array or an +/// indexed array of scalars; the result widens key/value to `Mixed` when the operands +/// disagree, via `PhpType::two_input_hash_result`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty1 = cx.checker.infer_type(&cx.args[0], cx.env)?; + let ty2 = cx.checker.infer_type(&cx.args[1], cx.env)?; + let accepted = + |t: &PhpType| matches!(t, PhpType::AssocArray { .. }) || t.is_scalar_indexed_array(); + if !accepted(&ty1) || !accepted(&ty2) { + return Err(CompileError::new( + cx.span, + &format!( + "{}() arguments must be associative arrays or indexed arrays of scalars", + cx.name + ), + )); + } + Ok(PhpType::two_input_hash_result(&ty1, &ty2)) +} + +/// Lowers an `array_replace_recursive` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_replace_recursive(ctx, inst) +} diff --git a/src/builtins/array/array_reverse.rs b/src/builtins/array/array_reverse.rs new file mode 100644 index 0000000000..639ab30e33 --- /dev/null +++ b/src/builtins/array/array_reverse.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Home of the PHP `array_reverse` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy rule: reversing preserves the array shape, so the +//! return type is the (array-or-assoc) input type unchanged. A check hook is +//! required both to reject non-array arguments and to echo the input type back. +//! - Arity (exactly 1 argument) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_reverse` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_reverse", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns an array with the elements in reverse order.", + php_manual: "https://www.php.net/manual/en/function.array-reverse.php", +} + +/// Returns the (shape-preserving) array type for an `array_reverse` call. +/// +/// Reversing keeps the array shape, so the input array/assoc type is returned +/// unchanged. Non-array arguments are rejected. The argument is re-inferred here; +/// the registry already inferred it once for side effects, and arity is pre-validated. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_reverse() argument must be array", + )); + } + Ok(ty) +} + +/// Lowers an `array_reverse` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_reverse(ctx, inst) +} diff --git a/src/builtins/array/array_search.rs b/src/builtins/array/array_search.rs new file mode 100644 index 0000000000..06b1c2ed06 --- /dev/null +++ b/src/builtins/array/array_search.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Home of the PHP `array_search` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the second argument is an array and returns a union of the +//! key type and Bool (false on not-found), or Int|Bool for indexed arrays. +//! - The golden signature carries the optional `strict` param (min=2, max=3), but the +//! legacy CHECK arm enforced exactly 2 arguments and the `lower_array_search` emitter +//! only supports 2 args. `max_args: 2` reproduces that exact-2 enforcement in +//! `check_arity` only; `function_sig` and the parity gate keep the full param-derived +//! bounds from the golden. This keeps the clean "takes exactly 2 arguments" checker +//! diagnostic for a 3-arg call instead of an EIR backend error. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_search` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_search", + area: Array, + params: [needle: Mixed, haystack: Mixed, strict: Bool = DefaultSpec::Bool(false)], + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Searches the array for a given value and returns the first corresponding key if successful.", + php_manual: "https://www.php.net/manual/en/function.array-search.php", +} + +/// Validates haystack is an array and returns the key-or-false union type. +/// +/// The registry's `check_arity` handles arity enforcement (capped at 2 by `max_args` +/// to match the legacy CHECK arm). For assoc arrays the return is `key_type | bool`; +/// for indexed arrays it is `int | bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let arr_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_search() second argument must be array", + )); + } + match arr_ty { + PhpType::AssocArray { key, .. } => { + Ok(cx.checker.normalize_union_type(vec![*key, PhpType::Bool])) + } + _ => Ok(PhpType::Union(vec![PhpType::Int, PhpType::Bool])), + } +} + +/// Lowers an `array_search` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_search(ctx, inst) +} diff --git a/src/builtins/array/array_shift.rs b/src/builtins/array/array_shift.rs new file mode 100644 index 0000000000..19dbdc5af6 --- /dev/null +++ b/src/builtins/array/array_shift.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Home of the PHP `array_shift` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` reproduces the legacy rule: `Array(elem)` yields the element type, +//! `AssocArray { value, .. }` yields the value type, any other type is an error. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_shift` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_shift", + area: Array, + params: [ref array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Shifts an element off the beginning of array.", + php_manual: "https://www.php.net/manual/en/function.array-shift.php", +} + +/// Returns the element type for an `array_shift` call. +/// +/// The `array` argument is re-inferred to drive the return type. Arity (exactly 1) is +/// pre-validated by the registry. `Array(elem)` yields the element type; `AssocArray` +/// yields the value type; any other type is a compile error. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(elem) => Ok(*elem), + PhpType::AssocArray { value, .. } => Ok(*value), + _ => Err(CompileError::new(cx.span, "array_shift() argument must be array")), + } +} + +/// Lowers an `array_shift` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_shift(ctx, inst) +} diff --git a/src/builtins/array/array_slice.rs b/src/builtins/array/array_slice.rs new file mode 100644 index 0000000000..40316d893a --- /dev/null +++ b/src/builtins/array/array_slice.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Home of the PHP `array_slice` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy rule: a slice preserves the array shape, so the +//! return type is the (array-or-assoc) input type unchanged; a boxed `Mixed`/`Union` +//! input yields `Mixed`. A check hook is required because the return type depends on +//! the inferred first-argument type. +//! - The declared signature carries the golden param list (`array`, `offset`, +//! `length`), with `length` optional (default `null`), so the registry's +//! `check_arity` accepts 2 or 3 arguments — matching the legacy CHECK arm. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_slice` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_slice", + area: Array, + params: [array: Mixed, offset: Mixed, length: Mixed = DefaultSpec::Null], + returns: Mixed, + check: check, + lower: lower, + summary: "Extracts a slice of an array.", + php_manual: "https://www.php.net/manual/en/function.array-slice.php", +} + +/// Returns the slice's array type for an `array_slice` call. +/// +/// A slice preserves the input array shape, so the (array-or-assoc) first-argument +/// type is returned unchanged; a boxed `Mixed`/`Union` first argument yields `Mixed`. +/// Non-array first arguments are rejected. The first argument is re-inferred here; +/// the registry already inferred every argument once for side effects, and arity +/// (2 or 3) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + return Ok(PhpType::Mixed); + } + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_slice() first argument must be array", + )); + } + Ok(ty) +} + +/// Lowers an `array_slice` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_slice(ctx, inst) +} diff --git a/src/builtins/array/array_splice.rs b/src/builtins/array/array_splice.rs new file mode 100644 index 0000000000..42046eec64 --- /dev/null +++ b/src/builtins/array/array_splice.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Home of the PHP `array_splice` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(optional(["array","offset","length"], required=2, [null]))`: +//! 3 params, `array` by-ref, `length` optional with default null, arity 2-3. The `ref` marker +//! is mandatory — it is what makes by-reference mutation lower correctly (ir_lower reads +//! `ref_params` from the registry sig). +//! - `check` reproduces the legacy rule: `Mixed`/`Union` first arg yields `Mixed`; `Array` +//! or `AssocArray` yields the first-arg type; any other type is an error. All remaining +//! args are inferred for side effects. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_splice` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_splice", + area: Array, + params: [ref array: Mixed, offset: Int, length: Mixed = DefaultSpec::Null], + returns: Mixed, + check: check, + lower: lower, + summary: "Removes a portion of the array and replaces it with something else.", + php_manual: "https://www.php.net/manual/en/function.array-splice.php", +} + +/// Returns the result type for an `array_splice` call. +/// +/// Arity (2 or 3 args) is pre-validated by the registry. The first argument is re-inferred +/// to drive the return type; remaining arguments are inferred for side effects. `Mixed` or +/// `Union` first arguments yield `Mixed` (opaque path); `Array`/`AssocArray` yield the +/// first-arg type; any other type is a compile error. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + for arg in &cx.args[1..] { + cx.checker.infer_type(arg, cx.env)?; + } + if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + return Ok(PhpType::Mixed); + } + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + Ok(ty) +} + +/// Lowers an `array_splice` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_splice(ctx, inst) +} diff --git a/src/builtins/array/array_sum.rs b/src/builtins/array/array_sum.rs new file mode 100644 index 0000000000..ceb79a709b --- /dev/null +++ b/src/builtins/array/array_sum.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Home of the PHP `array_sum` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` computes the actual return type (Int or Float) based on the element type +//! of the argument array. The declared `returns: Int` is only used as the FCC type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_sum` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_sum", + area: Array, + params: [array: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Calculate the sum of values in an array.", + php_manual: "https://www.php.net/manual/en/function.array-sum.php", +} + +/// Computes the return type (Int or Float) based on the array element type. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +/// A float-element array yields Float; integer or mixed-element arrays yield Int. +/// Non-array arguments are rejected. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(ref elem_ty) if **elem_ty == PhpType::Float => Ok(PhpType::Float), + PhpType::Array(_) => Ok(PhpType::Int), + PhpType::AssocArray { ref value, .. } if **value == PhpType::Float => Ok(PhpType::Float), + PhpType::AssocArray { .. } => Ok(PhpType::Int), + _ => Err(CompileError::new( + cx.span, + "array_sum() argument must be array", + )), + } +} + +/// Lowers an `array_sum` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_sum(ctx, inst) +} diff --git a/src/builtins/array/array_udiff.rs b/src/builtins/array/array_udiff.rs new file mode 100644 index 0000000000..606b89abcd --- /dev/null +++ b/src/builtins/array/array_udiff.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Home of the PHP `array_udiff` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `fixed(&["array1","array2","callback"])` (exactly 3 +//! required params). The legacy CHECK arm also required exactly 3 arguments; no arity +//! override is needed. +//! - `check` validates the first argument is an indexed array, builds a two-element +//! comparator dummy args list (one per array element), and validates the comparator +//! callback. Returns the first-argument array type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_udiff` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_udiff", + area: Array, + params: [array1: Mixed, array2: Mixed, callback: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Computes the difference of arrays using a callback comparator.", + php_manual: "https://www.php.net/manual/en/function.array-udiff.php", +} + +/// Validates the comparator callback for an `array_udiff` call and returns the first-array type. +/// +/// The first argument must be an indexed array. The comparator is validated with two dummy +/// element arguments (one per array element). Arity (exactly 3 args) is pre-validated by +/// `check_arity`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_)) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + let cmp_arg = + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem( + &arr_ty, cx.span, + ); + let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + let label = format!("{}() comparator", cx.name); + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[2], + &dummy_args, + cx.span, + cx.env, + &label, + )?; + Ok(arr_ty) +} + +/// Lowers an `array_udiff` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_udiff(ctx, inst) +} diff --git a/src/builtins/array/array_uintersect.rs b/src/builtins/array/array_uintersect.rs new file mode 100644 index 0000000000..0825de506f --- /dev/null +++ b/src/builtins/array/array_uintersect.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Home of the PHP `array_uintersect` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The PHP golden signature is `fixed(&["array1","array2","callback"])` (exactly 3 +//! required params). The legacy CHECK arm also required exactly 3 arguments; no arity +//! override is needed. +//! - `check` validates the first argument is an indexed array, builds a two-element +//! comparator dummy args list (one per array element), and validates the comparator +//! callback. Returns the first-argument array type. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_uintersect` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_uintersect", + area: Array, + params: [array1: Mixed, array2: Mixed, callback: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Computes the intersection of arrays using a callback comparator.", + php_manual: "https://www.php.net/manual/en/function.array-uintersect.php", +} + +/// Validates the comparator callback for an `array_uintersect` call and returns the first-array type. +/// +/// The first argument must be an indexed array. The comparator is validated with two dummy +/// element arguments (one per array element). Arity (exactly 3 args) is pre-validated by +/// `check_arity`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_)) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be array", cx.name), + )); + } + let cmp_arg = + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem( + &arr_ty, cx.span, + ); + let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + let label = format!("{}() comparator", cx.name); + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[2], + &dummy_args, + cx.span, + cx.env, + &label, + )?; + Ok(arr_ty) +} + +/// Lowers an `array_uintersect` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_uintersect(ctx, inst) +} diff --git a/src/builtins/array/array_unique.rs b/src/builtins/array/array_unique.rs new file mode 100644 index 0000000000..7ed57ada27 --- /dev/null +++ b/src/builtins/array/array_unique.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Home of the PHP `array_unique` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy rule: de-duplication preserves the array shape, +//! so the return type is the (array-or-assoc) input type unchanged. A check hook is +//! required both to reject non-array arguments and to echo the input type back. +//! - Arity (exactly 1 argument) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_unique` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_unique", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Removes duplicate values from an array.", + php_manual: "https://www.php.net/manual/en/function.array-unique.php", +} + +/// Returns the (shape-preserving) array type for an `array_unique` call. +/// +/// De-duplication keeps the array shape, so the input array/assoc type is returned +/// unchanged. Non-array arguments are rejected. The argument is re-inferred here; +/// the registry already inferred it once for side effects, and arity is pre-validated. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_unique() argument must be array", + )); + } + Ok(ty) +} + +/// Lowers an `array_unique` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_unique(ctx, inst) +} diff --git a/src/builtins/array/array_unshift.rs b/src/builtins/array/array_unshift.rs new file mode 100644 index 0000000000..7f71f0cb95 --- /dev/null +++ b/src/builtins/array/array_unshift.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Home of the PHP `array_unshift` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(variadic(["array"], "values"))`: `array` +//! by-ref plus a variadic `values` param. The legacy CHECK arm enforced exactly 2 +//! arguments, so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` +//! only; `function_sig` and the parity gate keep the variadic shape from the golden. +//! - The `ref` marker on `array` is mandatory — it is what makes by-reference mutation +//! lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - Returns `Int` — the new number of elements in the array. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_unshift` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_unshift", + area: Array, + params: [ref array: Mixed], + variadic: "values", + min_args: 2, + max_args: 2, + returns: Int, + check: check, + lower: lower, + summary: "Prepends one or more elements to the beginning of an array.", + php_manual: "https://www.php.net/manual/en/function.array-unshift.php", +} + +/// Validates the first argument is an array for an `array_unshift` call. +/// +/// Arity (exactly 2 args) is pre-validated by `check_arity`. Both arguments are inferred +/// to produce any side effects; the first must be an indexed or associative array or the +/// call is rejected. Returns `Int` — the new element count. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "array_unshift() first argument must be array", + )); + } + Ok(PhpType::Int) +} + +/// Lowers an `array_unshift` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_unshift(ctx, inst) +} diff --git a/src/builtins/array/array_values.rs b/src/builtins/array/array_values.rs new file mode 100644 index 0000000000..85ef2b55ac --- /dev/null +++ b/src/builtins/array/array_values.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Home of the PHP `array_values` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` reproduces the legacy return-type rule: the result is an indexed +//! `Array` whose element type is the input array's value type (the element type +//! for an indexed array, the value type for an associative array). A check hook +//! is required because the return type depends on the inferred argument type. +//! - Arity (exactly 1 argument) is validated by the registry's `check_arity` before +//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_values` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_values", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns all the values of an array, re-indexed numerically.", + php_manual: "https://www.php.net/manual/en/function.array-values.php", +} + +/// Returns the re-indexed value-array type for an `array_values` call. +/// +/// The result is an indexed `Array` carrying the input array's value type. The +/// argument is re-inferred here to drive the return type; the registry already +/// inferred it once for side effects, and arity is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(elem_ty) => Ok(PhpType::Array(elem_ty)), + PhpType::AssocArray { value, .. } => Ok(PhpType::Array(value)), + _ => Err(CompileError::new( + cx.span, + "array_values() argument must be array", + )), + } +} + +/// Lowers an `array_values` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_values(ctx, inst) +} diff --git a/src/builtins/array/array_walk.rs b/src/builtins/array/array_walk.rs new file mode 100644 index 0000000000..ab1526d4b7 --- /dev/null +++ b/src/builtins/array/array_walk.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Home of the PHP `array_walk` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array", "callback"]))`: exactly 2 +//! arguments, the `array` param is by-reference. The `ref` marker drives in-place +//! mutation (ir_lower reads `ref_params` from the registry sig). +//! - `check` validates the array and callback arguments: infers the array element type, +//! builds a dummy element argument for the callback, and validates the callback. +//! Returns `Void`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_walk` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_walk", + area: Array, + params: [ref array: Mixed, callback: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Applies a user function to every member of an array.", + php_manual: "https://www.php.net/manual/en/function.array-walk.php", +} + +/// Validates the array and callback arguments for an `array_walk` call. +/// +/// Infers each argument, derives the element type from the array, builds a single +/// dummy element argument for callback validation, and checks the callback signature. +/// Arity (exactly 2) is pre-validated by the registry. Returns `Ok(PhpType::Void)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let dummy_args = vec![ + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem(&arr_ty, cx.span), + ]; + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + &format!("{}() callback", cx.name), + )?; + Ok(PhpType::Void) +} + +/// Lowers an `array_walk` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_walk(ctx, inst) +} diff --git a/src/builtins/array/array_walk_recursive.rs b/src/builtins/array/array_walk_recursive.rs new file mode 100644 index 0000000000..9317a0671f --- /dev/null +++ b/src/builtins/array/array_walk_recursive.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Home of the PHP `array_walk_recursive` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array", "callback"]))`: exactly 2 +//! arguments, the `array` param is by-reference. The `ref` marker drives in-place +//! mutation (ir_lower reads `ref_params` from the registry sig). +//! - `check` validates the array and callback arguments: infers the array element type, +//! builds a dummy element argument for the callback, and validates the callback. +//! Returns `Void`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_array_walk_recursive` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "array_walk_recursive", + area: Array, + params: [ref array: Mixed, callback: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Applies a user function recursively to every member of an array.", + php_manual: "https://www.php.net/manual/en/function.array-walk-recursive.php", +} + +/// Validates the array and callback arguments for an `array_walk_recursive` call. +/// +/// Infers each argument, derives the element type from the array, builds a single +/// dummy element argument for callback validation, and checks the callback signature. +/// Arity (exactly 2) is pre-validated by the registry. Returns `Ok(PhpType::Void)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let dummy_args = vec![ + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem(&arr_ty, cx.span), + ]; + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + &format!("{}() callback", cx.name), + )?; + Ok(PhpType::Void) +} + +/// Lowers an `array_walk_recursive` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_array_walk_recursive(ctx, inst) +} diff --git a/src/builtins/array/arsort.rs b/src/builtins/array/arsort.rs new file mode 100644 index 0000000000..3bd21de53c --- /dev/null +++ b/src/builtins/array/arsort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `arsort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_arsort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "arsort", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array in descending order and maintains index association.", + php_manual: "https://www.php.net/manual/en/function.arsort.php", +} + +/// Validates the argument type for an `arsort` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers an `arsort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_arsort(ctx, inst) +} diff --git a/src/builtins/array/asort.rs b/src/builtins/array/asort.rs new file mode 100644 index 0000000000..939a665f41 --- /dev/null +++ b/src/builtins/array/asort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `asort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_asort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "asort", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array and maintains index association.", + php_manual: "https://www.php.net/manual/en/function.asort.php", +} + +/// Validates the argument type for an `asort` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers an `asort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_asort(ctx, inst) +} diff --git a/src/builtins/array/count.rs b/src/builtins/array/count.rs new file mode 100644 index 0000000000..5f2ed6c5a8 --- /dev/null +++ b/src/builtins/array/count.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Home of the PHP `count` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the argument type (Array, AssocArray, Mixed, Union-of-countable, or +//! Countable Object) and returns `Int`. The Countable interface check delegates to +//! `cx.checker.class_implements_interface`. +//! - `max_args: 1` reproduces the legacy checker's exactly-1 enforcement: `mode` has a +//! default so `min` derives to 1; capping `max` at 1 yields the standard +//! "count() takes exactly 1 argument" diagnostic. The 2-param golden is preserved for +//! FCC and parity. +//! - `lower` is a thin wrapper over the module-level `lower_count` emitter in +//! `crate::codegen::lower_inst::builtins`. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::checker::builtins::arrays::union_member_is_countable_array; +use crate::types::PhpType; + +builtin! { + name: "count", + area: Array, + params: [value: Mixed, mode: Int = DefaultSpec::Int(0)], + max_args: 1, + returns: Int, + check: check, + lower: lower, + summary: "Counts all elements in an array or Countable object.", + php_manual: "https://www.php.net/manual/en/function.count.php", +} + +/// Validates the argument type and returns `Int`. +/// +/// Accepts Array, AssocArray, Mixed (heterogeneous arrays), a Union where every member +/// is countable, or an Object that implements the `Countable` interface. Arity +/// enforcement (exactly 1 argument) is handled by the registry's `check_arity` via +/// `max_args: 1`. Returns a `CompileError` for non-countable types or non-Countable objects. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match &ty { + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Mixed => Ok(PhpType::Int), + PhpType::Union(members) if members.iter().all(union_member_is_countable_array) => { + Ok(PhpType::Int) + } + PhpType::Object(class_name) => { + if cx.checker.class_implements_interface(class_name, "Countable") { + Ok(PhpType::Int) + } else { + Err(CompileError::new( + cx.span, + "count() object argument must implement Countable", + )) + } + } + _ => Err(CompileError::new( + cx.span, + "count() argument must be array or Countable object", + )), + } +} + +/// Lowers a `count` call by dispatching to the shared module-level emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_count(ctx, inst) +} diff --git a/src/builtins/array/in_array.rs b/src/builtins/array/in_array.rs new file mode 100644 index 0000000000..e6dda91930 --- /dev/null +++ b/src/builtins/array/in_array.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Home of the PHP `in_array` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the second argument is an array and returns `Bool`. +//! - The optional `strict` (3rd) argument selects PHP `===` membership; omitted or +//! false strictness uses PHP `==` semantics for the supported scalar/string paths. +//! - `lower` is a thin wrapper over the shared `arrays::lower_in_array` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "in_array", + area: Array, + params: [needle: Mixed, haystack: Mixed, strict: Bool = DefaultSpec::Bool(false)], + returns: Bool, + check: check, + lower: lower, + summary: "Checks if a value exists in an array.", + php_manual: "https://www.php.net/manual/en/function.in-array.php", +} + +/// Validates that the second argument is an array and returns `Bool`. +/// +/// The registry's `check_arity` handles the 2-to-3 argument range. This hook validates +/// that `haystack` is an array and returns the `Bool` return type. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let arr_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(arr_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "in_array() second argument must be array", + )); + } + Ok(PhpType::Bool) +} + +/// Lowers an `in_array` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_in_array(ctx, inst) +} diff --git a/src/builtins/array/krsort.rs b/src/builtins/array/krsort.rs new file mode 100644 index 0000000000..4661febd77 --- /dev/null +++ b/src/builtins/array/krsort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `krsort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_krsort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "krsort", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array by key in descending order.", + php_manual: "https://www.php.net/manual/en/function.krsort.php", +} + +/// Validates the argument type for a `krsort` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers a `krsort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_krsort(ctx, inst) +} diff --git a/src/builtins/array/ksort.rs b/src/builtins/array/ksort.rs new file mode 100644 index 0000000000..8f71b588a5 --- /dev/null +++ b/src/builtins/array/ksort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `ksort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_ksort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ksort", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array by key in ascending order.", + php_manual: "https://www.php.net/manual/en/function.ksort.php", +} + +/// Validates the argument type for a `ksort` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers a `ksort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_ksort(ctx, inst) +} diff --git a/src/builtins/array/mod.rs b/src/builtins/array/mod.rs new file mode 100644 index 0000000000..e6d777d470 --- /dev/null +++ b/src/builtins/array/mod.rs @@ -0,0 +1,76 @@ +//! Purpose: +//! Groups all `array`-area builtin homes into this module so the registry can +//! collect them in one place. Each submodule declares exactly one builtin via +//! `builtin!` and provides its type-check and lowering hooks. +//! +//! Called from: +//! - `crate::builtins` (`mod array;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - Add `pub mod ;` here for every new array builtin home. +//! - Most array builtins need a `check` hook because their return type depends +//! on the inferred argument type (e.g. `array_keys` over an indexed vs assoc +//! array). The `builtin!` `returns:` field is only consulted when no hook is +//! present. + +pub mod array_all; +pub mod array_any; +pub mod array_chunk; +pub mod array_column; +pub mod array_combine; +pub mod array_diff; +pub mod array_diff_assoc; +pub mod array_diff_key; +pub mod array_fill; +pub mod array_fill_keys; +pub mod array_filter; +pub mod array_find; +pub mod array_flip; +pub mod array_intersect; +pub mod array_intersect_assoc; +pub mod array_intersect_key; +pub mod array_is_list; +pub mod array_key_exists; +pub mod array_key_first; +pub mod array_key_last; +pub mod array_keys; +pub mod array_map; +pub mod array_merge; +pub mod array_merge_recursive; +pub mod array_multisort; +pub mod array_pad; +pub mod array_pop; +pub mod array_product; +pub mod array_push; +pub mod array_rand; +pub mod array_reduce; +pub mod array_replace; +pub mod array_replace_recursive; +pub mod array_reverse; +pub mod array_search; +pub mod array_shift; +pub mod array_slice; +pub mod array_splice; +pub mod array_sum; +pub mod array_udiff; +pub mod array_uintersect; +pub mod array_unique; +pub mod array_unshift; +pub mod array_values; +pub mod array_walk; +pub mod array_walk_recursive; +pub mod arsort; +pub mod asort; +pub mod count; +pub mod in_array; +pub mod krsort; +pub mod ksort; +pub mod natcasesort; +pub mod natsort; +pub mod range; +pub mod rsort; +pub mod shuffle; +pub mod sort; +pub mod uasort; +pub mod uksort; +pub mod usort; diff --git a/src/builtins/array/natcasesort.rs b/src/builtins/array/natcasesort.rs new file mode 100644 index 0000000000..a0f17317bb --- /dev/null +++ b/src/builtins/array/natcasesort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `natcasesort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_natcasesort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "natcasesort", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array using a case-insensitive natural order algorithm.", + php_manual: "https://www.php.net/manual/en/function.natcasesort.php", +} + +/// Validates the argument type for a `natcasesort` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers a `natcasesort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_natcasesort(ctx, inst) +} diff --git a/src/builtins/array/natsort.rs b/src/builtins/array/natsort.rs new file mode 100644 index 0000000000..7d64aed076 --- /dev/null +++ b/src/builtins/array/natsort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `natsort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_natsort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "natsort", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array using a natural order algorithm.", + php_manual: "https://www.php.net/manual/en/function.natsort.php", +} + +/// Validates the argument type for a `natsort` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers a `natsort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_natsort(ctx, inst) +} diff --git a/src/builtins/array/range.rs b/src/builtins/array/range.rs new file mode 100644 index 0000000000..3b3ee2c425 --- /dev/null +++ b/src/builtins/array/range.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `range` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` infers both arguments and always returns `Array(Int)`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_range` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "range", + area: Array, + params: [start: Mixed, end: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Create an array containing a range of elements.", + php_manual: "https://www.php.net/manual/en/function.range.php", +} + +/// Infers both arguments and returns `Array(Int)`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// Both arguments are inferred for side-effect tracking; the return type is always +/// an indexed integer array matching the runtime emitter's output shape. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.infer_type(&cx.args[1], cx.env)?; + Ok(PhpType::Array(Box::new(PhpType::Int))) +} + +/// Lowers a `range` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_range(ctx, inst) +} diff --git a/src/builtins/array/rsort.rs b/src/builtins/array/rsort.rs new file mode 100644 index 0000000000..261118e06d --- /dev/null +++ b/src/builtins/array/rsort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `rsort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_rsort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "rsort", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array in descending order.", + php_manual: "https://www.php.net/manual/en/function.rsort.php", +} + +/// Validates the argument type for an `rsort` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers an `rsort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_rsort(ctx, inst) +} diff --git a/src/builtins/array/shuffle.rs b/src/builtins/array/shuffle.rs new file mode 100644 index 0000000000..86016fdba4 --- /dev/null +++ b/src/builtins/array/shuffle.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `shuffle` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_shuffle` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "shuffle", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Shuffles an array into random order.", + php_manual: "https://www.php.net/manual/en/function.shuffle.php", +} + +/// Validates the argument type for a `shuffle` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers a `shuffle` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_shuffle(ctx, inst) +} diff --git a/src/builtins/array/sort.rs b/src/builtins/array/sort.rs new file mode 100644 index 0000000000..6ee8174330 --- /dev/null +++ b/src/builtins/array/sort.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `sort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array"]))`: exactly 1 argument, +//! the `array` param is by-reference. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `check` requires the argument be an Array or AssocArray, returning Void. +//! - `lower` is a thin wrapper over the shared `arrays::lower_sort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "sort", + area: Array, + params: [ref array: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array in ascending order.", + php_manual: "https://www.php.net/manual/en/function.sort.php", +} + +/// Validates the argument type for a `sort` call. +/// +/// Requires the argument be an indexed or associative array. Arity (exactly 1) is +/// pre-validated by the registry. Returns `Ok(PhpType::Void)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new(cx.span, &format!("{}() argument must be array", cx.name))); + } + Ok(PhpType::Void) +} + +/// Lowers a `sort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_sort(ctx, inst) +} diff --git a/src/builtins/array/uasort.rs b/src/builtins/array/uasort.rs new file mode 100644 index 0000000000..f509184416 --- /dev/null +++ b/src/builtins/array/uasort.rs @@ -0,0 +1,112 @@ +//! Purpose: +//! Home of the PHP `uasort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array", "callback"]))`: exactly 2 +//! arguments, the `array` param is by-reference. The `ref` marker drives in-place +//! mutation (ir_lower reads `ref_params` from the registry sig). +//! - `check` derives the comparator element type from the array value type, validates the +//! callback with two dummy element arguments (comparator receives two values), and handles +//! object-element arrays with typed closure hints. Returns `Void`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_uasort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "uasort", + area: Array, + params: [ref array: Mixed, callback: Mixed], + returns: Void, + check: check, + lazy_check: true, + lower: lower, + summary: "Sorts an array with a user-defined comparison function and maintains index association.", + php_manual: "https://www.php.net/manual/en/function.uasort.php", +} + +/// Validates the array and comparator callback arguments for a `uasort` call. +/// +/// Infers the array value element type, and validates the comparator with two dummy +/// arguments of that element type. Object-element arrays use typed closure hints so +/// an unannotated comparator body (`$a <=> $b`) is checked against the real type. +/// Arity (exactly 2) is pre-validated by the registry. Returns `Ok(PhpType::Void)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let cmp_ty = crate::types::checker::builtins::array_element_type(&arr_ty); + let label = format!("{}() callback", cx.name); + if let PhpType::Object(_) = cmp_ty { + if let ExprKind::Closure { + params, + variadic, + return_type, + body, + captures, + capture_refs, + .. + } = &cx.args[1].kind + { + cx.checker.infer_closure_type_with_param_hints( + params, + variadic, + return_type, + body, + captures, + capture_refs, + &cx.args[1], + cx.env, + &[cmp_ty.clone(), cmp_ty.clone()], + )?; + } else { + cx.checker.infer_type(&cx.args[1], cx.env)?; + let (cmp_arg, elem_binding) = + crate::types::checker::builtins::comparator_dummy_arg_for_elem(&cmp_ty, cx.span); + let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + let mut env_with_elem; + let cb_env: &crate::types::TypeEnv = match &elem_binding { + Some((binding_name, binding_ty)) => { + env_with_elem = cx.env.clone(); + env_with_elem.insert(binding_name.clone(), binding_ty.clone()); + &env_with_elem + } + None => cx.env, + }; + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cb_env, + &label, + )?; + } + } else { + cx.checker.infer_type(&cx.args[1], cx.env)?; + let cmp_arg = + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem(&arr_ty, cx.span); + let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + &label, + )?; + } + Ok(PhpType::Void) +} + +/// Lowers a `uasort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_uasort(ctx, inst) +} diff --git a/src/builtins/array/uksort.rs b/src/builtins/array/uksort.rs new file mode 100644 index 0000000000..51dd67227e --- /dev/null +++ b/src/builtins/array/uksort.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Home of the PHP `uksort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array", "callback"]))`: exactly 2 +//! arguments, the `array` param is by-reference. The `ref` marker drives in-place +//! mutation (ir_lower reads `ref_params` from the registry sig). +//! - `check` validates the comparator with two integer dummy arguments — `uksort` compares +//! array keys (always integer in the supported subset), not values. Returns `Void`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_uksort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::{Expr, ExprKind}; +use crate::span::Span; +use crate::types::PhpType; + +builtin! { + name: "uksort", + area: Array, + params: [ref array: Mixed, callback: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Sorts an array by keys using a user-defined comparison function.", + php_manual: "https://www.php.net/manual/en/function.uksort.php", +} + +/// Validates the array and comparator callback arguments for a `uksort` call. +/// +/// `uksort` compares array keys, which are always integers in the supported subset. +/// The comparator is validated with two integer literal dummy arguments. Arity +/// (exactly 2) is pre-validated by the registry. Returns `Ok(PhpType::Void)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.infer_type(&cx.args[1], cx.env)?; + let cmp_arg = Expr::new(ExprKind::IntLiteral(0), Span::dummy()); + let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + let label = format!("{}() callback", cx.name); + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + &label, + )?; + Ok(PhpType::Void) +} + +/// Lowers a `uksort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_uksort(ctx, inst) +} diff --git a/src/builtins/array/usort.rs b/src/builtins/array/usort.rs new file mode 100644 index 0000000000..9f45cd533f --- /dev/null +++ b/src/builtins/array/usort.rs @@ -0,0 +1,112 @@ +//! Purpose: +//! Home of the PHP `usort` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The golden signature is `first_param_ref(fixed(["array", "callback"]))`: exactly 2 +//! arguments, the `array` param is by-reference. The `ref` marker drives in-place +//! mutation (ir_lower reads `ref_params` from the registry sig). +//! - `check` derives the comparator element type from the array value type, validates the +//! callback with two dummy element arguments (comparator receives two values), and handles +//! object-element arrays with typed closure hints. Returns `Void`. +//! - `lower` is a thin wrapper over the shared `arrays::lower_usort` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "usort", + area: Array, + params: [ref array: Mixed, callback: Mixed], + returns: Void, + check: check, + lazy_check: true, + lower: lower, + summary: "Sorts an array by values using a user-defined comparison function.", + php_manual: "https://www.php.net/manual/en/function.usort.php", +} + +/// Validates the array and comparator callback arguments for a `usort` call. +/// +/// Infers the array value element type, and validates the comparator with two dummy +/// arguments of that element type. Object-element arrays use typed closure hints so +/// an unannotated comparator body (`$a <=> $b`) is checked against the real type. +/// Arity (exactly 2) is pre-validated by the registry. Returns `Ok(PhpType::Void)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let cmp_ty = crate::types::checker::builtins::array_element_type(&arr_ty); + let label = format!("{}() callback", cx.name); + if let PhpType::Object(_) = cmp_ty { + if let ExprKind::Closure { + params, + variadic, + return_type, + body, + captures, + capture_refs, + .. + } = &cx.args[1].kind + { + cx.checker.infer_closure_type_with_param_hints( + params, + variadic, + return_type, + body, + captures, + capture_refs, + &cx.args[1], + cx.env, + &[cmp_ty.clone(), cmp_ty.clone()], + )?; + } else { + cx.checker.infer_type(&cx.args[1], cx.env)?; + let (cmp_arg, elem_binding) = + crate::types::checker::builtins::comparator_dummy_arg_for_elem(&cmp_ty, cx.span); + let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + let mut env_with_elem; + let cb_env: &crate::types::TypeEnv = match &elem_binding { + Some((binding_name, binding_ty)) => { + env_with_elem = cx.env.clone(); + env_with_elem.insert(binding_name.clone(), binding_ty.clone()); + &env_with_elem + } + None => cx.env, + }; + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cb_env, + &label, + )?; + } + } else { + cx.checker.infer_type(&cx.args[1], cx.env)?; + let cmp_arg = + crate::types::checker::builtins::dummy_arg_for_array_scalar_elem(&arr_ty, cx.span); + let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + crate::types::checker::builtins::check_callback_builtin_call( + cx.checker, + &cx.args[1], + &dummy_args, + cx.span, + cx.env, + &label, + )?; + } + Ok(PhpType::Void) +} + +/// Lowers a `usort` call by dispatching to the shared array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_usort(ctx, inst) +} diff --git a/src/builtins/callables/call_user_func.rs b/src/builtins/callables/call_user_func.rs new file mode 100644 index 0000000000..cb12d0fad7 --- /dev/null +++ b/src/builtins/callables/call_user_func.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Home of the PHP `call_user_func` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `lazy_check: true` so the hook controls all inference: the eager `for arg in args` +//! loop is the single inference pass, matching legacy behaviour exactly. +//! - The actual check logic lives in `callables::check_call_user_func` (in the checker +//! module tree) because it accesses checker internals unavailable from here. +//! - `lower` is a thin wrapper over `lower_call_user_func_builtin_escape`, parameterized +//! with the canonical function name. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "call_user_func", + area: Callables, + params: [callback: Mixed], + variadic: "args", + returns: Mixed, + check: check, + lazy_check: true, + lower: lower, + summary: "Calls a callback with the given arguments.", + php_manual: "function.call-user-func", +} + +/// Delegates to `check_call_user_func` which lives in the checker's callables module. +/// +/// The implementation accesses checker internals (callable targets, first-class callable +/// targets, function signatures, extern names, and the full expression type inference +/// machinery) that are only accessible from within the `types::checker::builtins` module tree. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::check_call_user_func(cx.checker, cx.args, cx.span, cx.env) +} + +/// Lowers a `call_user_func` builtin-call escape by dispatching to the shared emitter. +/// +/// This path is reached only for the rare truly-dynamic case where the static lowering in +/// `ir_lower` could not resolve the callback; it rejects the instruction with a diagnostic +/// to guide the user toward a statically resolvable form. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_call_user_func_builtin_escape(ctx, inst, "call_user_func") +} diff --git a/src/builtins/callables/call_user_func_array.rs b/src/builtins/callables/call_user_func_array.rs new file mode 100644 index 0000000000..7c7d1fd928 --- /dev/null +++ b/src/builtins/callables/call_user_func_array.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Home of the PHP `call_user_func_array` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `lazy_check: true` so the hook controls all inference: the eager `for arg in args` +//! loop is the single inference pass, matching legacy behaviour exactly. +//! - The actual check logic lives in `callables::check_call_user_func_array` (in the +//! checker module tree) because it accesses checker internals unavailable from here. +//! - `lower` is a thin wrapper over `lower_call_user_func_builtin_escape`, parameterized +//! with the canonical function name. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "call_user_func_array", + area: Callables, + params: [callback: Mixed, args: Mixed], + returns: Mixed, + check: check, + lazy_check: true, + lower: lower, + summary: "Calls a callback with an array of parameters.", + php_manual: "function.call-user-func-array", +} + +/// Delegates to `check_call_user_func_array` which lives in the checker's callables module. +/// +/// The implementation accesses checker internals (callable targets, first-class callable +/// targets, function signatures, extern names, and the full expression type inference +/// machinery) that are only accessible from within the `types::checker::builtins` module tree. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::check_call_user_func_array(cx.checker, cx.args, cx.span, cx.env) +} + +/// Lowers a `call_user_func_array` builtin-call escape by dispatching to the shared emitter. +/// +/// This path is reached only for the rare truly-dynamic case where the static lowering in +/// `ir_lower` could not resolve the callback; it rejects the instruction with a diagnostic +/// to guide the user toward a statically resolvable form. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::arrays::lower_call_user_func_builtin_escape(ctx, inst, "call_user_func_array") +} diff --git a/src/builtins/callables/class_alias.rs b/src/builtins/callables/class_alias.rs new file mode 100644 index 0000000000..2d04b0a673 --- /dev/null +++ b/src/builtins/callables/class_alias.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `class_alias` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook always errors: `class_alias()` is only supported as a top-level +//! statement with literal class names (handled by the AST-level resolver before +//! reaching the type checker). Any direct call that reaches this hook is rejected. +//! - Arguments are pre-inferred by the registry common path before the hook runs. +//! - `lower` is a thin wrapper over `types::lower_class_alias` (not parameterized). + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "class_alias", + area: Callables, + params: [class: Str, alias: Str, autoload: Bool = DefaultSpec::Bool(true)], + returns: Bool, + check: check, + lower: lower, + summary: "Creates an alias for a class.", + php_manual: "function.class-alias", +} + +/// Rejects any direct `class_alias()` call that reaches the type checker. +/// +/// AOT compilation resolves `class_alias()` at the top-level statement stage only. +/// Direct calls in other contexts are not supported and must be rejected here. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Err(CompileError::new( + cx.span, + "class_alias() is only supported as a top-level statement with literal class names", + )) +} + +/// Lowers a `class_alias` call by dispatching to the shared class-alias emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_class_alias(ctx, inst) +} diff --git a/src/builtins/callables/class_exists.rs b/src/builtins/callables/class_exists.rs new file mode 100644 index 0000000000..d04fff5990 --- /dev/null +++ b/src/builtins/callables/class_exists.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `class_exists` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook validates that the first argument is a string literal and the +//! optional autoload argument is a literal bool or int (AOT constraint). +//! - Arguments are pre-inferred by the registry common path before the hook runs. +//! - `lower` is a thin wrapper over `lower_class_like_exists` parameterized with +//! this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "class_exists", + area: Callables, + params: [class: Str, autoload: Bool = DefaultSpec::Bool(true)], + returns: Bool, + check: crate::builtins::callables::support::check_class_like_exists, + lower: lower, + summary: "Checks whether the class has been defined.", + php_manual: "function.class-exists", +} + +/// Lowers a `class_exists` call by dispatching to the shared class-like existence emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_class_like_exists(ctx, inst, "class_exists") +} diff --git a/src/builtins/callables/class_implements.rs b/src/builtins/callables/class_implements.rs new file mode 100644 index 0000000000..1bb2cc21f3 --- /dev/null +++ b/src/builtins/callables/class_implements.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `class_implements` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `lazy_check: true` so the hook infers each argument exactly once in source order, +//! matching the legacy arm. +//! - The check hook validates that the first argument is an object or string literal +//! and that the optional autoload arg is a literal bool or int. +//! - `lower` is a thin wrapper over `class_relations::lower_class_relation` parameterized +//! with this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "class_implements", + area: Callables, + params: [object_or_class: Mixed, autoload: Bool = DefaultSpec::Bool(true)], + returns: Mixed, + check: crate::builtins::callables::support::check_class_relation, + lazy_check: true, + lower: lower, + summary: "Returns the interfaces which are implemented by the given class or its parents.", + php_manual: "function.class-implements", +} + +/// Lowers a `class_implements` call by dispatching to the shared class-relation emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::class_relations::lower_class_relation( + ctx, + inst, + "class_implements", + ) +} diff --git a/src/builtins/callables/class_parents.rs b/src/builtins/callables/class_parents.rs new file mode 100644 index 0000000000..33a22054f5 --- /dev/null +++ b/src/builtins/callables/class_parents.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `class_parents` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `lazy_check: true` so the hook infers each argument exactly once in source order, +//! matching the legacy arm. +//! - The check hook validates that the first argument is an object or string literal +//! and that the optional autoload arg is a literal bool or int. +//! - `lower` is a thin wrapper over `class_relations::lower_class_relation` parameterized +//! with this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "class_parents", + area: Callables, + params: [object_or_class: Mixed, autoload: Bool = DefaultSpec::Bool(true)], + returns: Mixed, + check: crate::builtins::callables::support::check_class_relation, + lazy_check: true, + lower: lower, + summary: "Returns the parent classes of the given class.", + php_manual: "function.class-parents", +} + +/// Lowers a `class_parents` call by dispatching to the shared class-relation emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::class_relations::lower_class_relation( + ctx, + inst, + "class_parents", + ) +} diff --git a/src/builtins/callables/class_uses.rs b/src/builtins/callables/class_uses.rs new file mode 100644 index 0000000000..8215c3baec --- /dev/null +++ b/src/builtins/callables/class_uses.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `class_uses` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `lazy_check: true` so the hook infers each argument exactly once in source order, +//! matching the legacy arm. +//! - The check hook validates that the first argument is an object or string literal +//! and that the optional autoload arg is a literal bool or int. +//! - `lower` is a thin wrapper over `class_relations::lower_class_relation` parameterized +//! with this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "class_uses", + area: Callables, + params: [object_or_class: Mixed, autoload: Bool = DefaultSpec::Bool(true)], + returns: Mixed, + check: crate::builtins::callables::support::check_class_relation, + lazy_check: true, + lower: lower, + summary: "Returns the traits used by the given class.", + php_manual: "function.class-uses", +} + +/// Lowers a `class_uses` call by dispatching to the shared class-relation emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::class_relations::lower_class_relation( + ctx, + inst, + "class_uses", + ) +} diff --git a/src/builtins/callables/enum_exists.rs b/src/builtins/callables/enum_exists.rs new file mode 100644 index 0000000000..87fa79bf2f --- /dev/null +++ b/src/builtins/callables/enum_exists.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `enum_exists` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook validates that the first argument is a string literal and the +//! optional autoload argument is a literal bool or int (AOT constraint). +//! - Arguments are pre-inferred by the registry common path before the hook runs. +//! - `lower` is a thin wrapper over `lower_class_like_exists` parameterized with +//! this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "enum_exists", + area: Callables, + params: [enum: Str, autoload: Bool = DefaultSpec::Bool(true)], + returns: Bool, + check: crate::builtins::callables::support::check_class_like_exists, + lower: lower, + summary: "Checks if the enum has been defined.", + php_manual: "function.enum-exists", +} + +/// Lowers an `enum_exists` call by dispatching to the shared class-like existence emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_class_like_exists(ctx, inst, "enum_exists") +} diff --git a/src/builtins/callables/function_exists.rs b/src/builtins/callables/function_exists.rs new file mode 100644 index 0000000000..1d1d6ebfa7 --- /dev/null +++ b/src/builtins/callables/function_exists.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `function_exists` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `lazy_check: true` so the hook controls inference: it infers the single argument +//! once and, for a string-literal name, forces resolution of any not-yet-instantiated +//! declaration or variant group (matching legacy behaviour exactly). +//! - The actual check logic lives in `callables::check_function_exists` (in the checker +//! module tree) because it accesses checker internals unavailable from here. +//! - `lower` is a thin wrapper over `lower_function_exists` (not parameterized). + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "function_exists", + area: Callables, + params: [function: Str], + returns: Bool, + check: check, + lazy_check: true, + lower: lower, + summary: "Returns true if the given function has been defined.", + php_manual: "function.function-exists", +} + +/// Delegates to `check_function_exists` which lives in the checker's callables module. +/// +/// The implementation accesses checker internals (`fn_decls`, `functions`, +/// `function_variant_groups`, `canonical_function_name_folded`, `check_function_call`, +/// `ensure_function_variant_group_signature`) that are only accessible from within the +/// `types::checker::builtins` module tree. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::check_function_exists(cx.checker, cx.args, cx.span, cx.env) +} + +/// Lowers a `function_exists` call by dispatching to the shared emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_function_exists(ctx, inst) +} diff --git a/src/builtins/callables/get_class.rs b/src/builtins/callables/get_class.rs new file mode 100644 index 0000000000..256f3a6362 --- /dev/null +++ b/src/builtins/callables/get_class.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `get_class` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the registry common path infers the optional argument and +//! returns the declared `Str` type. +//! - `lower` is a thin wrapper over `types::lower_class_name_lookup` parameterized +//! with this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "get_class", + area: Callables, + params: [object: Mixed = DefaultSpec::Null], + returns: Str, + lower: lower, + summary: "Returns the name of the class of an object.", + php_manual: "function.get-class", +} + +/// Lowers a `get_class` call by dispatching to the shared class-name lookup emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_class_name_lookup(ctx, inst, "get_class") +} diff --git a/src/builtins/callables/get_declared_classes.rs b/src/builtins/callables/get_declared_classes.rs new file mode 100644 index 0000000000..a44513dba4 --- /dev/null +++ b/src/builtins/callables/get_declared_classes.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `get_declared_classes` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - Check hook returns `Array` unconditionally (zero-arg builtin). +//! - `lower` is a thin wrapper over `types::lower_get_declared_names` parameterized +//! with this builtin's name. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "get_declared_classes", + area: Callables, + params: [], + returns: Mixed, + check: crate::builtins::callables::support::check_declared_names, + lower: lower, + summary: "Returns an array of the names of the defined classes.", + php_manual: "function.get-declared-classes", +} + +/// Lowers a `get_declared_classes` call by dispatching to the shared declared-names emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_get_declared_names( + ctx, + inst, + "get_declared_classes", + ) +} diff --git a/src/builtins/callables/get_declared_interfaces.rs b/src/builtins/callables/get_declared_interfaces.rs new file mode 100644 index 0000000000..bae89f47a5 --- /dev/null +++ b/src/builtins/callables/get_declared_interfaces.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `get_declared_interfaces` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - Check hook returns `Array` unconditionally (zero-arg builtin). +//! - `lower` is a thin wrapper over `types::lower_get_declared_names` parameterized +//! with this builtin's name. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "get_declared_interfaces", + area: Callables, + params: [], + returns: Mixed, + check: crate::builtins::callables::support::check_declared_names, + lower: lower, + summary: "Returns an array of all declared interfaces.", + php_manual: "function.get-declared-interfaces", +} + +/// Lowers a `get_declared_interfaces` call by dispatching to the shared declared-names emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_get_declared_names( + ctx, + inst, + "get_declared_interfaces", + ) +} diff --git a/src/builtins/callables/get_declared_traits.rs b/src/builtins/callables/get_declared_traits.rs new file mode 100644 index 0000000000..66881525b8 --- /dev/null +++ b/src/builtins/callables/get_declared_traits.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `get_declared_traits` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - Check hook returns `Array` unconditionally (zero-arg builtin). +//! - `lower` is a thin wrapper over `types::lower_get_declared_names` parameterized +//! with this builtin's name. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "get_declared_traits", + area: Callables, + params: [], + returns: Mixed, + check: crate::builtins::callables::support::check_declared_names, + lower: lower, + summary: "Returns an array of all declared traits.", + php_manual: "function.get-declared-traits", +} + +/// Lowers a `get_declared_traits` call by dispatching to the shared declared-names emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_get_declared_names( + ctx, + inst, + "get_declared_traits", + ) +} diff --git a/src/builtins/callables/get_parent_class.rs b/src/builtins/callables/get_parent_class.rs new file mode 100644 index 0000000000..0d00a8df76 --- /dev/null +++ b/src/builtins/callables/get_parent_class.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Home of the PHP `get_parent_class` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the registry common path infers the optional argument and +//! returns the declared `Str` type. +//! - `lower` is a thin wrapper over `types::lower_class_name_lookup` parameterized +//! with this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "get_parent_class", + area: Callables, + params: [object_or_class: Mixed = DefaultSpec::Null], + returns: Str, + lower: lower, + summary: "Returns the name of the parent class of an object or class.", + php_manual: "function.get-parent-class", +} + +/// Lowers a `get_parent_class` call by dispatching to the shared class-name lookup emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_class_name_lookup( + ctx, + inst, + "get_parent_class", + ) +} diff --git a/src/builtins/callables/interface_exists.rs b/src/builtins/callables/interface_exists.rs new file mode 100644 index 0000000000..38357dd2af --- /dev/null +++ b/src/builtins/callables/interface_exists.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Home of the PHP `interface_exists` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook validates that the first argument is a string literal and the +//! optional autoload argument is a literal bool or int (AOT constraint). +//! - Arguments are pre-inferred by the registry common path before the hook runs. +//! - `lower` is a thin wrapper over `lower_class_like_exists` parameterized with +//! this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "interface_exists", + area: Callables, + params: [interface: Str, autoload: Bool = DefaultSpec::Bool(true)], + returns: Bool, + check: crate::builtins::callables::support::check_class_like_exists, + lower: lower, + summary: "Checks if the interface has been defined.", + php_manual: "function.interface-exists", +} + +/// Lowers an `interface_exists` call by dispatching to the shared class-like existence emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_class_like_exists( + ctx, + inst, + "interface_exists", + ) +} diff --git a/src/builtins/callables/is_a.rs b/src/builtins/callables/is_a.rs new file mode 100644 index 0000000000..471ffc61a9 --- /dev/null +++ b/src/builtins/callables/is_a.rs @@ -0,0 +1,33 @@ +//! Purpose: +//! Home of the PHP `is_a` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the registry common path infers all arguments and returns +//! the declared `Bool` type. +//! - `allow_string` defaults to `false` (PHP's default for `is_a`). +//! - `lower` is a thin wrapper over `types::lower_is_a_relation` parameterized +//! with this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_a", + area: Callables, + params: [object_or_class: Mixed, class: Str, allow_string: Bool = DefaultSpec::Bool(false)], + returns: Bool, + lower: lower, + summary: "Checks whether an object is of a given type or has it as one of its parents.", + php_manual: "function.is-a", +} + +/// Lowers an `is_a` call by dispatching to the shared is-a relation emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_is_a_relation(ctx, inst, "is_a") +} diff --git a/src/builtins/callables/is_subclass_of.rs b/src/builtins/callables/is_subclass_of.rs new file mode 100644 index 0000000000..9f8d094ac5 --- /dev/null +++ b/src/builtins/callables/is_subclass_of.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Home of the PHP `is_subclass_of` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the registry common path infers all arguments and returns +//! the declared `Bool` type. +//! - `allow_string` defaults to `true` (PHP's default for `is_subclass_of`). +//! - `lower` is a thin wrapper over `types::lower_is_a_relation` parameterized +//! with this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_subclass_of", + area: Callables, + params: [object_or_class: Mixed, class: Str, allow_string: Bool = DefaultSpec::Bool(true)], + returns: Bool, + lower: lower, + summary: "Checks if the object has a given class as one of its parents or implements it.", + php_manual: "function.is-subclass-of", +} + +/// Lowers an `is_subclass_of` call by dispatching to the shared is-a relation emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_is_a_relation( + ctx, + inst, + "is_subclass_of", + ) +} diff --git a/src/builtins/callables/mod.rs b/src/builtins/callables/mod.rs new file mode 100644 index 0000000000..5d3a03721a --- /dev/null +++ b/src/builtins/callables/mod.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Groups all `callables`-area builtin homes into this module so the registry can +//! collect them in one place. Each submodule declares exactly one builtin via +//! `builtin!` and provides its lowering hook. +//! +//! Called from: +//! - `crate::builtins` (`mod callables;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - `support` holds shared check hooks used by multiple homes to avoid duplication. +//! - Group A: no check hook (registry common path handles inference and return type). +//! - Group B: shared `check_declared_names` hook (returns `Array`). +//! - Group C: shared `check_class_like_exists` hook (requires string-literal first arg). +//! - Group E: shared `check_class_relation` hook with `lazy_check: true`. +//! - `class_alias`: always-error local check hook. +//! - `function_exists`: delegates to `callables::check_function_exists` with `lazy_check: true`. + +pub(crate) mod support; + +// Group A — no check hook +pub mod get_class; +pub mod get_parent_class; +pub mod is_a; +pub mod is_subclass_of; + +// Group B — check_declared_names +pub mod get_declared_classes; +pub mod get_declared_interfaces; +pub mod get_declared_traits; + +// Group C — check_class_like_exists +pub mod class_exists; +pub mod enum_exists; +pub mod interface_exists; +pub mod trait_exists; + +// Group E — check_class_relation + lazy_check +pub mod class_implements; +pub mod class_parents; +pub mod class_uses; + +// Callables batch B — lazy_check, delegates to checker::builtins::callables +pub mod call_user_func; +pub mod call_user_func_array; + +// Singletons +pub mod class_alias; +pub mod function_exists; +pub mod preg_replace_callback; diff --git a/src/builtins/callables/preg_replace_callback.rs b/src/builtins/callables/preg_replace_callback.rs new file mode 100644 index 0000000000..de1676801e --- /dev/null +++ b/src/builtins/callables/preg_replace_callback.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Home of the PHP `preg_replace_callback` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `lazy_check: true` is required: `contextual_closure_sig` injects `array` for the +//! closure's `$matches` parameter BEFORE the closure body is inferred. Pre-inference would +//! mistype the closure. The check hook controls argument inference order. +//! - The actual check logic lives in the checker submodule +//! `crate::types::checker::builtins::callables::preg_replace_callback::check`, which also +//! enforces the arity guard independently (needed for the first-class-callable path). +//! - `lower` is a thin wrapper over `lower_preg_replace_callback` (not parameterized). + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "preg_replace_callback", + area: Callables, + params: [pattern: Str, callback: Mixed, subject: Str], + returns: Str, + check: check, + lazy_check: true, + lower: lower, + summary: "Performs a regular expression search and replace using a callback.", + php_manual: "function.preg-replace-callback", +} + +/// Delegates to `check_preg_replace_callback_first_class_call`, which controls closure +/// argument inference order and injects `array` as the contextual type for +/// `$matches` before the closure body is inferred. +/// +/// That function lives in the checker's callables module (re-exported as `pub(crate)` from +/// `types::checker::builtins`) and is already used for the first-class-callable path; the +/// home simply reuses it so both paths share the same implementation. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::check_preg_replace_callback_first_class_call( + cx.checker, + cx.args, + cx.span, + cx.env, + ) +} + +/// Lowers a `preg_replace_callback` call by dispatching to the shared EIR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::regex::lower_preg_replace_callback(ctx, inst) +} diff --git a/src/builtins/callables/support.rs b/src/builtins/callables/support.rs new file mode 100644 index 0000000000..52655f991f --- /dev/null +++ b/src/builtins/callables/support.rs @@ -0,0 +1,91 @@ +//! Purpose: +//! Shared type-check hooks for the callables-area class-reflection builtin homes. +//! Provides the common validation logic used by multiple homes to avoid duplication. +//! +//! Called from: +//! - `crate::builtins::callables::*` homes that set `check:` to one of these functions. +//! +//! Key details: +//! - Each hook receives a pre-populated `BuiltinCheckCtx`; for non-lazy homes args are +//! already inferred by the registry common path before the hook runs. +//! - `check_class_like_exists` inspects `.kind` only (no infer) — the common path already +//! inferred every arg before this hook is called. +//! - `check_class_relation` homes use `lazy_check: true`, so the hook performs its own +//! inference in source order (matching the legacy arm). +//! - `check_declared_names` takes no args and returns `Array` unconditionally. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +/// Validates `class_exists` / `interface_exists` / `trait_exists` / `enum_exists` arguments. +/// +/// Requires that the first argument is a string literal and, if present, the second argument +/// is a literal bool or int (the autoload flag). Returns `Bool` on success. +/// Arguments are pre-inferred by the registry common path before this hook runs. +pub(crate) fn check_class_like_exists(cx: &mut BuiltinCheckCtx) -> Result { + if !matches!(cx.args[0].kind, ExprKind::StringLiteral(_)) { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be a string literal in AOT mode", cx.name), + )); + } + if let Some(autoload_arg) = cx.args.get(1) { + if !matches!( + autoload_arg.kind, + ExprKind::BoolLiteral(_) | ExprKind::IntLiteral(_) + ) { + return Err(CompileError::new( + cx.span, + &format!("{}() autoload argument must be a literal bool or int in AOT mode", cx.name), + )); + } + } + Ok(PhpType::Bool) +} + +/// Validates `class_implements` / `class_parents` / `class_uses` arguments. +/// +/// Infers the first argument and requires it to be an object or string literal. +/// If present, infers and validates the second argument (autoload flag) as a literal bool or int. +/// Returns the union `array|bool` used by the PHP class-relation builtins. +/// This hook is called with `lazy_check: true` so inference happens here, not in the common path. +pub(crate) fn check_class_relation(cx: &mut BuiltinCheckCtx) -> Result { + let first_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(first_ty, PhpType::Object(_)) + && !matches!(cx.args[0].kind, ExprKind::StringLiteral(_)) + { + return Err(CompileError::new( + cx.span, + &format!("{}() first argument must be an object or string literal in AOT mode", cx.name), + )); + } + if let Some(autoload_arg) = cx.args.get(1) { + cx.checker.infer_type(autoload_arg, cx.env)?; + if !matches!( + autoload_arg.kind, + ExprKind::BoolLiteral(_) | ExprKind::IntLiteral(_) + ) { + return Err(CompileError::new( + cx.span, + &format!("{}() autoload argument must be a literal bool or int in AOT mode", cx.name), + )); + } + } + Ok(PhpType::Union(vec![ + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + }, + PhpType::Bool, + ])) +} + +/// Returns `Array` for the zero-argument declared-names builtins. +/// +/// The hook ignores its context because these builtins take no arguments; the registry +/// common path enforces arity = 0 before this hook runs. +pub(crate) fn check_declared_names(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} diff --git a/src/builtins/callables/trait_exists.rs b/src/builtins/callables/trait_exists.rs new file mode 100644 index 0000000000..9cc0a6039b --- /dev/null +++ b/src/builtins/callables/trait_exists.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `trait_exists` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook via support), +//! and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook validates that the first argument is a string literal and the +//! optional autoload argument is a literal bool or int (AOT constraint). +//! - Arguments are pre-inferred by the registry common path before the hook runs. +//! - `lower` is a thin wrapper over `lower_class_like_exists` parameterized with +//! this builtin's name. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "trait_exists", + area: Callables, + params: [trait: Str, autoload: Bool = DefaultSpec::Bool(true)], + returns: Bool, + check: crate::builtins::callables::support::check_class_like_exists, + lower: lower, + summary: "Checks whether the trait exists.", + php_manual: "function.trait-exists", +} + +/// Lowers a `trait_exists` call by dispatching to the shared class-like existence emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_class_like_exists(ctx, inst, "trait_exists") +} diff --git a/src/builtins/convert.rs b/src/builtins/convert.rs new file mode 100644 index 0000000000..c14289e70a --- /dev/null +++ b/src/builtins/convert.rs @@ -0,0 +1,184 @@ +//! Purpose: +//! Conversion helpers that bridge `BuiltinSpec` fields (`TypeSpec`, `DefaultSpec`) +//! into the compiler's rich runtime types (`PhpType`, `Expr`) during the migration period. +//! +//! Called from: +//! - `crate::builtins::registry` when populating the legacy dispatch tables. +//! +//! Key details: +//! - `type_spec_to_php` must produce byte-for-byte equivalent `PhpType` values to the +//! legacy hand-coded type annotations in `src/types/signatures.rs` and the builtin +//! type-checker files. +//! - `default_spec_to_expr` must produce `Expr` nodes with the same `ExprKind` and +//! `Span::dummy()` span as the legacy literal helpers (`null_lit`, `int_lit`, etc.) +//! in `src/types/signatures.rs`. +//! - Only variants that exist in `TypeSpec`/`DefaultSpec` are handled; no speculative +//! mappings are added (YAGNI). +//! - This module is intentionally private (`mod convert;` without `pub`) and will +//! shrink as each legacy dispatch point is replaced by direct registry queries. + +// Dead-code warnings are expected during the multi-task migration before the +// registry wires these helpers into active dispatch paths. +#![allow(dead_code)] + +use crate::builtins::spec::{DefaultSpec, TypeSpec}; +use crate::parser::ast::{Expr, ExprKind}; +use crate::span::Span; +use crate::types::PhpType; + +/// Converts a `TypeSpec` descriptor into the corresponding `PhpType`. +/// +/// The mapping is one-to-one for scalar variants. Compound variants (`ArrayOf`, +/// `AssocOf`, `Union`) recurse so nested types are correctly translated. +/// `Null` maps to `PhpType::Void` because `Void` is the null sentinel used by the +/// runtime (stored as 8 bytes). `Void` maps to `PhpType::Void` for functions that +/// do not return a value. +pub fn type_spec_to_php(ty: &TypeSpec) -> PhpType { + match ty { + TypeSpec::Int => PhpType::Int, + TypeSpec::Float => PhpType::Float, + TypeSpec::Str => PhpType::Str, + TypeSpec::Bool => PhpType::Bool, + TypeSpec::Mixed => PhpType::Mixed, + TypeSpec::Null => PhpType::Void, + TypeSpec::Void => PhpType::Void, + TypeSpec::ArrayOf(elem) => PhpType::Array(Box::new(type_spec_to_php(elem))), + TypeSpec::AssocOf(val) => PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(type_spec_to_php(val)), + }, + TypeSpec::Union(members) => { + PhpType::Union(members.iter().map(type_spec_to_php).collect()) + } + } +} + +/// Converts a `DefaultSpec` descriptor into the `Expr` node the legacy +/// `src/types/signatures.rs` literal helpers would produce. +/// +/// Every variant uses `Span::dummy()` to match the convention used by `null_lit()`, +/// `int_lit()`, `bool_lit()`, `string_lit()`, and the inline array/float literals +/// in the legacy signature table. The result is structurally identical to what +/// those helpers return. +pub fn default_spec_to_expr(d: &DefaultSpec) -> Expr { + match d { + DefaultSpec::Null => Expr::new(ExprKind::Null, Span::dummy()), + DefaultSpec::Int(n) => Expr::new(ExprKind::IntLiteral(*n), Span::dummy()), + DefaultSpec::Bool(b) => Expr::new(ExprKind::BoolLiteral(*b), Span::dummy()), + DefaultSpec::Float(f) => Expr::new(ExprKind::FloatLiteral(*f), Span::dummy()), + DefaultSpec::Str(s) => Expr::new(ExprKind::StringLiteral(s.to_string()), Span::dummy()), + DefaultSpec::IntMax => Expr::new(ExprKind::IntLiteral(i64::MAX), Span::dummy()), + DefaultSpec::IntMin => Expr::new(ExprKind::IntLiteral(i64::MIN), Span::dummy()), + DefaultSpec::EmptyArray => Expr::new(ExprKind::ArrayLiteral(Vec::new()), Span::dummy()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builtins::spec::{DefaultSpec, TypeSpec}; + use crate::types::PhpType; + + /// Verifies scalar TypeSpec maps to the matching PhpType. + #[test] + fn scalar_type_spec_converts() { + assert_eq!(type_spec_to_php(&TypeSpec::Int), PhpType::Int); + assert_eq!(type_spec_to_php(&TypeSpec::Str), PhpType::Str); + } + + /// Verifies a null default lowers to the same Expr the legacy `null_lit()` helper produces. + #[test] + fn null_default_converts() { + let e = default_spec_to_expr(&DefaultSpec::Null); + assert!(matches!(e.kind, crate::parser::ast::ExprKind::Null)); + } + + /// Verifies remaining scalar TypeSpec variants map to their PhpType equivalents. + #[test] + fn all_scalar_type_specs_convert() { + assert_eq!(type_spec_to_php(&TypeSpec::Float), PhpType::Float); + assert_eq!(type_spec_to_php(&TypeSpec::Bool), PhpType::Bool); + assert_eq!(type_spec_to_php(&TypeSpec::Mixed), PhpType::Mixed); + assert_eq!(type_spec_to_php(&TypeSpec::Void), PhpType::Void); + assert_eq!(type_spec_to_php(&TypeSpec::Null), PhpType::Void); + } + + /// Verifies ArrayOf TypeSpec recurses correctly into PhpType::Array. + #[test] + fn array_of_type_spec_converts() { + assert_eq!( + type_spec_to_php(&TypeSpec::ArrayOf(&TypeSpec::Int)), + PhpType::Array(Box::new(PhpType::Int)) + ); + } + + /// Verifies AssocOf TypeSpec maps to PhpType::AssocArray with Str key and converted value. + #[test] + fn assoc_of_type_spec_converts() { + assert_eq!( + type_spec_to_php(&TypeSpec::AssocOf(&TypeSpec::Str)), + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + } + ); + } + + /// Verifies Union TypeSpec maps to PhpType::Union with all members converted. + #[test] + fn union_type_spec_converts() { + assert_eq!( + type_spec_to_php(&TypeSpec::Union(&[TypeSpec::Int, TypeSpec::Bool])), + PhpType::Union(vec![PhpType::Int, PhpType::Bool]) + ); + } + + /// Verifies integer DefaultSpec produces an IntLiteral expression matching int_lit(). + #[test] + fn int_default_converts() { + let e = default_spec_to_expr(&DefaultSpec::Int(42)); + assert!(matches!(e.kind, ExprKind::IntLiteral(42))); + } + + /// Verifies boolean DefaultSpec produces a BoolLiteral expression matching bool_lit(). + #[test] + fn bool_default_converts() { + let e = default_spec_to_expr(&DefaultSpec::Bool(true)); + assert!(matches!(e.kind, ExprKind::BoolLiteral(true))); + } + + /// Verifies float DefaultSpec produces a FloatLiteral expression. + #[test] + fn float_default_converts() { + let e = default_spec_to_expr(&DefaultSpec::Float(1.5)); + assert!(matches!(e.kind, ExprKind::FloatLiteral(v) if v == 1.5)); + } + + /// Verifies string DefaultSpec produces a StringLiteral expression matching string_lit(). + #[test] + fn str_default_converts() { + let e = default_spec_to_expr(&DefaultSpec::Str("hello")); + assert!(matches!(e.kind, ExprKind::StringLiteral(ref s) if s == "hello")); + } + + /// Verifies IntMax DefaultSpec produces IntLiteral(i64::MAX), matching the PHP_INT_MAX literal. + #[test] + fn int_max_default_converts() { + let e = default_spec_to_expr(&DefaultSpec::IntMax); + assert!(matches!(e.kind, ExprKind::IntLiteral(i64::MAX))); + } + + /// Verifies IntMin DefaultSpec produces IntLiteral(i64::MIN), matching the PHP_INT_MIN literal. + #[test] + fn int_min_default_converts() { + let e = default_spec_to_expr(&DefaultSpec::IntMin); + assert!(matches!(e.kind, ExprKind::IntLiteral(i64::MIN))); + } + + /// Verifies EmptyArray DefaultSpec produces an empty ArrayLiteral expression. + #[test] + fn empty_array_default_converts() { + let e = default_spec_to_expr(&DefaultSpec::EmptyArray); + assert!(matches!(e.kind, ExprKind::ArrayLiteral(ref v) if v.is_empty())); + } +} diff --git a/src/builtins/docs.rs b/src/builtins/docs.rs new file mode 100644 index 0000000000..c0522d0b12 --- /dev/null +++ b/src/builtins/docs.rs @@ -0,0 +1,180 @@ +//! Purpose: +//! Serialises the single-source builtin registry to a JSON array for documentation tooling. +//! 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()`. +//! +//! Key details: +//! - Uses `crate::builtins::registry::{names, lookup}` so `inventory::iter` runs in the same +//! crate that submitted the `builtin!` entries — required for the iterator to see all specs. +//! - `TypeSpec` rendering is recursive (handles `ArrayOf`/`AssocOf`/`Union` nesting). +//! - Builtins with `internal: true` are excluded from the export. +//! - `#![allow(dead_code)]` suppresses warnings when the module is compiled in the context of +//! the `elephc` binary (which never calls `export_builtins_json`); all items here are live +//! from the `gen_builtins` binary's perspective. +#![allow(dead_code)] + +use crate::builtins::registry::{lookup, names}; +use crate::builtins::spec::{Area, DefaultSpec, TypeSpec}; +use serde_json::{json, Value}; + +/// Renders a `TypeSpec` as a PHP-style type string for documentation JSON. +fn type_spec_str(ty: &TypeSpec) -> String { + match ty { + TypeSpec::Int => "int".to_string(), + TypeSpec::Float => "float".to_string(), + TypeSpec::Str => "string".to_string(), + TypeSpec::Bool => "bool".to_string(), + TypeSpec::Mixed => "mixed".to_string(), + TypeSpec::Null => "null".to_string(), + TypeSpec::Void => "void".to_string(), + TypeSpec::ArrayOf(inner) => format!("array<{}>", type_spec_str(inner)), + TypeSpec::AssocOf(inner) => format!("array", type_spec_str(inner)), + TypeSpec::Union(members) => members + .iter() + .map(type_spec_str) + .collect::>() + .join("|"), + } +} + +/// Maps a builtin `Area` to its lowercase documentation category name. +fn area_str(area: Area) -> &'static str { + match area { + Area::String => "string", + Area::Array => "array", + Area::Math => "math", + Area::Io => "io", + Area::System => "system", + Area::Types => "types", + Area::Callables => "callables", + Area::Spl => "spl", + Area::Pointers => "pointers", + Area::Internal => "internal", + } +} + +/// Renders a parameter `DefaultSpec` as its documentation JSON value. +fn default_spec_json(default: &DefaultSpec) -> Value { + match default { + DefaultSpec::Null => Value::Null, + DefaultSpec::Int(v) => json!(v), + DefaultSpec::Bool(v) => json!(v), + DefaultSpec::Float(v) => json!(v), + DefaultSpec::Str(v) => json!(v), + DefaultSpec::IntMax => json!("PHP_INT_MAX"), + DefaultSpec::IntMin => json!("PHP_INT_MIN"), + DefaultSpec::EmptyArray => json!([]), + } +} + +/// 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 +/// builtin (see [`build_json`] for the object shape). Consumed by the `gen_builtins` binary for +/// documentation generation. +pub fn export_builtins_json() -> Value { + build_json(false) +} + +/// Builds the documentation JSON array for every registered builtin, INCLUDING `internal` ones. +/// +/// Same object shape as [`export_builtins_json`]; used by the docs pipeline, which renders +/// compiler-internals pages for internal `__elephc_*` helpers as well as the PHP-visible surface. +pub fn export_builtins_json_all() -> Value { + build_json(true) +} + +/// Builds the builtin documentation JSON array, optionally including `internal` builtins. +/// +/// Iterates the registry in sorted name order and emits one object per builtin with its area, +/// `internal` flag, parameters (name/type/by_ref/optional/default), variadic name, arity overrides, +/// return type, summary, examples, PHP-manual fragment, and deprecation. When `include_internal` is +/// false, builtins flagged `internal` are skipped. +fn build_json(include_internal: bool) -> Value { + let mut out: Vec = Vec::new(); + for name in names() { + let Some(def) = lookup(name) else { continue }; + let spec = def.spec; + if spec.internal && !include_internal { + continue; + } + let params: Vec = spec + .params + .iter() + .map(|p| { + json!({ + "name": p.name, + "type": type_spec_str(&p.ty), + "by_ref": p.by_ref, + // `optional` disambiguates a required param (no default) from an + // optional param whose default value is literally `null`; both would + // otherwise render as JSON `null` under the `default` key. + "optional": p.default.is_some(), + "default": p.default.as_ref().map(default_spec_json).unwrap_or(Value::Null), + }) + }) + .collect(); + out.push(json!({ + "name": spec.name, + "area": area_str(spec.area), + "internal": spec.internal, + "params": params, + "variadic": spec.variadic, + "returns": type_spec_str(&spec.returns), + "by_ref_return": spec.by_ref_return, + "min_args": spec.min_args, + "max_args": spec.max_args, + "arity_error": spec.arity_error, + "summary": spec.summary, + "examples": spec.examples, + "php_manual": spec.php_manual, + "deprecated": spec.deprecation, + })); + } + Value::Array(out) +} + +#[cfg(test)] +mod tests { + /// Verifies the exporter emits a non-empty array and a known builtin (`strlen`) with its + /// documented shape (required string param, int return, non-internal). + #[test] + fn export_contains_strlen_with_expected_shape() { + let v = super::export_builtins_json(); + let arr = v.as_array().expect("top-level array"); + assert!(!arr.is_empty()); + let strlen = arr + .iter() + .find(|e| e["name"] == "strlen") + .expect("strlen present"); + assert_eq!(strlen["area"], "string"); + assert_eq!(strlen["returns"], "int"); + assert_eq!(strlen["params"][0]["name"], "string"); + // `strlen`'s sole param is required, so `optional` must be false (it has no default). + assert_eq!(strlen["params"][0]["optional"], false); + // No internal builtins leak into the docs export. + assert!(arr.iter().all(|e| e["name"].as_str().map_or(false, |n| !n.starts_with("__elephc_")))); + // The default export carries the `internal` flag, always false here. + assert_eq!(strlen["internal"], false); + } + + /// Verifies the include-internal export is a strict superset of the PHP-visible one and + /// surfaces at least one `internal` builtin flagged `internal: true`. + #[test] + fn export_all_includes_internal_builtins() { + let visible = super::export_builtins_json(); + let all = super::export_builtins_json_all(); + let visible_len = visible.as_array().expect("array").len(); + let all_arr = all.as_array().expect("array"); + assert!(all_arr.len() >= visible_len); + // Every builtin flagged internal is present only in the include-internal export. + assert!(all_arr.iter().any(|e| e["internal"] == true)); + assert!(visible + .as_array() + .unwrap() + .iter() + .all(|e| e["internal"] == false)); + } +} diff --git a/src/builtins/io/__elephc_phar_bzip2_archive.rs b/src/builtins/io/__elephc_phar_bzip2_archive.rs new file mode 100644 index 0000000000..ca6221f50f --- /dev/null +++ b/src/builtins/io/__elephc_phar_bzip2_archive.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_bzip2_archive` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_bzip2_archive` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_bzip2_archive", + area: Io, + params: [src: Str], + returns: Str, + check: check, + lower: lower, + summary: "Compresses a PHAR archive using bzip2.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Str` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Str) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_bzip2_archive(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_decompress_archive.rs b/src/builtins/io/__elephc_phar_decompress_archive.rs new file mode 100644 index 0000000000..e4193bd572 --- /dev/null +++ b/src/builtins/io/__elephc_phar_decompress_archive.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_decompress_archive` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_decompress_archive` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_decompress_archive", + area: Io, + params: [src: Str], + returns: Str, + check: check, + lower: lower, + summary: "Decompresses a PHAR archive to a new path.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Str` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Str) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_decompress_archive(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_get_file_metadata.rs b/src/builtins/io/__elephc_phar_get_file_metadata.rs new file mode 100644 index 0000000000..015c715a3e --- /dev/null +++ b/src/builtins/io/__elephc_phar_get_file_metadata.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_get_file_metadata` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_get_file_metadata` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_get_file_metadata", + area: Io, + params: [url: Str], + returns: Str, + check: check, + lower: lower, + summary: "Reads the serialized per-file metadata blob.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Str` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Str) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_get_file_metadata(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_get_metadata.rs b/src/builtins/io/__elephc_phar_get_metadata.rs new file mode 100644 index 0000000000..982e2d57f7 --- /dev/null +++ b/src/builtins/io/__elephc_phar_get_metadata.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_get_metadata` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_get_metadata` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_get_metadata", + area: Io, + params: [filename: Str], + returns: Str, + check: check, + lower: lower, + summary: "Reads the serialized PHAR-level metadata blob.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Str` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Str) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_get_metadata(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_get_signature_hash.rs b/src/builtins/io/__elephc_phar_get_signature_hash.rs new file mode 100644 index 0000000000..c3e7520337 --- /dev/null +++ b/src/builtins/io/__elephc_phar_get_signature_hash.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_get_signature_hash` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_get_signature_hash` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_get_signature_hash", + area: Io, + params: [path: Str], + returns: Str, + check: check, + lower: lower, + summary: "Returns the PHAR signature hash bytes.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Str` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Str) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_get_signature_hash(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_get_signature_type.rs b/src/builtins/io/__elephc_phar_get_signature_type.rs new file mode 100644 index 0000000000..b93e597e69 --- /dev/null +++ b/src/builtins/io/__elephc_phar_get_signature_type.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_get_signature_type` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_get_signature_type` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_get_signature_type", + area: Io, + params: [path: Str], + returns: Str, + check: check, + lower: lower, + summary: "Returns the PHAR signature algorithm name.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Str` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Str) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_get_signature_type(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_get_stub.rs b/src/builtins/io/__elephc_phar_get_stub.rs new file mode 100644 index 0000000000..770813eeaf --- /dev/null +++ b/src/builtins/io/__elephc_phar_get_stub.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_get_stub` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_get_stub` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_get_stub", + area: Io, + params: [filename: Str], + returns: Str, + check: check, + lower: lower, + summary: "Reads the PHAR stub script.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Str` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Str) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_get_stub(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_gzip_archive.rs b/src/builtins/io/__elephc_phar_gzip_archive.rs new file mode 100644 index 0000000000..c08a06621e --- /dev/null +++ b/src/builtins/io/__elephc_phar_gzip_archive.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_gzip_archive` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_gzip_archive` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_gzip_archive", + area: Io, + params: [src: Str], + returns: Str, + check: check, + lower: lower, + summary: "Compresses a PHAR archive using gzip.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Str` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Str) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_gzip_archive(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_list_entries.rs b/src/builtins/io/__elephc_phar_list_entries.rs new file mode 100644 index 0000000000..cb0591c4ee --- /dev/null +++ b/src/builtins/io/__elephc_phar_list_entries.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_list_entries` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_list_entries` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_list_entries", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Lists the file paths within a PHAR archive.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns `Array` for the entry path list. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_list_entries(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_set_compression.rs b/src/builtins/io/__elephc_phar_set_compression.rs new file mode 100644 index 0000000000..19c0756845 --- /dev/null +++ b/src/builtins/io/__elephc_phar_set_compression.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_set_compression` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_set_compression` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_set_compression", + area: Io, + params: [filename: Str, compression: Int], + returns: Bool, + check: check, + lower: lower, + summary: "Sets the compression algorithm for a PHAR archive.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Bool` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Bool) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_set_compression(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_set_file_metadata.rs b/src/builtins/io/__elephc_phar_set_file_metadata.rs new file mode 100644 index 0000000000..23c083157f --- /dev/null +++ b/src/builtins/io/__elephc_phar_set_file_metadata.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_set_file_metadata` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_set_file_metadata` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_set_file_metadata", + area: Io, + params: [url: Str, metadata: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Writes the serialized per-file metadata blob.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Bool` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Bool) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_set_file_metadata(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_set_metadata.rs b/src/builtins/io/__elephc_phar_set_metadata.rs new file mode 100644 index 0000000000..d8fe7ec478 --- /dev/null +++ b/src/builtins/io/__elephc_phar_set_metadata.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_set_metadata` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_set_metadata` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_set_metadata", + area: Io, + params: [filename: Str, metadata: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Writes the serialized PHAR-level metadata blob.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Bool` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Bool) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_set_metadata(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_set_stub.rs b/src/builtins/io/__elephc_phar_set_stub.rs new file mode 100644 index 0000000000..c0083b9335 --- /dev/null +++ b/src/builtins/io/__elephc_phar_set_stub.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_set_stub` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_set_stub` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_set_stub", + area: Io, + params: [filename: Str, stub: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Writes the PHAR stub script.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Bool` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Bool) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_set_stub(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_set_zip_password.rs b/src/builtins/io/__elephc_phar_set_zip_password.rs new file mode 100644 index 0000000000..aeac30b778 --- /dev/null +++ b/src/builtins/io/__elephc_phar_set_zip_password.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_set_zip_password` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_set_zip_password` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_set_zip_password", + area: Io, + params: [password: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Sets the encryption password for a PHAR ZIP archive.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Bool` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Bool) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_set_zip_password(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_sign_hash.rs b/src/builtins/io/__elephc_phar_sign_hash.rs new file mode 100644 index 0000000000..95d48f426d --- /dev/null +++ b/src/builtins/io/__elephc_phar_sign_hash.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_sign_hash` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_sign_hash` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_sign_hash", + area: Io, + params: [path: Str, algo: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Signs a PHAR archive with the given hash algorithm.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Bool` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Bool) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_sign_hash(ctx, inst) +} diff --git a/src/builtins/io/__elephc_phar_sign_openssl.rs b/src/builtins/io/__elephc_phar_sign_openssl.rs new file mode 100644 index 0000000000..0a4819dee9 --- /dev/null +++ b/src/builtins/io/__elephc_phar_sign_openssl.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the internal `__elephc_phar_sign_openssl` PHAR intrinsic: its declaration, +//! type-check hook, and lowering. Compiler-synthesized; not PHP-visible. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `internal: true` keeps it out of PHP-visible builtin name sets and +//! `function_exists()`; it is reachable only through compiler-generated PHAR bodies. +//! - The `check` hook links the `elephc_phar` bridge library (a mandatory side effect); +//! argument inference is handled by the registry common path, so the hook does not +//! call `infer_type`. +//! - `lower` is a thin wrapper over `io::lower_elephc_phar_sign_openssl` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "__elephc_phar_sign_openssl", + area: Io, + params: [path: Str, key: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Signs a PHAR archive using an OpenSSL private key.", + internal: true, +} + +/// Links the `elephc_phar` bridge and returns the intrinsic's `Bool` result type. +/// Argument inference is performed by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_phar"); + Ok(PhpType::Bool) +} + +/// Lowers the call by dispatching to the shared io PHAR emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_elephc_phar_sign_openssl(ctx, inst) +} diff --git a/src/builtins/io/basename.rs b/src/builtins/io/basename.rs new file mode 100644 index 0000000000..fc8cb83c8e --- /dev/null +++ b/src/builtins/io/basename.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `basename` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `basename` is a pure-data builtin whose return type +//! (`Str`) is fully determined by its declaration. The registry common path +//! infers arguments and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_basename` in the EIR backend. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "basename", + area: Io, + params: [path: Str, suffix: Str = DefaultSpec::Str("")], + returns: Str, + lower: lower, + summary: "Returns the trailing name component of a path.", + php_manual: "function.basename", +} + +/// Lowers a `basename` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_basename(ctx, inst) +} diff --git a/src/builtins/io/chdir.rs b/src/builtins/io/chdir.rs new file mode 100644 index 0000000000..d97eb93a83 --- /dev/null +++ b/src/builtins/io/chdir.rs @@ -0,0 +1,33 @@ +//! Purpose: +//! Home of the PHP `chdir` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `chdir` is a pure-data builtin whose `Bool` return type is +//! fully determined by its declaration. Unlike `unlink`, `chdir` has no PHAR +//! side effect, so no library-linking check hook is required. The registry +//! common path infers the argument and enforces the exactly-1-argument arity +//! before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_chdir` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "chdir", + area: Io, + params: [directory: Str], + returns: Bool, + lower: lower, + summary: "Changes the current directory.", + php_manual: "function.chdir", +} + +/// Lowers a `chdir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_chdir(ctx, inst) +} diff --git a/src/builtins/io/chgrp.rs b/src/builtins/io/chgrp.rs new file mode 100644 index 0000000000..d08eb27cc1 --- /dev/null +++ b/src/builtins/io/chgrp.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `chgrp` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Bool` and requires the `group` argument to be `Int` or `Str` +//! (a numeric GID or a group name), emitting the diagnostic at that argument's span. +//! - `lower` is a thin wrapper over `io::lower_chgrp` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "chgrp", + area: Io, + params: [filename: Str, group: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Changes file group.", + php_manual: "function.chgrp", +} + +/// Returns `Bool`, rejecting a `group` argument that is neither `Int` nor `Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let principal_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(principal_ty, PhpType::Int | PhpType::Str) { + return Err(CompileError::new( + cx.args[1].span, + &format!("{}() owner/group must be int or string", cx.name), + )); + } + Ok(PhpType::Bool) +} + +/// Lowers a `chgrp` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_chgrp(ctx, inst) +} diff --git a/src/builtins/io/chmod.rs b/src/builtins/io/chmod.rs new file mode 100644 index 0000000000..4e5d44fec5 --- /dev/null +++ b/src/builtins/io/chmod.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `chmod` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Bool` and requires the `permissions` argument to be `Int`, +//! emitting the diagnostic at the mode argument's span. +//! - `lower` is a thin wrapper over `io::lower_chmod` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "chmod", + area: Io, + params: [filename: Str, permissions: Int], + returns: Bool, + check: check, + lower: lower, + summary: "Changes file mode.", + php_manual: "function.chmod", +} + +/// Returns `Bool`, rejecting a non-`Int` `permissions` argument at its own span. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let mode_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if mode_ty != PhpType::Int { + return Err(CompileError::new(cx.args[1].span, "chmod() mode must be int")); + } + Ok(PhpType::Bool) +} + +/// Lowers a `chmod` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_chmod(ctx, inst) +} diff --git a/src/builtins/io/chown.rs b/src/builtins/io/chown.rs new file mode 100644 index 0000000000..4a14037018 --- /dev/null +++ b/src/builtins/io/chown.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `chown` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Bool` and requires the `user` argument to be `Int` or `Str` +//! (a numeric UID or a user name), emitting the diagnostic at that argument's span. +//! - `lower` is a thin wrapper over `io::lower_chown` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "chown", + area: Io, + params: [filename: Str, user: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Changes file owner.", + php_manual: "function.chown", +} + +/// Returns `Bool`, rejecting a `user` argument that is neither `Int` nor `Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let principal_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(principal_ty, PhpType::Int | PhpType::Str) { + return Err(CompileError::new( + cx.args[1].span, + &format!("{}() owner/group must be int or string", cx.name), + )); + } + Ok(PhpType::Bool) +} + +/// Lowers a `chown` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_chown(ctx, inst) +} diff --git a/src/builtins/io/clearstatcache.rs b/src/builtins/io/clearstatcache.rs new file mode 100644 index 0000000000..a30b088246 --- /dev/null +++ b/src/builtins/io/clearstatcache.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Home of the PHP `clearstatcache` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `clearstatcache` is a pure-data builtin whose return +//! type (`Void`) is fully determined by its declaration. The registry common path +//! infers arguments and enforces arity before falling back to `returns`. +//! - PHP accepts up to 2 optional arguments; elephc has no stat cache but accepts +//! and ignores them (matching legacy behavior). +//! - `lower` is a thin wrapper over `io::lower_clearstatcache` in the EIR backend. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "clearstatcache", + area: Io, + params: [ + clear_realpath_cache: Bool = DefaultSpec::Bool(false), + filename: Str = DefaultSpec::Str("") + ], + returns: Void, + lower: lower, + summary: "Clears file status cache.", + php_manual: "function.clearstatcache", +} + +/// Lowers a `clearstatcache` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_clearstatcache(ctx, inst) +} diff --git a/src/builtins/io/closedir.rs b/src/builtins/io/closedir.rs new file mode 100644 index 0000000000..721322a628 --- /dev/null +++ b/src/builtins/io/closedir.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `closedir` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `dir_handle` argument is a stream resource and returns `Void`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_closedir` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "closedir", + area: Io, + params: [dir_handle: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Closes directory handle.", + php_manual: "function.closedir", +} + +/// Validates the directory handle is a stream resource and returns `Void`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Void) +} + +/// Lowers a `closedir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_closedir(ctx, inst) +} diff --git a/src/builtins/io/copy.rs b/src/builtins/io/copy.rs new file mode 100644 index 0000000000..bca8976d6c --- /dev/null +++ b/src/builtins/io/copy.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `copy` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `copy` is a pure-data builtin whose `Bool` return type is +//! fully determined by its declaration. The registry common path infers the +//! arguments and enforces the exactly-2-argument arity before falling back to +//! `returns`. +//! - `lower` is a thin wrapper over `io::lower_copy` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "copy", + area: Io, + params: [from: Str, to: Str], + returns: Bool, + lower: lower, + summary: "Copies a file.", + php_manual: "function.copy", +} + +/// Lowers a `copy` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_copy(ctx, inst) +} diff --git a/src/builtins/io/dirname.rs b/src/builtins/io/dirname.rs new file mode 100644 index 0000000000..8f8bc1096d --- /dev/null +++ b/src/builtins/io/dirname.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Home of the PHP `dirname` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the optional `levels` argument, when a static integer literal, +//! is greater than or equal to 1 (PHP requirement). +//! - The registry pre-infers arguments before calling the hook; the hook does not +//! call `infer_type` again. +//! - `lower` is a thin wrapper over `io::lower_dirname` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "dirname", + area: Io, + params: [path: Str, levels: Int = DefaultSpec::Int(1)], + returns: Str, + check: check, + lower: lower, + summary: "Returns a parent directory's path.", + php_manual: "function.dirname", +} + +/// Returns `Str`, rejecting static integer `levels` arguments less than 1. +/// +/// The registry pre-infers arguments before calling this hook. The hook checks +/// whether the optional `levels` argument is a compile-time integer literal less +/// than 1 and emits a diagnostic if so; otherwise returns `PhpType::Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if matches!( + cx.args.get(1).map(|arg| &arg.kind), + Some(ExprKind::IntLiteral(levels)) if *levels < 1 + ) { + return Err(CompileError::new( + cx.span, + "dirname() levels must be greater than or equal to 1", + )); + } + Ok(PhpType::Str) +} + +/// Lowers a `dirname` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_dirname(ctx, inst) +} diff --git a/src/builtins/io/disk_free_space.rs b/src/builtins/io/disk_free_space.rs new file mode 100644 index 0000000000..ec4cf6ce53 --- /dev/null +++ b/src/builtins/io/disk_free_space.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `disk_free_space` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `disk_free_space` is a pure-data builtin whose return +//! type (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_disk_free_space` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "disk_free_space", + area: Io, + params: [directory: Str], + returns: Float, + lower: lower, + summary: "Returns available space on filesystem or disk partition.", + php_manual: "function.disk-free-space", +} + +/// Lowers a `disk_free_space` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_disk_free_space(ctx, inst) +} diff --git a/src/builtins/io/disk_total_space.rs b/src/builtins/io/disk_total_space.rs new file mode 100644 index 0000000000..536f6e304c --- /dev/null +++ b/src/builtins/io/disk_total_space.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `disk_total_space` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `disk_total_space` is a pure-data builtin whose return +//! type (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_disk_total_space` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "disk_total_space", + area: Io, + params: [directory: Str], + returns: Float, + lower: lower, + summary: "Returns the total size of a filesystem or disk partition.", + php_manual: "function.disk-total-space", +} + +/// Lowers a `disk_total_space` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_disk_total_space(ctx, inst) +} diff --git a/src/builtins/io/fclose.rs b/src/builtins/io/fclose.rs new file mode 100644 index 0000000000..e0623a8fda --- /dev/null +++ b/src/builtins/io/fclose.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `fclose` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Bool`. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fclose` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fclose", + area: Io, + params: [stream: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Closes an open file pointer.", + php_manual: "function.fclose", +} + +/// Validates the stream argument is a stream resource and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers an `fclose` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fclose(ctx, inst) +} diff --git a/src/builtins/io/fdatasync.rs b/src/builtins/io/fdatasync.rs new file mode 100644 index 0000000000..66dbecf18f --- /dev/null +++ b/src/builtins/io/fdatasync.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `fdatasync` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `stream` argument is a stream resource and returns `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fdatasync` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fdatasync", + area: Io, + params: [stream: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Synchronizes data (but not meta-data) to file.", + php_manual: "function.fdatasync", +} + +/// Validates the stream argument is a stream resource and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `fdatasync` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fdatasync(ctx, inst) +} diff --git a/src/builtins/io/feof.rs b/src/builtins/io/feof.rs new file mode 100644 index 0000000000..dab5ea00f3 --- /dev/null +++ b/src/builtins/io/feof.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `feof` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Bool`. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_feof` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "feof", + area: Io, + params: [stream: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Tests for end-of-file on a file pointer.", + php_manual: "function.feof", +} + +/// Validates the stream argument is a stream resource and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers an `feof` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_feof(ctx, inst) +} diff --git a/src/builtins/io/fflush.rs b/src/builtins/io/fflush.rs new file mode 100644 index 0000000000..8e6dd037a5 --- /dev/null +++ b/src/builtins/io/fflush.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `fflush` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `stream` argument is a stream resource and returns `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fflush` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fflush", + area: Io, + params: [stream: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Flushes the output to a file.", + php_manual: "function.fflush", +} + +/// Validates the stream argument is a stream resource and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `fflush` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fflush(ctx, inst) +} diff --git a/src/builtins/io/fgetc.rs b/src/builtins/io/fgetc.rs new file mode 100644 index 0000000000..357daf962d --- /dev/null +++ b/src/builtins/io/fgetc.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `fgetc` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Union(Str, Bool)` reflecting PHP behaviour where `fgetc` returns a +//! single character or `false` on EOF. `returns: Mixed` is used because the union +//! cannot be expressed through the scalar `returns:` field. +//! - `lower` is a thin wrapper over `io::lower_fgetc` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fgetc", + area: Io, + params: [stream: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets a character from the given file pointer.", + php_manual: "function.fgetc", +} + +/// Validates the stream argument and returns `Union(Str, Bool)` for the EOF pattern. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers an `fgetc` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fgetc(ctx, inst) +} diff --git a/src/builtins/io/fgetcsv.rs b/src/builtins/io/fgetcsv.rs new file mode 100644 index 0000000000..c56996e47c --- /dev/null +++ b/src/builtins/io/fgetcsv.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Home of the PHP `fgetcsv` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `stream` argument is a stream resource and returns `Array`. +//! - `returns: Mixed` is used because the array type cannot be expressed through the +//! scalar `returns:` field. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fgetcsv` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fgetcsv", + area: Io, + params: [stream: Mixed, length: Int = DefaultSpec::Null, separator: Str = DefaultSpec::Str(",")], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets line from file pointer and parse for CSV fields.", + php_manual: "function.fgetcsv", +} + +/// Validates the stream argument is a stream resource and returns `Array`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `fgetcsv` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fgetcsv(ctx, inst) +} diff --git a/src/builtins/io/fgets.rs b/src/builtins/io/fgets.rs new file mode 100644 index 0000000000..9c3f75bae5 --- /dev/null +++ b/src/builtins/io/fgets.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Home of the PHP `fgets` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Mixed` (reflecting PHP's `string|false` on EOF). `returns: Mixed` is used +//! because the precise union cannot be expressed through the scalar `returns:` field. +//! - `lower` is a thin wrapper over `io::lower_fgets` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fgets", + area: Io, + params: [stream: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets line from file pointer.", + php_manual: "function.fgets", +} + +/// Validates the stream argument and returns `Mixed` for the `string|false` EOF pattern. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Mixed) +} + +/// Lowers an `fgets` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fgets(ctx, inst) +} diff --git a/src/builtins/io/file.rs b/src/builtins/io/file.rs new file mode 100644 index 0000000000..329349d841 --- /dev/null +++ b/src/builtins/io/file.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `file` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Array` (the file's lines). A check hook is required +//! because the array return type cannot be expressed through the scalar `returns:` +//! field. +//! - `lower` is a thin wrapper over `io::lower_file` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "file", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Reads an entire file into an array.", + php_manual: "function.file", +} + +/// Returns `Array` reflecting that `file` yields the file's lines as strings. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `file` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_file(ctx, inst) +} diff --git a/src/builtins/io/file_exists.rs b/src/builtins/io/file_exists.rs new file mode 100644 index 0000000000..9d98d30a35 --- /dev/null +++ b/src/builtins/io/file_exists.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `file_exists` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `file_exists` is a pure-data builtin whose return +//! type (`Bool`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_file_exists` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "file_exists", + area: Io, + params: [filename: Str], + returns: Bool, + lower: lower, + summary: "Checks whether a file or directory exists.", + php_manual: "function.file-exists", +} + +/// Lowers a `file_exists` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_file_exists(ctx, inst) +} diff --git a/src/builtins/io/file_get_contents.rs b/src/builtins/io/file_get_contents.rs new file mode 100644 index 0000000000..44b8aefe4e --- /dev/null +++ b/src/builtins/io/file_get_contents.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Home of the PHP `file_get_contents` builtin: its declaration, type-check hook, +//! and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` reflecting PHP behaviour where the read +//! returns the file contents or `false` on failure. +//! - The `check` hook has a library-linking side effect: a literal `https://` / +//! `ftps://` URL links `elephc_tls`; a non-literal path conservatively links +//! `elephc_tls`, `elephc_phar`, `z`, and `bz2` because the scheme and PHAR entry +//! flags are unknown until run time. +//! - `lower` is a thin wrapper over `io::lower_file_get_contents` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "file_get_contents", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Reads an entire file into a string.", + php_manual: "function.file-get-contents", +} + +/// Returns `Union(Str, Bool)` and records the runtime libraries the call may need. +/// +/// A literal `https://`/`ftps://` URL is read over TLS, so it links `elephc_tls`. +/// A non-literal path routes through the runtime URL dispatcher, whose scheme and +/// PHAR entry flags are unknown at compile time, so it conservatively links TLS +/// plus the PHAR bridge and decompression libraries (`z`, `bz2`). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(ExprKind::StringLiteral(url)) = cx.args.first().map(|a| &a.kind) { + if url.starts_with("https://") || url.starts_with("ftps://") { + cx.checker.require_builtin_library("elephc_tls"); + } + } else { + cx.checker.require_builtin_library("elephc_tls"); + cx.checker.require_builtin_library("elephc_phar"); + cx.checker.require_builtin_library("z"); + cx.checker.require_builtin_library("bz2"); + } + cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(PhpType::Union(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `file_get_contents` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_file_get_contents(ctx, inst) +} diff --git a/src/builtins/io/file_put_contents.rs b/src/builtins/io/file_put_contents.rs new file mode 100644 index 0000000000..c161babf1b --- /dev/null +++ b/src/builtins/io/file_put_contents.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Home of the PHP `file_put_contents` builtin: its declaration, type-check hook, +//! and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Int` (the number of bytes written). +//! - The `check` hook links the PHAR bridge: a literal `phar://` URL writes through +//! the read-modify-write bridge and links `elephc_phar` plus `elephc_crypto` (the +//! assembly SHA1 path remains a fallback); any non-literal path links `elephc_phar`. +//! - `lower` is a thin wrapper over `io::lower_file_put_contents` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "file_put_contents", + area: Io, + params: [filename: Str, data: Str], + returns: Int, + check: check, + lower: lower, + summary: "Writes data to a file.", + php_manual: "function.file-put-contents", +} + +/// Returns `Int` and records the PHAR libraries the write may need. +/// +/// A literal `phar://` target writes through the `elephc_phar` bridge and also links +/// `elephc_crypto`; any other target (including non-literal paths) links `elephc_phar`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(ExprKind::StringLiteral(url)) = cx.args.first().map(|a| &a.kind) { + if url.starts_with("phar://") { + cx.checker.require_builtin_library("elephc_phar"); + cx.checker.require_builtin_library("elephc_crypto"); + } + } else { + cx.checker.require_builtin_library("elephc_phar"); + } + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + Ok(PhpType::Int) +} + +/// Lowers a `file_put_contents` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_file_put_contents(ctx, inst) +} diff --git a/src/builtins/io/fileatime.rs b/src/builtins/io/fileatime.rs new file mode 100644 index 0000000000..3803aab061 --- /dev/null +++ b/src/builtins/io/fileatime.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `fileatime` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Int, Bool)` reflecting PHP behaviour where `fileatime` +//! returns the last access time as a Unix timestamp on success or `false` on failure. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_fileatime` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fileatime", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets last access time of file.", + php_manual: "function.fileatime", +} + +/// Returns `Union(Int, Bool)` reflecting that `fileatime` can return a timestamp or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `fileatime` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fileatime(ctx, inst) +} diff --git a/src/builtins/io/filectime.rs b/src/builtins/io/filectime.rs new file mode 100644 index 0000000000..207197de2b --- /dev/null +++ b/src/builtins/io/filectime.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `filectime` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Int, Bool)` reflecting PHP behaviour where `filectime` +//! returns the inode change time as a Unix timestamp on success or `false` on failure. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_filectime` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "filectime", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets inode change time of file.", + php_manual: "function.filectime", +} + +/// Returns `Union(Int, Bool)` reflecting that `filectime` can return a timestamp or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `filectime` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_filectime(ctx, inst) +} diff --git a/src/builtins/io/filegroup.rs b/src/builtins/io/filegroup.rs new file mode 100644 index 0000000000..7bf79052a6 --- /dev/null +++ b/src/builtins/io/filegroup.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `filegroup` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Int, Bool)` reflecting PHP behaviour where `filegroup` +//! returns the numeric group ID of the file owner on success or `false` on failure. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_filegroup` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "filegroup", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets file group.", + php_manual: "function.filegroup", +} + +/// Returns `Union(Int, Bool)` reflecting that `filegroup` can return a group ID or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `filegroup` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_filegroup(ctx, inst) +} diff --git a/src/builtins/io/fileinode.rs b/src/builtins/io/fileinode.rs new file mode 100644 index 0000000000..4976fb834d --- /dev/null +++ b/src/builtins/io/fileinode.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `fileinode` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Int, Bool)` reflecting PHP behaviour where `fileinode` +//! returns the inode number of the file on success or `false` on failure. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_fileinode` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fileinode", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets file inode.", + php_manual: "function.fileinode", +} + +/// Returns `Union(Int, Bool)` reflecting that `fileinode` can return an inode number or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `fileinode` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fileinode(ctx, inst) +} diff --git a/src/builtins/io/filemtime.rs b/src/builtins/io/filemtime.rs new file mode 100644 index 0000000000..6a2d6182c5 --- /dev/null +++ b/src/builtins/io/filemtime.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `filemtime` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `filemtime` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_filemtime` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "filemtime", + area: Io, + params: [filename: Str], + returns: Int, + lower: lower, + summary: "Gets file modification time.", + php_manual: "function.filemtime", +} + +/// Lowers a `filemtime` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_filemtime(ctx, inst) +} diff --git a/src/builtins/io/fileowner.rs b/src/builtins/io/fileowner.rs new file mode 100644 index 0000000000..15a476ddcb --- /dev/null +++ b/src/builtins/io/fileowner.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `fileowner` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Int, Bool)` reflecting PHP behaviour where `fileowner` +//! returns the numeric user ID of the file owner on success or `false` on failure. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_fileowner` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fileowner", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets file owner.", + php_manual: "function.fileowner", +} + +/// Returns `Union(Int, Bool)` reflecting that `fileowner` can return a user ID or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `fileowner` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fileowner(ctx, inst) +} diff --git a/src/builtins/io/fileperms.rs b/src/builtins/io/fileperms.rs new file mode 100644 index 0000000000..1de7712f31 --- /dev/null +++ b/src/builtins/io/fileperms.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `fileperms` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Int, Bool)` reflecting PHP behaviour where `fileperms` +//! returns the file's permissions as an integer on success or `false` on failure. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_fileperms` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fileperms", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets file permissions.", + php_manual: "function.fileperms", +} + +/// Returns `Union(Int, Bool)` reflecting that `fileperms` can return permissions or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `fileperms` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fileperms(ctx, inst) +} diff --git a/src/builtins/io/filesize.rs b/src/builtins/io/filesize.rs new file mode 100644 index 0000000000..92eced95ec --- /dev/null +++ b/src/builtins/io/filesize.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `filesize` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `filesize` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_filesize` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "filesize", + area: Io, + params: [filename: Str], + returns: Int, + lower: lower, + summary: "Gets file size.", + php_manual: "function.filesize", +} + +/// Lowers a `filesize` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_filesize(ctx, inst) +} diff --git a/src/builtins/io/filetype.rs b/src/builtins/io/filetype.rs new file mode 100644 index 0000000000..2ce6f4aea7 --- /dev/null +++ b/src/builtins/io/filetype.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `filetype` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` reflecting PHP behaviour where `filetype` +//! returns a string describing the file type on success or `false` on failure. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_filetype` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "filetype", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets file type.", + php_manual: "function.filetype", +} + +/// Returns `Union(Str, Bool)` reflecting that `filetype` can return a type string or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `filetype` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_filetype(ctx, inst) +} diff --git a/src/builtins/io/flock.rs b/src/builtins/io/flock.rs new file mode 100644 index 0000000000..ac49795b85 --- /dev/null +++ b/src/builtins/io/flock.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Home of the PHP `flock` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the stream resource, checks that `operation` is strictly `Int` +//! (not just accepts_int), and verifies that `would_block` (when present) is passed +//! as a variable — both checks match the legacy behaviour exactly. +//! - `would_block` is a by-reference parameter (`ref` marker in `params:`); the hook's +//! variable check is in addition to, not instead of, the ref-ness. +//! - Arguments are pre-inferred by the registry before the hook runs; `operation` is +//! re-inferred inside the hook to obtain its type for validation. +//! - `lower` is a thin wrapper over `io::lower_flock` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "flock", + area: Io, + params: [stream: Mixed, operation: Int, ref would_block: Mixed = DefaultSpec::Null], + returns: Bool, + check: check, + lower: lower, + summary: "Portable advisory file locking.", + php_manual: "function.flock", +} + +/// Validates the stream resource, enforces strict Int type for operation, and +/// requires that `would_block` (if provided) is passed as a plain variable. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + let op_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; // re-infer to obtain the type + if op_ty != PhpType::Int { // STRICT eq (not accepts_int) + return Err(CompileError::new( + cx.args[1].span, + "flock() operation must be int", + )); + } + if let Some(arg2) = cx.args.get(2) { + if !matches!(arg2.kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + arg2.span, + "flock() parameter $would_block must be passed a variable", + )); + } + } + Ok(PhpType::Bool) +} + +/// Lowers a `flock` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_flock(ctx, inst) +} diff --git a/src/builtins/io/fnmatch.rs b/src/builtins/io/fnmatch.rs new file mode 100644 index 0000000000..f2556ecb9a --- /dev/null +++ b/src/builtins/io/fnmatch.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Home of the PHP `fnmatch` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the optional `flags` argument, when present, has type `Int`. +//! - The registry pre-infers all arguments before calling the hook; the hook calls +//! `infer_type` on `flags` again (idempotent) to obtain its resolved type. +//! - `lower` is a thin wrapper over `io::lower_fnmatch` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fnmatch", + area: Io, + params: [pattern: Str, filename: Str, flags: Int = DefaultSpec::Int(0)], + returns: Bool, + check: check, + lower: lower, + summary: "Matches a filename against a pattern.", + php_manual: "function.fnmatch", +} + +/// Returns `Bool`, requiring the optional `flags` argument to be of type `Int`. +/// +/// The registry pre-infers all arguments before calling this hook. The hook +/// re-infers the optional `flags` argument (idempotent) to obtain its resolved +/// type, and emits a diagnostic if the type is not `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(flags) = cx.args.get(2) { + let flags_ty = cx.checker.infer_type(flags, cx.env)?; + if flags_ty != PhpType::Int { + return Err(CompileError::new(cx.span, "fnmatch() flags must be int")); + } + } + Ok(PhpType::Bool) +} + +/// Lowers a `fnmatch` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fnmatch(ctx, inst) +} diff --git a/src/builtins/io/fopen.rs b/src/builtins/io/fopen.rs new file mode 100644 index 0000000000..5e8f268ec7 --- /dev/null +++ b/src/builtins/io/fopen.rs @@ -0,0 +1,94 @@ +//! Purpose: +//! Home of the PHP `fopen` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` detects the URL scheme from a string-literal first argument and links +//! the appropriate runtime libraries (`elephc_tls`, `z`, `bz2`, `elephc_phar`, +//! `elephc_crypto`) at compile time. Non-literal paths conservatively link all +//! PHAR and decompression libraries. +//! - Returns `Union(stream_resource, Bool)` via `returns: Mixed` because the union +//! involves a resource type that the scalar `returns:` field cannot express. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does +//! NOT re-infer them. +//! - `lower` is a thin wrapper over `io::lower_fopen` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "fopen", + area: Io, + params: [ + filename: Str, + mode: Str, + use_include_path: Bool = DefaultSpec::Bool(false), + context: Mixed = DefaultSpec::Null + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Opens file or URL.", + php_manual: "function.fopen", +} + +/// Detects URL scheme from the filename literal and links the required runtime libraries. +/// +/// A literal `https://` or `ftps://` URL links `elephc_tls`. A `compress.zlib://` scheme +/// links `z`. A `compress.bzip2://` scheme links `bz2`. A `phar://` URL in write mode +/// links `elephc_phar` and `elephc_crypto`. A non-literal path conservatively links +/// `elephc_phar`, `z`, and `bz2` because the scheme is unknown until run time. +/// Returns `Union(stream_resource, Bool)` for the success/false-on-failure PHP pattern. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(ExprKind::StringLiteral(s)) = cx.args.first().map(|a| &a.kind) { + if s.starts_with("https://") || s.starts_with("ftps://") { + cx.checker.require_builtin_library("elephc_tls"); + } + if s.starts_with("compress.zlib://") { + // compress.zlib:// attaches a zlib.inflate filter, which pulls in libz. + cx.checker.require_builtin_library("z"); + } + if s.starts_with("compress.bzip2://") { + // compress.bzip2:// calls libbz2's BZ2_bzBuffToBuffDecompress at fopen time. + cx.checker.require_builtin_library("bz2"); + } + // phar:// write mode uses the elephc-phar read-modify-write bridge when available + // and keeps the elephc-crypto SHA1 path as the assembly fallback. Reads need + // neither write bridge nor crypto here. + if s.starts_with("phar://") { + let write_mode = matches!( + cx.args.get(1).map(|a| &a.kind), + Some(ExprKind::StringLiteral(m)) + if matches!(m.as_bytes().first(), Some(b'w') | Some(b'a') | Some(b'c') | Some(b'x')) + ); + if write_mode { + cx.checker.require_builtin_library("elephc_phar"); + cx.checker.require_builtin_library("elephc_crypto"); + } + } + } else { + // Non-literal paths can route to a phar:// entry at run time for reads or + // write-mode opens. Reads may use tar/zip and compressed entries through the + // elephc-phar/zlib/bz2 bridge. + cx.checker.require_builtin_library("elephc_phar"); + cx.checker.require_builtin_library("z"); + cx.checker.require_builtin_library("bz2"); + } + Ok(cx.checker.normalize_union_type(vec![ + PhpType::stream_resource(), + PhpType::Bool, + ])) +} + +/// Lowers an `fopen` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fopen(ctx, inst) +} diff --git a/src/builtins/io/fpassthru.rs b/src/builtins/io/fpassthru.rs new file mode 100644 index 0000000000..36db09d601 --- /dev/null +++ b/src/builtins/io/fpassthru.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `fpassthru` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Int`. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fpassthru` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fpassthru", + area: Io, + params: [stream: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Output all remaining data on a file pointer.", + php_manual: "function.fpassthru", +} + +/// Validates the stream argument is a stream resource and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Int) +} + +/// Lowers an `fpassthru` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fpassthru(ctx, inst) +} diff --git a/src/builtins/io/fprintf.rs b/src/builtins/io/fprintf.rs new file mode 100644 index 0000000000..18fefd9d8a --- /dev/null +++ b/src/builtins/io/fprintf.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `fprintf` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Int`. Arguments are pre-inferred by the registry before the hook runs. +//! - The variadic `values` parameter accepts zero or more format arguments after the +//! stream and format string. +//! - `lower` is a thin wrapper over `io::lower_fprintf` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fprintf", + area: Io, + params: [stream: Mixed, format: Str], + variadic: "values", + returns: Int, + check: check, + lower: lower, + summary: "Write a formatted string to a stream.", + php_manual: "function.fprintf", +} + +/// Validates the stream argument is a stream resource and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Int) +} + +/// Lowers an `fprintf` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fprintf(ctx, inst) +} diff --git a/src/builtins/io/fputcsv.rs b/src/builtins/io/fputcsv.rs new file mode 100644 index 0000000000..34684b4d3e --- /dev/null +++ b/src/builtins/io/fputcsv.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Home of the PHP `fputcsv` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `stream` argument is a stream resource and returns `Int`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fputcsv` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fputcsv", + area: Io, + params: [ + stream: Mixed, + fields: Mixed, + separator: Str = DefaultSpec::Str(","), + enclosure: Str = DefaultSpec::Str("\"") + ], + returns: Int, + check: check, + lower: lower, + summary: "Format line as CSV and write to file pointer.", + php_manual: "function.fputcsv", +} + +/// Validates the stream argument is a stream resource and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Int) +} + +/// Lowers a `fputcsv` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fputcsv(ctx, inst) +} diff --git a/src/builtins/io/fread.rs b/src/builtins/io/fread.rs new file mode 100644 index 0000000000..2a5af54ef6 --- /dev/null +++ b/src/builtins/io/fread.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `fread` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Str`. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fread` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fread", + area: Io, + params: [stream: Mixed, length: Int], + returns: Str, + check: check, + lower: lower, + summary: "Binary-safe file read.", + php_manual: "function.fread", +} + +/// Validates the stream argument is a stream resource and returns `Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Str) +} + +/// Lowers an `fread` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fread(ctx, inst) +} diff --git a/src/builtins/io/fscanf.rs b/src/builtins/io/fscanf.rs new file mode 100644 index 0000000000..aa08fe0fe3 --- /dev/null +++ b/src/builtins/io/fscanf.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Home of the PHP `fscanf` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Array` reflecting the 2-argument form that returns matched fields. +//! `returns: Mixed` is used because `Array` cannot be expressed through the +//! scalar `returns:` field. Arguments are pre-inferred by the registry before the +//! hook runs. +//! - The variadic `vars` parameter is accepted but the by-ref output form is not yet +//! supported (mirroring `sscanf()`). +//! - `lower` is a thin wrapper over `io::lower_fscanf` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fscanf", + area: Io, + params: [stream: Mixed, format: Str], + variadic: "vars", + returns: Mixed, + check: check, + lower: lower, + summary: "Parses input from a file according to a format.", + php_manual: "function.fscanf", +} + +/// Validates the stream argument and returns `Array` for the matched-fields result. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers an `fscanf` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fscanf(ctx, inst) +} diff --git a/src/builtins/io/fseek.rs b/src/builtins/io/fseek.rs new file mode 100644 index 0000000000..2f63311138 --- /dev/null +++ b/src/builtins/io/fseek.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `fseek` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Union(Int, Bool)`. `returns: Mixed` is used because the union cannot be +//! expressed through the scalar `returns:` field. Arguments are pre-inferred by the +//! registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fseek` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fseek", + area: Io, + params: [stream: Mixed, offset: Int, whence: Int = DefaultSpec::Int(0)], + returns: Mixed, + check: check, + lower: lower, + summary: "Seeks on a file pointer.", + php_manual: "function.fseek", +} + +/// Validates the stream argument and returns `Union(Int, Bool)` for the seek result. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers an `fseek` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fseek(ctx, inst) +} diff --git a/src/builtins/io/fsockopen.rs b/src/builtins/io/fsockopen.rs new file mode 100644 index 0000000000..99600e6d10 --- /dev/null +++ b/src/builtins/io/fsockopen.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Home of the PHP `fsockopen` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that `error_code` (arg[2]) and `error_message` (arg[3]), if provided, +//! are plain variables (they are written by reference). Returns `Union(stream_resource, Bool)`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` dispatches to `io::lower_fsockopen` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "fsockopen", + area: Io, + params: [ + hostname: Str, + port: Int, + ref error_code: Mixed = DefaultSpec::Null, + ref error_message: Mixed = DefaultSpec::Null, + timeout: Mixed = DefaultSpec::Null + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Open Internet or Unix domain socket connection.", + php_manual: "function.fsockopen", +} + +/// Validates ref output params are plain variables, then returns `Union(stream_resource, Bool)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(ec) = cx.args.get(2) { + if !matches!(ec.kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + ec.span, + &format!("{}() parameter $error_code must be passed a variable", cx.name), + )); + } + } + if let Some(em) = cx.args.get(3) { + if !matches!(em.kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + em.span, + &format!("{}() parameter $error_message must be passed a variable", cx.name), + )); + } + } + Ok(cx.checker.normalize_union_type(vec![PhpType::stream_resource(), PhpType::Bool])) +} + +/// Lowers a `fsockopen` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fsockopen(ctx, inst) +} diff --git a/src/builtins/io/fstat.rs b/src/builtins/io/fstat.rs new file mode 100644 index 0000000000..721e249c04 --- /dev/null +++ b/src/builtins/io/fstat.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Home of the PHP `fstat` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `stream` argument is a stream resource via +//! `ensure_stream_resource`, then returns `assoc-array|bool` via +//! `stat_result_type`. PHP's `fstat` returns the stat buffer array on success or +//! `false` on failure. +//! - `ensure_stream_resource` is kept in `common.rs` (not moved) because +//! `streams.rs` also uses it; it is widened to `pub(crate)` for access here. +//! - The registry pre-infers arguments before calling this hook (idempotent with +//! the infer call inside `ensure_stream_resource`). +//! - `lower` is a thin wrapper over `io::lower_fstat` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fstat", + area: Io, + params: [stream: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets information about a file using an open file pointer.", + php_manual: "function.fstat", +} + +/// Validates `stream` is a stream resource and returns `assoc-array|bool`. +/// +/// Calls `ensure_stream_resource` to emit a type error if the argument is not a +/// compatible stream type, then returns the stat result type via `stat_result_type`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(crate::builtins::io::stat_support::stat_result_type(cx.checker)) +} + +/// Lowers an `fstat` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fstat(ctx, inst) +} diff --git a/src/builtins/io/fsync.rs b/src/builtins/io/fsync.rs new file mode 100644 index 0000000000..7f6f4c461a --- /dev/null +++ b/src/builtins/io/fsync.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `fsync` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `stream` argument is a stream resource and returns `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fsync` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fsync", + area: Io, + params: [stream: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Synchronizes changes to the file (including meta-data).", + php_manual: "function.fsync", +} + +/// Validates the stream argument is a stream resource and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `fsync` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fsync(ctx, inst) +} diff --git a/src/builtins/io/ftell.rs b/src/builtins/io/ftell.rs new file mode 100644 index 0000000000..13c2a6416e --- /dev/null +++ b/src/builtins/io/ftell.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `ftell` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Int`. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_ftell` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ftell", + area: Io, + params: [stream: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Returns the current position of the file read/write pointer.", + php_manual: "function.ftell", +} + +/// Validates the stream argument is a stream resource and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Int) +} + +/// Lowers an `ftell` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_ftell(ctx, inst) +} diff --git a/src/builtins/io/ftruncate.rs b/src/builtins/io/ftruncate.rs new file mode 100644 index 0000000000..c6d1b67bff --- /dev/null +++ b/src/builtins/io/ftruncate.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `ftruncate` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Bool`. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_ftruncate` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ftruncate", + area: Io, + params: [stream: Mixed, size: Int], + returns: Bool, + check: check, + lower: lower, + summary: "Truncates a file to a given length.", + php_manual: "function.ftruncate", +} + +/// Validates the stream argument is a stream resource and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers an `ftruncate` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_ftruncate(ctx, inst) +} diff --git a/src/builtins/io/fwrite.rs b/src/builtins/io/fwrite.rs new file mode 100644 index 0000000000..3cb4c9064e --- /dev/null +++ b/src/builtins/io/fwrite.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `fwrite` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Int`. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_fwrite` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "fwrite", + area: Io, + params: [stream: Mixed, data: Str], + returns: Int, + check: check, + lower: lower, + summary: "Binary-safe file write.", + php_manual: "function.fwrite", +} + +/// Validates the stream argument is a stream resource and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Int) +} + +/// Lowers an `fwrite` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fwrite(ctx, inst) +} diff --git a/src/builtins/io/getcwd.rs b/src/builtins/io/getcwd.rs new file mode 100644 index 0000000000..0bd0ef0aa4 --- /dev/null +++ b/src/builtins/io/getcwd.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `getcwd` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `getcwd` is a pure-data builtin whose `Str` return type is +//! fully determined by its declaration. The registry common path enforces its +//! 0-argument arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_getcwd` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "getcwd", + area: Io, + params: [], + returns: Str, + lower: lower, + summary: "Gets the current working directory.", + php_manual: "function.getcwd", +} + +/// Lowers a `getcwd` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_getcwd(ctx, inst) +} diff --git a/src/builtins/io/gethostbyaddr.rs b/src/builtins/io/gethostbyaddr.rs new file mode 100644 index 0000000000..4181382278 --- /dev/null +++ b/src/builtins/io/gethostbyaddr.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `gethostbyaddr` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` reflecting PHP's false-on-failure return. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` dispatches to `io::lower_gethostbyaddr` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "gethostbyaddr", + area: Io, + params: [ip: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets the Internet host name corresponding to a given IP address.", + php_manual: "function.gethostbyaddr", +} + +/// Returns `Union(Str, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `gethostbyaddr` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_gethostbyaddr(ctx, inst) +} diff --git a/src/builtins/io/gethostbyname.rs b/src/builtins/io/gethostbyname.rs new file mode 100644 index 0000000000..930b290760 --- /dev/null +++ b/src/builtins/io/gethostbyname.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `gethostbyname` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers the hostname argument and returns `Str`. +//! - `lower` dispatches to `io::lower_gethostbyname` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "gethostbyname", + area: Io, + params: [hostname: Str], + returns: Str, + lower: lower, + summary: "Gets the IPv4 address corresponding to the given Internet host name.", + php_manual: "function.gethostbyname", +} + +/// Lowers a `gethostbyname` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_gethostbyname(ctx, inst) +} diff --git a/src/builtins/io/gethostname.rs b/src/builtins/io/gethostname.rs new file mode 100644 index 0000000000..33201d6416 --- /dev/null +++ b/src/builtins/io/gethostname.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `gethostname` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers no arguments and returns `Str`. +//! - `lower` dispatches to `io::lower_gethostname` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "gethostname", + area: Io, + params: [], + returns: Str, + lower: lower, + summary: "Gets the standard host name for the local machine.", + php_manual: "function.gethostname", +} + +/// Lowers a `gethostname` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_gethostname(ctx, inst) +} diff --git a/src/builtins/io/getprotobyname.rs b/src/builtins/io/getprotobyname.rs new file mode 100644 index 0000000000..2ac2e16011 --- /dev/null +++ b/src/builtins/io/getprotobyname.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `getprotobyname` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Int, Bool)` reflecting PHP's false-on-failure return. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` dispatches to `io::lower_getprotobyname` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "getprotobyname", + area: Io, + params: [protocol: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets the protocol number associated with the given protocol name.", + php_manual: "function.getprotobyname", +} + +/// Returns `Union(Int, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `getprotobyname` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_getprotobyname(ctx, inst) +} diff --git a/src/builtins/io/getprotobynumber.rs b/src/builtins/io/getprotobynumber.rs new file mode 100644 index 0000000000..f68370f132 --- /dev/null +++ b/src/builtins/io/getprotobynumber.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `getprotobynumber` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` reflecting PHP's false-on-failure return. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` dispatches to `io::lower_getprotobynumber` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "getprotobynumber", + area: Io, + params: [protocol: Int], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets the protocol name associated with the given protocol number.", + php_manual: "function.getprotobynumber", +} + +/// Returns `Union(Str, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `getprotobynumber` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_getprotobynumber(ctx, inst) +} diff --git a/src/builtins/io/getservbyname.rs b/src/builtins/io/getservbyname.rs new file mode 100644 index 0000000000..b6404deb45 --- /dev/null +++ b/src/builtins/io/getservbyname.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `getservbyname` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Int, Bool)` reflecting PHP's false-on-failure return. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` dispatches to `io::lower_getservbyname` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "getservbyname", + area: Io, + params: [service: Str, protocol: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets port number associated with an Internet service and protocol.", + php_manual: "function.getservbyname", +} + +/// Returns `Union(Int, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `getservbyname` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_getservbyname(ctx, inst) +} diff --git a/src/builtins/io/getservbyport.rs b/src/builtins/io/getservbyport.rs new file mode 100644 index 0000000000..836dc26aab --- /dev/null +++ b/src/builtins/io/getservbyport.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `getservbyport` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` reflecting PHP's false-on-failure return. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` dispatches to `io::lower_getservbyport` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "getservbyport", + area: Io, + params: [port: Int, protocol: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets the Internet service that corresponds to a port and protocol.", + php_manual: "function.getservbyport", +} + +/// Returns `Union(Str, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `getservbyport` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_getservbyport(ctx, inst) +} diff --git a/src/builtins/io/glob.rs b/src/builtins/io/glob.rs new file mode 100644 index 0000000000..f8fab13141 --- /dev/null +++ b/src/builtins/io/glob.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `glob` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Array` (the matched pathnames). A check hook is required +//! because the array return type cannot be expressed through the scalar `returns:` +//! field. +//! - `lower` is a thin wrapper over `io::lower_glob` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "glob", + area: Io, + params: [pattern: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Finds pathnames matching a pattern.", + php_manual: "function.glob", +} + +/// Returns `Array` reflecting that `glob` yields the matched pathnames. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `glob` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_glob(ctx, inst) +} diff --git a/src/builtins/io/hash_file.rs b/src/builtins/io/hash_file.rs new file mode 100644 index 0000000000..bfbe5e9469 --- /dev/null +++ b/src/builtins/io/hash_file.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `hash_file` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` reflecting PHP behaviour where `hash_file` +//! returns the digest string or `false` when the file cannot be read. +//! - The `check` hook links `elephc_crypto`: `hash_file` reads the file then hashes +//! through the crypto bridge (full algorithm set, raw `$binary` output). +//! - `lower` is a thin wrapper over `io::lower_hash_file` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "hash_file", + area: Io, + params: [algo: Str, filename: Str, binary: Bool = DefaultSpec::Bool(false)], + returns: Mixed, + check: check, + lower: lower, + summary: "Generates a hash value using the contents of a given file.", + php_manual: "function.hash-file", +} + +/// Returns `Union(Str, Bool)` and links `elephc_crypto` for the digest routine. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + for arg in cx.args { + cx.checker.infer_type(arg, cx.env)?; + } + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Union(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `hash_file` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_hash_file(ctx, inst) +} diff --git a/src/builtins/io/is_dir.rs b/src/builtins/io/is_dir.rs new file mode 100644 index 0000000000..f87800aa45 --- /dev/null +++ b/src/builtins/io/is_dir.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `is_dir` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `is_dir` is a pure-data builtin whose return type +//! (`Bool`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_is_dir` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_dir", + area: Io, + params: [filename: Str], + returns: Bool, + lower: lower, + summary: "Tells whether the filename is a directory.", + php_manual: "function.is-dir", +} + +/// Lowers an `is_dir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_is_dir(ctx, inst) +} diff --git a/src/builtins/io/is_executable.rs b/src/builtins/io/is_executable.rs new file mode 100644 index 0000000000..fff534afb2 --- /dev/null +++ b/src/builtins/io/is_executable.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `is_executable` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `is_executable` is a pure-data builtin whose return +//! type (`Bool`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_is_executable` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_executable", + area: Io, + params: [filename: Str], + returns: Bool, + lower: lower, + summary: "Tells whether the filename is executable.", + php_manual: "function.is-executable", +} + +/// Lowers an `is_executable` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_is_executable(ctx, inst) +} diff --git a/src/builtins/io/is_file.rs b/src/builtins/io/is_file.rs new file mode 100644 index 0000000000..abdaa22d4c --- /dev/null +++ b/src/builtins/io/is_file.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `is_file` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `is_file` is a pure-data builtin whose return type +//! (`Bool`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_is_file` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_file", + area: Io, + params: [filename: Str], + returns: Bool, + lower: lower, + summary: "Tells whether the filename is a regular file.", + php_manual: "function.is-file", +} + +/// Lowers an `is_file` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_is_file(ctx, inst) +} diff --git a/src/builtins/io/is_link.rs b/src/builtins/io/is_link.rs new file mode 100644 index 0000000000..7be57e7bf5 --- /dev/null +++ b/src/builtins/io/is_link.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `is_link` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `is_link` is a pure-data builtin whose return type +//! (`Bool`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_is_link` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_link", + area: Io, + params: [filename: Str], + returns: Bool, + lower: lower, + summary: "Tells whether the filename is a symbolic link.", + php_manual: "function.is-link", +} + +/// Lowers an `is_link` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_is_link(ctx, inst) +} diff --git a/src/builtins/io/is_readable.rs b/src/builtins/io/is_readable.rs new file mode 100644 index 0000000000..68836c438b --- /dev/null +++ b/src/builtins/io/is_readable.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `is_readable` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `is_readable` is a pure-data builtin whose return +//! type (`Bool`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_is_readable` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_readable", + area: Io, + params: [filename: Str], + returns: Bool, + lower: lower, + summary: "Tells whether the filename is readable.", + php_manual: "function.is-readable", +} + +/// Lowers an `is_readable` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_is_readable(ctx, inst) +} diff --git a/src/builtins/io/is_writable.rs b/src/builtins/io/is_writable.rs new file mode 100644 index 0000000000..7df086abe1 --- /dev/null +++ b/src/builtins/io/is_writable.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `is_writable` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `is_writable` is a pure-data builtin whose return +//! type (`Bool`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_is_writable` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_writable", + area: Io, + params: [filename: Str], + returns: Bool, + lower: lower, + summary: "Tells whether the filename is writable.", + php_manual: "function.is-writable", +} + +/// Lowers an `is_writable` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_is_writable(ctx, inst) +} diff --git a/src/builtins/io/is_writeable.rs b/src/builtins/io/is_writeable.rs new file mode 100644 index 0000000000..59d12fc101 --- /dev/null +++ b/src/builtins/io/is_writeable.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `is_writeable` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `is_writeable` is a pure-data builtin whose return +//! type (`Bool`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `is_writeable` is an alias for `is_writable`; both share the same lowering. +//! - `lower` is a thin wrapper over `io::lower_is_writeable` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_writeable", + area: Io, + params: [filename: Str], + returns: Bool, + lower: lower, + summary: "Tells whether the filename is writable (alias of is_writable).", + php_manual: "function.is-writable", +} + +/// Lowers an `is_writeable` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_is_writeable(ctx, inst) +} diff --git a/src/builtins/io/lchgrp.rs b/src/builtins/io/lchgrp.rs new file mode 100644 index 0000000000..05a29b83be --- /dev/null +++ b/src/builtins/io/lchgrp.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `lchgrp` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Bool` and requires the `group` argument to be `Int` or `Str`. +//! `lchgrp` changes the group of a symlink itself rather than its target. +//! - `lower` is a thin wrapper over `io::lower_lchgrp` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "lchgrp", + area: Io, + params: [filename: Str, group: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Changes group ownership of a symlink.", + php_manual: "function.lchgrp", +} + +/// Returns `Bool`, rejecting a `group` argument that is neither `Int` nor `Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let principal_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(principal_ty, PhpType::Int | PhpType::Str) { + return Err(CompileError::new( + cx.args[1].span, + &format!("{}() owner/group must be int or string", cx.name), + )); + } + Ok(PhpType::Bool) +} + +/// Lowers an `lchgrp` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_lchgrp(ctx, inst) +} diff --git a/src/builtins/io/lchown.rs b/src/builtins/io/lchown.rs new file mode 100644 index 0000000000..ed64ebfc13 --- /dev/null +++ b/src/builtins/io/lchown.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `lchown` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Bool` and requires the `user` argument to be `Int` or `Str`. +//! `lchown` changes the owner of a symlink itself rather than its target. +//! - `lower` is a thin wrapper over `io::lower_lchown` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "lchown", + area: Io, + params: [filename: Str, user: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Changes user ownership of a symlink.", + php_manual: "function.lchown", +} + +/// Returns `Bool`, rejecting a `user` argument that is neither `Int` nor `Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let principal_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(principal_ty, PhpType::Int | PhpType::Str) { + return Err(CompileError::new( + cx.args[1].span, + &format!("{}() owner/group must be int or string", cx.name), + )); + } + Ok(PhpType::Bool) +} + +/// Lowers an `lchown` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_lchown(ctx, inst) +} diff --git a/src/builtins/io/link.rs b/src/builtins/io/link.rs new file mode 100644 index 0000000000..bb1dea7342 --- /dev/null +++ b/src/builtins/io/link.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `link` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `link` is a pure-data builtin whose `Bool` return type is +//! fully determined by its declaration. The registry common path infers the +//! arguments and enforces the exactly-2-argument arity before falling back to +//! `returns`. +//! - `lower` is a thin wrapper over `io::lower_link` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "link", + area: Io, + params: [target: Str, link: Str], + returns: Bool, + lower: lower, + summary: "Creates a hard link.", + php_manual: "function.link", +} + +/// Lowers a `link` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_link(ctx, inst) +} diff --git a/src/builtins/io/linkinfo.rs b/src/builtins/io/linkinfo.rs new file mode 100644 index 0000000000..b6d6f2d0f2 --- /dev/null +++ b/src/builtins/io/linkinfo.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `linkinfo` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `linkinfo` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_linkinfo` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "linkinfo", + area: Io, + params: [path: Str], + returns: Int, + lower: lower, + summary: "Gets information about a link.", + php_manual: "function.linkinfo", +} + +/// Lowers a `linkinfo` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_linkinfo(ctx, inst) +} diff --git a/src/builtins/io/lstat.rs b/src/builtins/io/lstat.rs new file mode 100644 index 0000000000..167b55cb94 --- /dev/null +++ b/src/builtins/io/lstat.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Home of the PHP `lstat` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `assoc-array|bool` via `stat_result_type`, reflecting +//! PHP behaviour where `lstat` returns the stat buffer array on success or `false` on failure. +//! Unlike `stat`, `lstat` does not follow symbolic links. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_lstat` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "lstat", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gives information about a file or symbolic link.", + php_manual: "function.lstat", +} + +/// Returns `assoc-array|bool` reflecting that `lstat` returns a buffer or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(crate::builtins::io::stat_support::stat_result_type(cx.checker)) +} + +/// Lowers an `lstat` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_lstat(ctx, inst) +} diff --git a/src/builtins/io/mkdir.rs b/src/builtins/io/mkdir.rs new file mode 100644 index 0000000000..0772785792 --- /dev/null +++ b/src/builtins/io/mkdir.rs @@ -0,0 +1,33 @@ +//! Purpose: +//! Home of the PHP `mkdir` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `mkdir` is a pure-data builtin whose `Bool` return type is +//! fully determined by its declaration. Unlike `unlink`, `mkdir` has no PHAR +//! side effect, so no library-linking check hook is required. The registry +//! common path infers the argument and enforces the exactly-1-argument arity +//! before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_mkdir` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "mkdir", + area: Io, + params: [directory: Str], + returns: Bool, + lower: lower, + summary: "Makes a directory.", + php_manual: "function.mkdir", +} + +/// Lowers a `mkdir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_mkdir(ctx, inst) +} diff --git a/src/builtins/io/mod.rs b/src/builtins/io/mod.rs new file mode 100644 index 0000000000..5febc5606f --- /dev/null +++ b/src/builtins/io/mod.rs @@ -0,0 +1,192 @@ +//! Purpose: +//! Groups all `io`-area path, debug, stat, and filesystem builtin homes into this +//! module so the registry can collect them in one place. Each submodule declares +//! exactly one builtin via `builtin!` and provides its lowering hook (and optional +//! check hook). +//! +//! Called from: +//! - `crate::builtins` (`mod io;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - Pure-data builtins (no check hook): var_dump, print_r, basename, +//! realpath_cache_size, file_exists, is_file, is_dir, is_readable, is_writable, +//! is_writeable, is_executable, is_link, filesize, filemtime, linkinfo, +//! disk_free_space, disk_total_space, clearstatcache, getcwd, sys_get_temp_dir, +//! tempnam, copy, rename, mkdir, rmdir, chdir, symlink, link, umask. +//! - Check-hook builtins: dirname (levels >= 1 constraint), fnmatch (flags type check), +//! realpath (returns Union(Str, Bool)), realpath_cache_get (returns AssocArray{Str, Mixed}), +//! pathinfo (flag-dependent return type with static constant folding), +//! fileatime/filectime/fileperms/fileowner/filegroup/fileinode (Union(Int, Bool)), +//! filetype (Union(Str, Bool)), stat/lstat/fstat (assoc-array|bool), +//! file/scandir/glob (Array), readfile (Union(Int, Bool)), +//! readlink (Union(Str, Bool)), chmod (mode must be int), +//! chown/chgrp/lchown/lchgrp (owner/group must be int or string), touch (timestamp +//! validation via `check_touch`). +//! - Library-linking check hooks: file_get_contents (TLS / PHAR / z / bz2), +//! file_put_contents (PHAR / crypto), hash_file (crypto), unlink (PHAR). +//! - Internal PHAR intrinsics (`internal: true`): all 16 `__elephc_phar_*` builtins +//! migrated from `src/types/checker/builtins/io/files.rs` (io batch C2). +//! - `pathinfo` owns the relocated `pathinfo_static_flag_value` helper (was in io/paths.rs). +//! - `stat_support` holds `stat_result_type` shared by stat/lstat/fstat check hooks. +//! - `touch` owns the relocated `check_touch` helper (was in io/files.rs). +//! - Add `pub mod ;` here for every new io builtin home. + +pub mod __elephc_phar_bzip2_archive; +pub mod __elephc_phar_decompress_archive; +pub mod __elephc_phar_get_file_metadata; +pub mod __elephc_phar_get_metadata; +pub mod __elephc_phar_get_signature_hash; +pub mod __elephc_phar_get_signature_type; +pub mod __elephc_phar_get_stub; +pub mod __elephc_phar_gzip_archive; +pub mod __elephc_phar_list_entries; +pub mod __elephc_phar_set_compression; +pub mod __elephc_phar_set_file_metadata; +pub mod __elephc_phar_set_metadata; +pub mod __elephc_phar_set_stub; +pub mod __elephc_phar_set_zip_password; +pub mod __elephc_phar_sign_hash; +pub mod __elephc_phar_sign_openssl; +pub mod basename; +pub mod chdir; +pub mod chgrp; +pub mod chmod; +pub mod chown; +pub mod clearstatcache; +pub mod closedir; +pub mod copy; +pub mod dirname; +pub mod disk_free_space; +pub mod disk_total_space; +pub mod fclose; +pub mod fdatasync; +pub mod feof; +pub mod fflush; +pub mod fgetc; +pub mod fgetcsv; +pub mod fgets; +pub mod file; +pub mod file_exists; +pub mod file_get_contents; +pub mod file_put_contents; +pub mod fileatime; +pub mod filectime; +pub mod filegroup; +pub mod fileinode; +pub mod filemtime; +pub mod fileowner; +pub mod fileperms; +pub mod filesize; +pub mod filetype; +pub mod flock; +pub mod fnmatch; +pub mod fopen; +pub mod fpassthru; +pub mod fprintf; +pub mod fputcsv; +pub mod fread; +pub mod fscanf; +pub mod fseek; +pub mod fstat; +pub mod fsync; +pub mod ftell; +pub mod ftruncate; +pub mod fsockopen; +pub mod fwrite; +pub mod getcwd; +pub mod gethostbyaddr; +pub mod gethostbyname; +pub mod gethostname; +pub mod getprotobyname; +pub mod getprotobynumber; +pub mod getservbyname; +pub mod getservbyport; +pub mod glob; +pub mod hash_file; +pub mod is_dir; +pub mod is_executable; +pub mod is_file; +pub mod is_link; +pub mod is_readable; +pub mod is_writable; +pub mod is_writeable; +pub mod lchgrp; +pub mod lchown; +pub mod link; +pub mod linkinfo; +pub mod lstat; +pub mod mkdir; +pub mod opendir; +pub mod pathinfo; +pub mod pclose; +pub mod pfsockopen; +pub mod popen; +pub mod print_r; +pub mod readdir; +pub mod readfile; +pub mod readline; +pub mod readlink; +pub mod realpath; +pub mod realpath_cache_get; +pub mod realpath_cache_size; +pub mod rename; +pub mod rewind; +pub mod rewinddir; +pub mod rmdir; +pub mod scandir; +pub mod stat; +pub(crate) mod stat_support; +pub mod stream_bucket_append; +pub mod stream_bucket_make_writeable; +pub mod stream_bucket_new; +pub mod stream_bucket_prepend; +pub mod stream_context_create; +pub mod stream_context_get_default; +pub mod stream_context_get_options; +pub mod stream_context_get_params; +pub mod stream_context_set_default; +pub mod stream_context_set_option; +pub mod stream_context_set_params; +pub mod stream_copy_to_stream; +pub mod stream_filter_append; +pub mod stream_filter_prepend; +pub mod stream_filter_register; +pub mod stream_filter_remove; +pub mod stream_get_contents; +pub mod stream_get_filters; +pub mod stream_get_line; +pub mod stream_get_meta_data; +pub mod stream_get_transports; +pub mod stream_get_wrappers; +pub mod stream_is_local; +pub mod stream_isatty; +pub mod stream_resolve_include_path; +pub mod stream_select; +pub mod stream_set_blocking; +pub mod stream_set_chunk_size; +pub mod stream_set_read_buffer; +pub mod stream_set_timeout; +pub mod stream_set_write_buffer; +pub mod stream_socket_accept; +pub mod stream_socket_client; +pub mod stream_socket_enable_crypto; +pub mod stream_socket_get_name; +pub mod stream_socket_pair; +pub mod stream_socket_recvfrom; +pub mod stream_socket_sendto; +pub mod stream_socket_server; +pub mod stream_socket_shutdown; +pub(crate) mod stream_support; +pub mod stream_supports_lock; +pub mod stream_wrapper_register; +pub mod stream_wrapper_restore; +pub mod stream_wrapper_unregister; +pub mod symlink; +pub mod sys_get_temp_dir; +pub mod tempnam; +pub mod tmpfile; +pub mod touch; +pub mod umask; +pub mod unlink; +pub mod var_dump; +pub mod vfprintf; diff --git a/src/builtins/io/opendir.rs b/src/builtins/io/opendir.rs new file mode 100644 index 0000000000..c4caf283d1 --- /dev/null +++ b/src/builtins/io/opendir.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `opendir` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(stream_resource, Bool)` to reflect PHP's false-on-failure +//! pattern. The `directory` argument is a path string, not a resource — it is +//! pre-inferred by the registry and no resource validation is performed. +//! - `returns: Mixed` is used because the union involves a resource type that the +//! scalar `returns:` field cannot express. +//! - `lower` is a thin wrapper over `io::lower_opendir` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "opendir", + area: Io, + params: [directory: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Open directory handle.", + php_manual: "function.opendir", +} + +/// Returns `Union(stream_resource, Bool)` for the directory open result. +/// +/// The `directory` argument is a path string, not a stream resource; no resource +/// validation is performed here. The common registry path pre-infers the argument. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![ + PhpType::stream_resource(), + PhpType::Bool, + ])) +} + +/// Lowers an `opendir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_opendir(ctx, inst) +} diff --git a/src/builtins/io/pathinfo.rs b/src/builtins/io/pathinfo.rs new file mode 100644 index 0000000000..3c5561156e --- /dev/null +++ b/src/builtins/io/pathinfo.rs @@ -0,0 +1,110 @@ +//! Purpose: +//! Home of the PHP `pathinfo` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the optional `flags` argument is `Int`, evaluates it statically +//! where possible via `pathinfo_static_flag_value`, and returns either `AssocArray{Str,Str}`, +//! `Str`, or a union depending on the flag value. +//! - `pathinfo_static_flag_value` is a private helper that resolves `PATHINFO_*` constants +//! at compile time; it was relocated verbatim from `src/types/checker/builtins/io/paths.rs`. +//! - The registry pre-infers arguments before calling the hook; the hook re-infers the +//! optional `flags` argument (idempotent) to obtain its resolved type. +//! - `lower` is a thin wrapper over `io::lower_pathinfo` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::{BinOp, Expr, ExprKind}; +use crate::types::PhpType; + +builtin! { + name: "pathinfo", + area: Io, + params: [path: Str, flags: Int = DefaultSpec::Int(15)], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns information about a file path.", + php_manual: "function.pathinfo", +} + +/// Validates `pathinfo()` flag argument and returns the refined return type. +/// +/// Infers the optional `flags` argument (idempotent after registry pre-inference), +/// requires it to be `Int`, and resolves its static value via `pathinfo_static_flag_value`. +/// Returns `AssocArray{Str,Str}` for no-flag or `PATHINFO_ALL` (15), `Str` for a known +/// specific flag, or a union `Union(Str, AssocArray{Str,Str})` for a dynamic/unknown flag. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let flag = match cx.args.get(1) { + Some(flag_expr) => { + let flag_ty = cx.checker.infer_type(flag_expr, cx.env)?; + if flag_ty != PhpType::Int { + return Err(CompileError::new( + cx.args[1].span, + "pathinfo() flag must be int", + )); + } + pathinfo_static_flag_value(flag_expr) + } + None => None, + }; + if cx.args.get(1).is_none() || flag == Some(15) { + Ok(PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + }) + } else if flag.is_none() { + Ok(cx.checker.normalize_union_type(vec![ + PhpType::Str, + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + }, + ])) + } else { + Ok(PhpType::Str) + } +} + +/// Extracts a literal `PATHINFO_*` constant value from `flag` expression at compile time. +/// +/// Handles integer literals, `PATHINFO_*` constants (`PATHINFO_DIRNAME`=1, `PATHINFO_BASENAME`=2, +/// `PATHINFO_EXTENSION`=4, `PATHINFO_FILENAME`=8, `PATHINFO_ALL`=15), negation, and bitwise +/// combinators (`|`, `&`, `^`). Returns `None` for non-static expressions (variables, function +/// calls, etc.) so the `check` hook can fall back to a union type. +fn pathinfo_static_flag_value(flag: &Expr) -> Option { + match &flag.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::ConstRef(name) => match name.as_str() { + "PATHINFO_DIRNAME" => Some(1), + "PATHINFO_BASENAME" => Some(2), + "PATHINFO_EXTENSION" => Some(4), + "PATHINFO_FILENAME" => Some(8), + "PATHINFO_ALL" => Some(15), + _ => None, + }, + ExprKind::Negate(inner) => pathinfo_static_flag_value(inner).map(|value| -value), + ExprKind::BinaryOp { left, op, right } => { + let left = pathinfo_static_flag_value(left)?; + let right = pathinfo_static_flag_value(right)?; + match op { + BinOp::BitAnd => Some(left & right), + BinOp::BitOr => Some(left | right), + BinOp::BitXor => Some(left ^ right), + _ => None, + } + } + _ => None, + } +} + +/// Lowers a `pathinfo` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_pathinfo(ctx, inst) +} diff --git a/src/builtins/io/pclose.rs b/src/builtins/io/pclose.rs new file mode 100644 index 0000000000..9dfd54667f --- /dev/null +++ b/src/builtins/io/pclose.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `pclose` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `handle` argument is a stream resource and returns `Int`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_pclose` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "pclose", + area: Io, + params: [handle: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Closes process file pointer.", + php_manual: "function.pclose", +} + +/// Validates the handle argument is a stream resource and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Int) +} + +/// Lowers a `pclose` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_pclose(ctx, inst) +} diff --git a/src/builtins/io/pfsockopen.rs b/src/builtins/io/pfsockopen.rs new file mode 100644 index 0000000000..3293adc4bd --- /dev/null +++ b/src/builtins/io/pfsockopen.rs @@ -0,0 +1,65 @@ +//! Purpose: +//! Home of the PHP `pfsockopen` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that `error_code` (arg[2]) and `error_message` (arg[3]), if provided, +//! are plain variables (they are written by reference). Returns `Union(stream_resource, Bool)`. +//! - Shares the same params, check logic, and lower target as `fsockopen`; `cx.name` is used +//! in error messages so diagnostics name `pfsockopen` correctly. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` dispatches to `io::lower_fsockopen` in the EIR backend (shared emitter). + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "pfsockopen", + area: Io, + params: [ + hostname: Str, + port: Int, + ref error_code: Mixed = DefaultSpec::Null, + ref error_message: Mixed = DefaultSpec::Null, + timeout: Mixed = DefaultSpec::Null + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Open persistent Internet or Unix domain socket connection.", + php_manual: "function.pfsockopen", +} + +/// Validates ref output params are plain variables, then returns `Union(stream_resource, Bool)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(ec) = cx.args.get(2) { + if !matches!(ec.kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + ec.span, + &format!("{}() parameter $error_code must be passed a variable", cx.name), + )); + } + } + if let Some(em) = cx.args.get(3) { + if !matches!(em.kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + em.span, + &format!("{}() parameter $error_message must be passed a variable", cx.name), + )); + } + } + Ok(cx.checker.normalize_union_type(vec![PhpType::stream_resource(), PhpType::Bool])) +} + +/// Lowers a `pfsockopen` call by dispatching to the shared io emitter (same as `fsockopen`). +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_fsockopen(ctx, inst) +} diff --git a/src/builtins/io/popen.rs b/src/builtins/io/popen.rs new file mode 100644 index 0000000000..364c249160 --- /dev/null +++ b/src/builtins/io/popen.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `popen` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(stream_resource, Bool)` to reflect PHP's false-on-failure +//! pattern. The arguments are a command string and mode string, not resources — they +//! are pre-inferred by the registry and no resource validation is performed. +//! - `returns: Mixed` is used because the union involves a resource type that the +//! scalar `returns:` field cannot express. +//! - `lower` is a thin wrapper over `io::lower_popen` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "popen", + area: Io, + params: [command: Str, mode: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Opens process file pointer.", + php_manual: "function.popen", +} + +/// Returns `Union(stream_resource, Bool)` for the pipe open result. +/// +/// The arguments are command and mode strings, not stream resources; no resource +/// validation is performed here. The common registry path pre-infers the arguments. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![ + PhpType::stream_resource(), + PhpType::Bool, + ])) +} + +/// Lowers a `popen` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_popen(ctx, inst) +} diff --git a/src/builtins/io/print_r.rs b/src/builtins/io/print_r.rs new file mode 100644 index 0000000000..84ca7ef27b --- /dev/null +++ b/src/builtins/io/print_r.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `print_r` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `print_r` is a pure-data builtin whose return type +//! (`Void`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `debug::lower_print_r` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "print_r", + area: Io, + params: [value: Mixed], + returns: Void, + lower: lower, + summary: "Prints human-readable information about a variable.", + php_manual: "function.print-r", +} + +/// Lowers a `print_r` call by dispatching to the shared debug emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::debug::lower_print_r(ctx, inst) +} diff --git a/src/builtins/io/readdir.rs b/src/builtins/io/readdir.rs new file mode 100644 index 0000000000..0d91fc5972 --- /dev/null +++ b/src/builtins/io/readdir.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Home of the PHP `readdir` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `dir_handle` argument is a stream resource and returns +//! `Union(Str, Bool)` to reflect PHP's false-on-failure pattern. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar +//! `returns:` field. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_readdir` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "readdir", + area: Io, + params: [dir_handle: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Read entry from directory handle.", + php_manual: "function.readdir", +} + +/// Validates the directory handle is a stream resource and returns `Union(Str, Bool)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(cx.checker.normalize_union_type(vec![ + PhpType::Str, + PhpType::Bool, + ])) +} + +/// Lowers a `readdir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_readdir(ctx, inst) +} diff --git a/src/builtins/io/readfile.rs b/src/builtins/io/readfile.rs new file mode 100644 index 0000000000..9471aa536f --- /dev/null +++ b/src/builtins/io/readfile.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `readfile` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `normalize_union_type([Int, Bool])` reflecting PHP behaviour +//! where `readfile` outputs the file and returns the byte count or `false` on +//! failure. A check hook is required because the union return cannot be expressed +//! through the scalar `returns:` field. +//! - `lower` is a thin wrapper over `io::lower_readfile` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "readfile", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Outputs a file.", + php_manual: "function.readfile", +} + +/// Returns `Union(Int, Bool)` reflecting the byte count on success or `false` on failure. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `readfile` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_readfile(ctx, inst) +} diff --git a/src/builtins/io/readline.rs b/src/builtins/io/readline.rs new file mode 100644 index 0000000000..ebcdc5072f --- /dev/null +++ b/src/builtins/io/readline.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `readline` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` to match PHP's false-on-failure pattern for +//! end-of-input. The `prompt` argument is optional and pre-inferred by the registry. +//! - `arity_error` is overridden to "readline() takes 0 or 1 arguments" because the +//! registry's default message for min0/max1 ("takes at most 1 argument") does not +//! match the legacy error text. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar +//! `returns:` field. +//! - `lower` is a thin wrapper over `io::lower_readline` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "readline", + area: Io, + params: [prompt: Str = DefaultSpec::Null], + arity_error: "readline() takes 0 or 1 arguments", + returns: Mixed, + check: check, + lower: lower, + summary: "Reads a line from the user's terminal.", + php_manual: "function.readline", +} + +/// Returns `Union(Str, Bool)` for the readline result (false on end-of-input). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![ + PhpType::Str, + PhpType::Bool, + ])) +} + +/// Lowers a `readline` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_readline(ctx, inst) +} diff --git a/src/builtins/io/readlink.rs b/src/builtins/io/readlink.rs new file mode 100644 index 0000000000..b493414059 --- /dev/null +++ b/src/builtins/io/readlink.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `readlink` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `normalize_union_type([Str, Bool])` reflecting PHP behaviour +//! where `readlink` returns the symlink target or `false` on failure. A check hook +//! is required because the union return cannot be expressed through the scalar +//! `returns:` field. +//! - `lower` is a thin wrapper over `io::lower_readlink` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "readlink", + area: Io, + params: [path: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns the target of a symbolic link.", + php_manual: "function.readlink", +} + +/// Returns `Union(Str, Bool)` reflecting the link target on success or `false` on failure. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `readlink` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_readlink(ctx, inst) +} diff --git a/src/builtins/io/realpath.rs b/src/builtins/io/realpath.rs new file mode 100644 index 0000000000..44cb3bfadd --- /dev/null +++ b/src/builtins/io/realpath.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `realpath` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` to reflect PHP's behaviour where `realpath` +//! returns the resolved path on success or `false` if the path cannot be resolved. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_realpath` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "realpath", + area: Io, + params: [path: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns canonicalized absolute pathname.", + php_manual: "function.realpath", +} + +/// Returns `Union(Str, Bool)` reflecting that `realpath` can return a path or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `realpath` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_realpath(ctx, inst) +} diff --git a/src/builtins/io/realpath_cache_get.rs b/src/builtins/io/realpath_cache_get.rs new file mode 100644 index 0000000000..5776200ffd --- /dev/null +++ b/src/builtins/io/realpath_cache_get.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `realpath_cache_get` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `AssocArray{Str, Mixed}` to reflect the cache map structure. +//! - `arity_error` is overridden to preserve the legacy message +//! "realpath_cache_get() takes exactly 0 arguments" (the registry default for +//! 0-arg builtins produces "takes no arguments"). +//! - `lower` is a thin wrapper over `io::lower_realpath_cache_get` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "realpath_cache_get", + area: Io, + params: [], + arity_error: "realpath_cache_get() takes exactly 0 arguments", + returns: Mixed, + check: check, + lower: lower, + summary: "Returns realpath cache entries.", + php_manual: "function.realpath-cache-get", +} + +/// Returns `AssocArray{Str, Mixed}` reflecting the realpath cache structure. +/// +/// The registry enforces 0-argument arity via `arity_error` before calling this hook. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }) +} + +/// Lowers a `realpath_cache_get` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_realpath_cache_get(ctx, inst) +} diff --git a/src/builtins/io/realpath_cache_size.rs b/src/builtins/io/realpath_cache_size.rs new file mode 100644 index 0000000000..43ab8911e4 --- /dev/null +++ b/src/builtins/io/realpath_cache_size.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `realpath_cache_size` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `realpath_cache_size` is a pure-data builtin whose +//! return type (`Int`) is fully determined by its declaration. +//! - `arity_error` is overridden to preserve the legacy message +//! "realpath_cache_size() takes exactly 0 arguments" (the registry default for +//! 0-arg builtins produces "takes no arguments"). +//! - `lower` is a thin wrapper over `io::lower_realpath_cache_size` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "realpath_cache_size", + area: Io, + params: [], + arity_error: "realpath_cache_size() takes exactly 0 arguments", + returns: Int, + lower: lower, + summary: "Returns the amount of memory used by the realpath cache.", + php_manual: "function.realpath-cache-size", +} + +/// Lowers a `realpath_cache_size` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_realpath_cache_size(ctx, inst) +} diff --git a/src/builtins/io/rename.rs b/src/builtins/io/rename.rs new file mode 100644 index 0000000000..85b5be1441 --- /dev/null +++ b/src/builtins/io/rename.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `rename` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `rename` is a pure-data builtin whose `Bool` return type is +//! fully determined by its declaration. The registry common path infers the +//! arguments and enforces the exactly-2-argument arity before falling back to +//! `returns`. +//! - `lower` is a thin wrapper over `io::lower_rename` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "rename", + area: Io, + params: [from: Str, to: Str], + returns: Bool, + lower: lower, + summary: "Renames a file or directory.", + php_manual: "function.rename", +} + +/// Lowers a `rename` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_rename(ctx, inst) +} diff --git a/src/builtins/io/rewind.rs b/src/builtins/io/rewind.rs new file mode 100644 index 0000000000..821cbf66b1 --- /dev/null +++ b/src/builtins/io/rewind.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `rewind` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Bool`. Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_rewind` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "rewind", + area: Io, + params: [stream: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Rewind the position of a file pointer.", + php_manual: "function.rewind", +} + +/// Validates the stream argument is a stream resource and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `rewind` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_rewind(ctx, inst) +} diff --git a/src/builtins/io/rewinddir.rs b/src/builtins/io/rewinddir.rs new file mode 100644 index 0000000000..85223550a7 --- /dev/null +++ b/src/builtins/io/rewinddir.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `rewinddir` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the `dir_handle` argument is a stream resource and returns `Void`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_rewinddir` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "rewinddir", + area: Io, + params: [dir_handle: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Rewind directory handle.", + php_manual: "function.rewinddir", +} + +/// Validates the directory handle is a stream resource and returns `Void`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Void) +} + +/// Lowers a `rewinddir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_rewinddir(ctx, inst) +} diff --git a/src/builtins/io/rmdir.rs b/src/builtins/io/rmdir.rs new file mode 100644 index 0000000000..c12f6f3a74 --- /dev/null +++ b/src/builtins/io/rmdir.rs @@ -0,0 +1,33 @@ +//! Purpose: +//! Home of the PHP `rmdir` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `rmdir` is a pure-data builtin whose `Bool` return type is +//! fully determined by its declaration. Unlike `unlink`, `rmdir` has no PHAR +//! side effect, so no library-linking check hook is required. The registry +//! common path infers the argument and enforces the exactly-1-argument arity +//! before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_rmdir` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "rmdir", + area: Io, + params: [directory: Str], + returns: Bool, + lower: lower, + summary: "Removes a directory.", + php_manual: "function.rmdir", +} + +/// Lowers a `rmdir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_rmdir(ctx, inst) +} diff --git a/src/builtins/io/scandir.rs b/src/builtins/io/scandir.rs new file mode 100644 index 0000000000..209e45d8f6 --- /dev/null +++ b/src/builtins/io/scandir.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `scandir` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Array` (the directory entries). A check hook is required +//! because the array return type cannot be expressed through the scalar `returns:` +//! field. +//! - `lower` is a thin wrapper over `io::lower_scandir` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "scandir", + area: Io, + params: [directory: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Lists files and directories inside the specified path.", + php_manual: "function.scandir", +} + +/// Returns `Array` reflecting that `scandir` yields directory entry names. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `scandir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_scandir(ctx, inst) +} diff --git a/src/builtins/io/stat.rs b/src/builtins/io/stat.rs new file mode 100644 index 0000000000..ee1746616d --- /dev/null +++ b/src/builtins/io/stat.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `stat` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `assoc-array|bool` via `stat_result_type`, reflecting +//! PHP behaviour where `stat` returns the stat buffer array on success or `false` on failure. +//! - The registry pre-infers arguments before calling this hook. +//! - `lower` is a thin wrapper over `io::lower_stat` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stat", + area: Io, + params: [filename: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gives information about a file.", + php_manual: "function.stat", +} + +/// Returns `assoc-array|bool` reflecting that `stat` returns a buffer or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(crate::builtins::io::stat_support::stat_result_type(cx.checker)) +} + +/// Lowers a `stat` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stat(ctx, inst) +} diff --git a/src/builtins/io/stat_support.rs b/src/builtins/io/stat_support.rs new file mode 100644 index 0000000000..3f299dda05 --- /dev/null +++ b/src/builtins/io/stat_support.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Shared helper for the stat/lstat/fstat builtin homes in the io area. +//! Provides the `stat_result_type` helper that returns the normalized PHP return +//! type for stat-family functions (`assoc-array|bool`). +//! +//! Called from: +//! - `crate::builtins::io::stat` (check hook) +//! - `crate::builtins::io::lstat` (check hook) +//! - `crate::builtins::io::fstat` (check hook) +//! +//! Key details: +//! - The union type reflects PHP's stat functions returning `array|false`: the +//! AssocArray represents the stat buffer (mode, ino, uid, etc. as int values), +//! and Bool represents the `false` return on failure. +//! - `Mixed` is used as the key type to reflect PHP's heterogeneous array indexing +//! (stat arrays are accessible by both numeric and string keys). + +use crate::types::checker::Checker; +use crate::types::PhpType; + +/// Returns the normalized return type for `stat()` / `lstat()` / `fstat()`. +/// +/// Produces `assoc-array|bool` as a normalized union type. PHP's stat functions +/// return `array|false` — the AssocArray represents the stat buffer keys (mode, ino, uid, etc. +/// as int values), and `Bool` represents the false return on failure. The `Mixed` key type +/// reflects PHP's heterogeneous array indexing (stat arrays are accessible by both numeric +/// and string keys). +pub(crate) fn stat_result_type(checker: &Checker) -> PhpType { + checker.normalize_union_type(vec![ + PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Int), + }, + PhpType::Bool, + ]) +} diff --git a/src/builtins/io/stream_bucket_append.rs b/src/builtins/io/stream_bucket_append.rs new file mode 100644 index 0000000000..8d55ff1767 --- /dev/null +++ b/src/builtins/io/stream_bucket_append.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `stream_bucket_append` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers both arguments and returns `Void`. +//! - `lower` dispatches to `io::lower_stream_bucket_append_or_prepend` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_bucket_append", + area: Io, + params: [brigade: Mixed, bucket: Mixed], + returns: Void, + lower: lower, + summary: "Appends a bucket to the brigade.", + php_manual: "function.stream-bucket-append", +} + +/// Lowers a `stream_bucket_append` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_bucket_append_or_prepend(ctx, inst) +} diff --git a/src/builtins/io/stream_bucket_make_writeable.rs b/src/builtins/io/stream_bucket_make_writeable.rs new file mode 100644 index 0000000000..3aace50fcd --- /dev/null +++ b/src/builtins/io/stream_bucket_make_writeable.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `stream_bucket_make_writeable` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers the single argument and returns `Mixed`. +//! - `lower` is a thin wrapper over `io::lower_stream_bucket_make_writeable` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_bucket_make_writeable", + area: Io, + params: [brigade: Mixed], + returns: Mixed, + lower: lower, + summary: "Returns a bucket object from the brigade for use in a stream filter.", + php_manual: "function.stream-bucket-make-writeable", +} + +/// Lowers a `stream_bucket_make_writeable` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_bucket_make_writeable(ctx, inst) +} diff --git a/src/builtins/io/stream_bucket_new.rs b/src/builtins/io/stream_bucket_new.rs new file mode 100644 index 0000000000..c1c9172f75 --- /dev/null +++ b/src/builtins/io/stream_bucket_new.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `stream_bucket_new` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers both arguments and returns `Mixed`. +//! - `lower` is a thin wrapper over `io::lower_stream_bucket_new` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_bucket_new", + area: Io, + params: [stream: Mixed, buffer: Str], + returns: Mixed, + lower: lower, + summary: "Creates a new bucket for use in a stream filter.", + php_manual: "function.stream-bucket-new", +} + +/// Lowers a `stream_bucket_new` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_bucket_new(ctx, inst) +} diff --git a/src/builtins/io/stream_bucket_prepend.rs b/src/builtins/io/stream_bucket_prepend.rs new file mode 100644 index 0000000000..dd259e0ab3 --- /dev/null +++ b/src/builtins/io/stream_bucket_prepend.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `stream_bucket_prepend` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers both arguments and returns `Void`. +//! - `lower` dispatches to `io::lower_stream_bucket_append_or_prepend` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_bucket_prepend", + area: Io, + params: [brigade: Mixed, bucket: Mixed], + returns: Void, + lower: lower, + summary: "Prepends a bucket to the brigade.", + php_manual: "function.stream-bucket-prepend", +} + +/// Lowers a `stream_bucket_prepend` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_bucket_append_or_prepend(ctx, inst) +} diff --git a/src/builtins/io/stream_context_create.rs b/src/builtins/io/stream_context_create.rs new file mode 100644 index 0000000000..5944f00608 --- /dev/null +++ b/src/builtins/io/stream_context_create.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `stream_context_create` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `PhpType::stream_resource()` which is not scalar-expressible, so +//! `returns: Mixed` is used and the hook overrides the return type. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer them. +//! - `lower` is a thin wrapper over `io::lower_stream_context_create` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_context_create", + area: Io, + params: [ + options: Mixed = DefaultSpec::Null, + params: Mixed = DefaultSpec::Null + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Creates a stream context.", + php_manual: "function.stream-context-create", +} + +/// Returns `stream_resource()` as the precise return type for `stream_context_create`. +/// +/// Arguments are pre-inferred by the registry; this hook only refines the return type +/// beyond what the scalar `returns: Mixed` field can express. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::stream_resource()) +} + +/// Lowers a `stream_context_create` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_context_create(ctx, inst) +} diff --git a/src/builtins/io/stream_context_get_default.rs b/src/builtins/io/stream_context_get_default.rs new file mode 100644 index 0000000000..aebb0ab45a --- /dev/null +++ b/src/builtins/io/stream_context_get_default.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `stream_context_get_default` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `PhpType::stream_resource()` which is not scalar-expressible, so +//! `returns: Mixed` is used and the hook overrides the return type. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer them. +//! - `lower` is a thin wrapper over `io::lower_stream_context_get_default` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_context_get_default", + area: Io, + params: [options: Mixed = DefaultSpec::Null], + returns: Mixed, + check: check, + lower: lower, + summary: "Retrieves the default stream context.", + php_manual: "function.stream-context-get-default", +} + +/// Returns `stream_resource()` as the precise return type for `stream_context_get_default`. +/// +/// Arguments are pre-inferred by the registry; this hook only refines the return type +/// beyond what the scalar `returns: Mixed` field can express. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::stream_resource()) +} + +/// Lowers a `stream_context_get_default` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_context_get_default(ctx, inst) +} diff --git a/src/builtins/io/stream_context_get_options.rs b/src/builtins/io/stream_context_get_options.rs new file mode 100644 index 0000000000..972e6d02ff --- /dev/null +++ b/src/builtins/io/stream_context_get_options.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `stream_context_get_options` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `AssocArray{Str, Mixed}` which is not scalar-expressible, so +//! `returns: Mixed` is used and the hook overrides the return type. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer them. +//! - `lower` is a thin wrapper over `io::lower_stream_context_get_options` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_context_get_options", + area: Io, + params: [context: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Retrieves options for the specified stream context.", + php_manual: "function.stream-context-get-options", +} + +/// Returns `AssocArray{Str, Mixed}` reflecting the context options map structure. +/// +/// Arguments are pre-inferred by the registry; this hook only refines the return type +/// beyond what the scalar `returns: Mixed` field can express. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }) +} + +/// Lowers a `stream_context_get_options` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_context_get_options(ctx, inst) +} diff --git a/src/builtins/io/stream_context_get_params.rs b/src/builtins/io/stream_context_get_params.rs new file mode 100644 index 0000000000..b16bd0236e --- /dev/null +++ b/src/builtins/io/stream_context_get_params.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `stream_context_get_params` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `AssocArray{Str, Mixed}` which is not scalar-expressible, so +//! `returns: Mixed` is used and the hook overrides the return type. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer them. +//! - `lower` is a thin wrapper over `io::lower_stream_context_get_params` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_context_get_params", + area: Io, + params: [context: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Retrieves parameters from the specified stream context.", + php_manual: "function.stream-context-get-params", +} + +/// Returns `AssocArray{Str, Mixed}` reflecting the context parameters map structure. +/// +/// Arguments are pre-inferred by the registry; this hook only refines the return type +/// beyond what the scalar `returns: Mixed` field can express. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }) +} + +/// Lowers a `stream_context_get_params` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_context_get_params(ctx, inst) +} diff --git a/src/builtins/io/stream_context_set_default.rs b/src/builtins/io/stream_context_set_default.rs new file mode 100644 index 0000000000..1f833ccd8c --- /dev/null +++ b/src/builtins/io/stream_context_set_default.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `stream_context_set_default` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `PhpType::stream_resource()` which is not scalar-expressible, so +//! `returns: Mixed` is used and the hook overrides the return type. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer them. +//! - `lower` is a thin wrapper over `io::lower_stream_context_set_default` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_context_set_default", + area: Io, + params: [options: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Sets the default stream context.", + php_manual: "function.stream-context-set-default", +} + +/// Returns `stream_resource()` as the precise return type for `stream_context_set_default`. +/// +/// Arguments are pre-inferred by the registry; this hook only refines the return type +/// beyond what the scalar `returns: Mixed` field can express. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::stream_resource()) +} + +/// Lowers a `stream_context_set_default` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_context_set_default(ctx, inst) +} diff --git a/src/builtins/io/stream_context_set_option.rs b/src/builtins/io/stream_context_set_option.rs new file mode 100644 index 0000000000..cad2cce85f --- /dev/null +++ b/src/builtins/io/stream_context_set_option.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Home of the PHP `stream_context_set_option` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers all arguments and returns `Bool`. +//! PHP accepts two call shapes — (ctx, options_array) or (ctx, wrapper, option, value) — +//! both accepted inertly. +//! - `lower` is a thin wrapper over `io::lower_stream_context_set_option` in the EIR backend. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_context_set_option", + area: Io, + params: [ + context: Mixed, + wrapper_or_options: Mixed, + option_name: Str = DefaultSpec::Null, + value: Mixed = DefaultSpec::Null + ], + returns: Bool, + lower: lower, + summary: "Sets an option on the specified context.", + php_manual: "function.stream-context-set-option", +} + +/// Lowers a `stream_context_set_option` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_context_set_option(ctx, inst) +} diff --git a/src/builtins/io/stream_context_set_params.rs b/src/builtins/io/stream_context_set_params.rs new file mode 100644 index 0000000000..25c036f80b --- /dev/null +++ b/src/builtins/io/stream_context_set_params.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `stream_context_set_params` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers both arguments and returns `Bool`. +//! - `lower` is a thin wrapper over `io::lower_stream_context_set_params` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_context_set_params", + area: Io, + params: [context: Mixed, params: Mixed], + returns: Bool, + lower: lower, + summary: "Sets parameters on the specified context.", + php_manual: "function.stream-context-set-params", +} + +/// Lowers a `stream_context_set_params` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_context_set_params(ctx, inst) +} diff --git a/src/builtins/io/stream_copy_to_stream.rs b/src/builtins/io/stream_copy_to_stream.rs new file mode 100644 index 0000000000..40fff73329 --- /dev/null +++ b/src/builtins/io/stream_copy_to_stream.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Home of the PHP `stream_copy_to_stream` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates both stream resource arguments, then validates `length` (int|null) and +//! `offset` (int) via `stream_support` helpers. Returns `Union(Int, Bool)`. +//! - `length` and `offset` are optional with defaults `null` and `-1` respectively. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` is a thin wrapper over `io::lower_stream_copy_to_stream` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::builtins::io::stream_support; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; +use crate::types::checker::builtins::io::common; + +builtin! { + name: "stream_copy_to_stream", + area: Io, + params: [ + from: Mixed, + to: Mixed, + length: Int = DefaultSpec::Null, + offset: Int = DefaultSpec::Int(-1) + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Copies data from one stream to another.", + php_manual: "function.stream-copy-to-stream", +} + +/// Validates both stream resource arguments, optional length (int|null), and optional offset (int). +/// Returns `Union(Int, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + common::ensure_stream_resource(cx.checker, cx.name, &cx.args[0], cx.env)?; + common::ensure_stream_resource(cx.checker, cx.name, &cx.args[1], cx.env)?; + if let Some(length) = cx.args.get(2) { + stream_support::ensure_optional_int(cx.checker, cx.name, "length", length, cx.env)?; + } + if let Some(offset) = cx.args.get(3) { + stream_support::ensure_int(cx.checker, cx.name, "offset", offset, cx.env)?; + } + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `stream_copy_to_stream` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_copy_to_stream(ctx, inst) +} diff --git a/src/builtins/io/stream_filter_append.rs b/src/builtins/io/stream_filter_append.rs new file mode 100644 index 0000000000..7b9e4d228d --- /dev/null +++ b/src/builtins/io/stream_filter_append.rs @@ -0,0 +1,90 @@ +//! Purpose: +//! Home of the PHP `stream_filter_append` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates stream resource arg[0], then matches on a literal filter name +//! to link the appropriate runtime libraries (zlib, iconv, bz2). Returns `Mixed`. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer args[2..]. The source deliberately does not infer arg[1] in the +//! StringLiteral branch (harmless since the common path infers it side-effect-free). +//! - `lower` dispatches to `io::lower_stream_filter_attach` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "stream_filter_append", + area: Io, + params: [ + stream: Mixed, + filtername: Str, + read_write: Int = DefaultSpec::Int(3), + params: Mixed = DefaultSpec::Null + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Attaches a filter to a stream.", + php_manual: "function.stream-filter-append", +} + +/// Validates the stream resource and links required filter libraries for known literal filter names. +/// +/// Checks that arg[0] is a stream resource. For a string-literal arg[1], links the appropriate +/// system library: `z` for zlib filters, `iconv` (macOS only) for iconv filters, `bz2` for +/// bzip2 filters. Dynamic filter names are routed through the runtime filter registry. Returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + match &cx.args[1].kind { + ExprKind::StringLiteral(filter) => { + // The zlib.* filters call into the system zlib, so any + // program that attaches one must link against libz. + if filter.as_str() == "zlib.deflate" || filter.as_str() == "zlib.inflate" { + cx.checker.require_builtin_library("z"); + } + // convert.iconv.* uses libc iconv: in libc on Linux + // (glibc/musl) but a separate library on macOS, so only + // macOS needs explicit -liconv linkage. + if filter.starts_with("convert.iconv.") { + cx.checker.require_macos_builtin_library("iconv"); + } + // The bzip2.* filters call into libbz2 (BZ2_bz*), so any + // program that attaches one must link against -lbz2. The + // existing compress.bzip2:// require fires only on the fopen + // path, not here, so this is the filter path's own wiring. + if filter.as_str() == "bzip2.compress" || filter.as_str() == "bzip2.decompress" { + cx.checker.require_builtin_library("bz2"); + } + // Unknown built-in names are routed through the user + // filter registry at runtime (Phase 10 tier 3); the + // helper returns PHP false for unregistered names. + } + _ => { + // Dynamic filter names resolve through the user filter + // registry at runtime. The codegen pulls the name from + // the expression result regs and the helper does the + // lookup. + cx.checker.infer_type(&cx.args[1], cx.env)?; + } + } + Ok(PhpType::Mixed) +} + +/// Lowers a `stream_filter_append` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_filter_attach(ctx, inst, "stream_filter_append") +} diff --git a/src/builtins/io/stream_filter_prepend.rs b/src/builtins/io/stream_filter_prepend.rs new file mode 100644 index 0000000000..9f56727b81 --- /dev/null +++ b/src/builtins/io/stream_filter_prepend.rs @@ -0,0 +1,90 @@ +//! Purpose: +//! Home of the PHP `stream_filter_prepend` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates stream resource arg[0], then matches on a literal filter name +//! to link the appropriate runtime libraries (zlib, iconv, bz2). Returns `Mixed`. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer args[2..]. The source deliberately does not infer arg[1] in the +//! StringLiteral branch (harmless since the common path infers it side-effect-free). +//! - `lower` dispatches to `io::lower_stream_filter_attach` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "stream_filter_prepend", + area: Io, + params: [ + stream: Mixed, + filtername: Str, + read_write: Int = DefaultSpec::Int(3), + params: Mixed = DefaultSpec::Null + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Attaches a filter to a stream (prepend).", + php_manual: "function.stream-filter-prepend", +} + +/// Validates the stream resource and links required filter libraries for known literal filter names. +/// +/// Checks that arg[0] is a stream resource. For a string-literal arg[1], links the appropriate +/// system library: `z` for zlib filters, `iconv` (macOS only) for iconv filters, `bz2` for +/// bzip2 filters. Dynamic filter names are routed through the runtime filter registry. Returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + match &cx.args[1].kind { + ExprKind::StringLiteral(filter) => { + // The zlib.* filters call into the system zlib, so any + // program that attaches one must link against libz. + if filter.as_str() == "zlib.deflate" || filter.as_str() == "zlib.inflate" { + cx.checker.require_builtin_library("z"); + } + // convert.iconv.* uses libc iconv: in libc on Linux + // (glibc/musl) but a separate library on macOS, so only + // macOS needs explicit -liconv linkage. + if filter.starts_with("convert.iconv.") { + cx.checker.require_macos_builtin_library("iconv"); + } + // The bzip2.* filters call into libbz2 (BZ2_bz*), so any + // program that attaches one must link against -lbz2. The + // existing compress.bzip2:// require fires only on the fopen + // path, not here, so this is the filter path's own wiring. + if filter.as_str() == "bzip2.compress" || filter.as_str() == "bzip2.decompress" { + cx.checker.require_builtin_library("bz2"); + } + // Unknown built-in names are routed through the user + // filter registry at runtime (Phase 10 tier 3); the + // helper returns PHP false for unregistered names. + } + _ => { + // Dynamic filter names resolve through the user filter + // registry at runtime. The codegen pulls the name from + // the expression result regs and the helper does the + // lookup. + cx.checker.infer_type(&cx.args[1], cx.env)?; + } + } + Ok(PhpType::Mixed) +} + +/// Lowers a `stream_filter_prepend` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_filter_attach(ctx, inst, "stream_filter_prepend") +} diff --git a/src/builtins/io/stream_filter_register.rs b/src/builtins/io/stream_filter_register.rs new file mode 100644 index 0000000000..b676b5ba9b --- /dev/null +++ b/src/builtins/io/stream_filter_register.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `stream_filter_register` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the class argument names a declared class and returns `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer them. +//! - `lower` is a thin wrapper over `io::lower_stream_filter_register` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_filter_register", + area: Io, + params: [filter_name: Str, class: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Registers a user-defined stream filter.", + php_manual: "function.stream-filter-register", +} + +/// Validates the class argument names a declared class and returns `Bool`. +/// +/// Arguments are pre-inferred by the registry; this hook validates the class +/// registration using the shared `validate_registered_stream_class` helper. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::builtins::io::stream_support::validate_registered_stream_class( + cx.checker, + cx.name, + &cx.args[1], + cx.span, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `stream_filter_register` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_filter_register(ctx, inst) +} diff --git a/src/builtins/io/stream_filter_remove.rs b/src/builtins/io/stream_filter_remove.rs new file mode 100644 index 0000000000..75f08980b8 --- /dev/null +++ b/src/builtins/io/stream_filter_remove.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `stream_filter_remove` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a stream resource before returning `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer them. +//! - `lower` is a thin wrapper over `io::lower_stream_filter_remove` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_filter_remove", + area: Io, + params: [stream_filter: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Removes a filter from a stream.", + php_manual: "function.stream-filter-remove", +} + +/// Validates the argument is a stream resource and returns `Bool`. +/// +/// Arguments are pre-inferred by the registry; this hook only validates the resource constraint. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `stream_filter_remove` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_filter_remove(ctx, inst) +} diff --git a/src/builtins/io/stream_get_contents.rs b/src/builtins/io/stream_get_contents.rs new file mode 100644 index 0000000000..44228e42c0 --- /dev/null +++ b/src/builtins/io/stream_get_contents.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Home of the PHP `stream_get_contents` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the stream resource, then validates `length` (int|null) and `offset` +//! (int) via `stream_support` helpers. Returns `Union(Str, Bool)`. +//! - `length` and `offset` are optional with defaults `null` and `-1` respectively. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` is a thin wrapper over `io::lower_stream_get_contents` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::builtins::io::stream_support; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; +use crate::types::checker::builtins::io::common; + +builtin! { + name: "stream_get_contents", + area: Io, + params: [ + stream: Mixed, + length: Int = DefaultSpec::Null, + offset: Int = DefaultSpec::Int(-1) + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Reads remainder of a stream into a string.", + php_manual: "function.stream-get-contents", +} + +/// Validates the stream resource, optional length (int|null), and optional offset (int). +/// Returns `Union(Str, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + common::ensure_stream_resource(cx.checker, cx.name, &cx.args[0], cx.env)?; + if let Some(length) = cx.args.get(1) { + stream_support::ensure_optional_int(cx.checker, cx.name, "length", length, cx.env)?; + } + if let Some(offset) = cx.args.get(2) { + stream_support::ensure_int(cx.checker, cx.name, "offset", offset, cx.env)?; + } + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `stream_get_contents` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_get_contents(ctx, inst) +} diff --git a/src/builtins/io/stream_get_filters.rs b/src/builtins/io/stream_get_filters.rs new file mode 100644 index 0000000000..8a80301373 --- /dev/null +++ b/src/builtins/io/stream_get_filters.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `stream_get_filters` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Array(Str)`, which is not scalar-expressible, so `returns: Mixed` is +//! used and the hook overrides the return type. The hook takes no arguments. +//! - `lower` is a thin wrapper over `io::lower_stream_get_filters` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_get_filters", + area: Io, + params: [], + returns: Mixed, + check: check, + lower: lower, + summary: "Retrieves list of registered filters.", + php_manual: "function.stream-get-filters", +} + +/// Returns `Array(Str)` as the precise return type for `stream_get_filters`. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `stream_get_filters` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_get_filters(ctx, inst) +} diff --git a/src/builtins/io/stream_get_line.rs b/src/builtins/io/stream_get_line.rs new file mode 100644 index 0000000000..9d67a65043 --- /dev/null +++ b/src/builtins/io/stream_get_line.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `stream_get_line` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the first argument is a stream resource before returning `Str`. +//! - `ending` is optional (defaults to empty string). Arguments are pre-inferred by the registry. +//! - `lower` is a thin wrapper over `io::lower_stream_get_line` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_get_line", + area: Io, + params: [stream: Mixed, length: Int, ending: Str = DefaultSpec::Str("")], + returns: Str, + check: check, + lower: lower, + summary: "Gets line from stream resource up to a given delimiter.", + php_manual: "function.stream-get-line", +} + +/// Validates the stream resource argument and returns `Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Str) +} + +/// Lowers a `stream_get_line` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_get_line(ctx, inst) +} diff --git a/src/builtins/io/stream_get_meta_data.rs b/src/builtins/io/stream_get_meta_data.rs new file mode 100644 index 0000000000..bb4b2bd61b --- /dev/null +++ b/src/builtins/io/stream_get_meta_data.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `stream_get_meta_data` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates the stream resource and returns `AssocArray{Str, Mixed}`, which is not +//! scalar-expressible, so `returns: Mixed` is used and the hook overrides the return type. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_stream_get_meta_data` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_get_meta_data", + area: Io, + params: [stream: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Retrieves metadata from streams/file pointers.", + php_manual: "function.stream-get-meta-data", +} + +/// Validates the stream resource and returns `AssocArray{Str, Mixed}`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }) +} + +/// Lowers a `stream_get_meta_data` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_get_meta_data(ctx, inst) +} diff --git a/src/builtins/io/stream_get_transports.rs b/src/builtins/io/stream_get_transports.rs new file mode 100644 index 0000000000..bcecdde19a --- /dev/null +++ b/src/builtins/io/stream_get_transports.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `stream_get_transports` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Array(Str)`, which is not scalar-expressible, so `returns: Mixed` is +//! used and the hook overrides the return type. The hook takes no arguments. +//! - `lower` is a thin wrapper over `io::lower_stream_get_transports` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_get_transports", + area: Io, + params: [], + returns: Mixed, + check: check, + lower: lower, + summary: "Retrieves list of registered socket transports.", + php_manual: "function.stream-get-transports", +} + +/// Returns `Array(Str)` as the precise return type for `stream_get_transports`. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `stream_get_transports` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_get_transports(ctx, inst) +} diff --git a/src/builtins/io/stream_get_wrappers.rs b/src/builtins/io/stream_get_wrappers.rs new file mode 100644 index 0000000000..3323a4d132 --- /dev/null +++ b/src/builtins/io/stream_get_wrappers.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `stream_get_wrappers` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Array(Str)`, which is not scalar-expressible, so `returns: Mixed` is +//! used and the hook overrides the return type. The hook takes no arguments. +//! - `lower` is a thin wrapper over `io::lower_stream_get_wrappers` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_get_wrappers", + area: Io, + params: [], + returns: Mixed, + check: check, + lower: lower, + summary: "Retrieves list of registered streams.", + php_manual: "function.stream-get-wrappers", +} + +/// Returns `Array(Str)` as the precise return type for `stream_get_wrappers`. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `stream_get_wrappers` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_get_wrappers(ctx, inst) +} diff --git a/src/builtins/io/stream_is_local.rs b/src/builtins/io/stream_is_local.rs new file mode 100644 index 0000000000..2f93c42512 --- /dev/null +++ b/src/builtins/io/stream_is_local.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `stream_is_local` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers the stream argument and returns `Bool`. +//! - `lower` is a thin wrapper over `io::lower_stream_is_local` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_is_local", + area: Io, + params: [stream: Mixed], + returns: Bool, + lower: lower, + summary: "Checks if a stream is a local stream.", + php_manual: "function.stream-is-local", +} + +/// Lowers a `stream_is_local` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_is_local(ctx, inst) +} diff --git a/src/builtins/io/stream_isatty.rs b/src/builtins/io/stream_isatty.rs new file mode 100644 index 0000000000..786bea9d68 --- /dev/null +++ b/src/builtins/io/stream_isatty.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `stream_isatty` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the stream argument is a stream resource before returning `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_stream_isatty` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_isatty", + area: Io, + params: [stream: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Checks if a stream is a TTY.", + php_manual: "function.stream-isatty", +} + +/// Validates the stream resource argument and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `stream_isatty` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_isatty(ctx, inst) +} diff --git a/src/builtins/io/stream_resolve_include_path.rs b/src/builtins/io/stream_resolve_include_path.rs new file mode 100644 index 0000000000..a7c28e4ace --- /dev/null +++ b/src/builtins/io/stream_resolve_include_path.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `stream_resolve_include_path` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers the filename argument and returns `Mixed`. +//! - `returns: Mixed` reflects the `string|false` PHP return type. +//! - `lower` is a thin wrapper over `io::lower_stream_resolve_include_path` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_resolve_include_path", + area: Io, + params: [filename: Str], + returns: Mixed, + lower: lower, + summary: "Resolves filename against the include path.", + php_manual: "function.stream-resolve-include-path", +} + +/// Lowers a `stream_resolve_include_path` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_resolve_include_path(ctx, inst) +} diff --git a/src/builtins/io/stream_select.rs b/src/builtins/io/stream_select.rs new file mode 100644 index 0000000000..730f4d2abb --- /dev/null +++ b/src/builtins/io/stream_select.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Home of the PHP `stream_select` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers all arguments and returns `Int`. +//! - `read`, `write`, and `except` are by-reference parameters (`ref` marker) for parity +//! with PHP's mutating select semantics and EIR by-ref lowering. +//! - `lower` is a thin wrapper over `io::lower_stream_select` in the EIR backend. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_select", + area: Io, + params: [ + ref read: Mixed, + ref write: Mixed, + ref except: Mixed, + seconds: Int, + microseconds: Int = DefaultSpec::Int(0) + ], + returns: Int, + lower: lower, + summary: "Runs the equivalent of the select() system call on the given arrays of streams.", + php_manual: "function.stream-select", +} + +/// Lowers a `stream_select` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_select(ctx, inst) +} diff --git a/src/builtins/io/stream_set_blocking.rs b/src/builtins/io/stream_set_blocking.rs new file mode 100644 index 0000000000..61d55f0634 --- /dev/null +++ b/src/builtins/io/stream_set_blocking.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `stream_set_blocking` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the first argument is a stream resource before returning `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_stream_set_blocking` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_set_blocking", + area: Io, + params: [stream: Mixed, enable: Bool], + returns: Bool, + check: check, + lower: lower, + summary: "Sets blocking/non-blocking mode on a stream.", + php_manual: "function.stream-set-blocking", +} + +/// Validates the stream resource argument and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `stream_set_blocking` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_set_blocking(ctx, inst) +} diff --git a/src/builtins/io/stream_set_chunk_size.rs b/src/builtins/io/stream_set_chunk_size.rs new file mode 100644 index 0000000000..f6d00d29df --- /dev/null +++ b/src/builtins/io/stream_set_chunk_size.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `stream_set_chunk_size` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers both arguments and returns `Int` +//! (the previous chunk size, or the PHP default of 8192 on failure). +//! - `lower` is a thin wrapper over `io::lower_stream_set_chunk_size` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_set_chunk_size", + area: Io, + params: [stream: Mixed, size: Int], + returns: Int, + lower: lower, + summary: "Sets the read chunk size on a stream.", + php_manual: "function.stream-set-chunk-size", +} + +/// Lowers a `stream_set_chunk_size` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_set_chunk_size(ctx, inst) +} diff --git a/src/builtins/io/stream_set_read_buffer.rs b/src/builtins/io/stream_set_read_buffer.rs new file mode 100644 index 0000000000..86655c5307 --- /dev/null +++ b/src/builtins/io/stream_set_read_buffer.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `stream_set_read_buffer` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers both arguments and returns `Int` +//! (0 on success, matching PHP's successful no-op behaviour). +//! - `lower` dispatches to `io::lower_stream_set_buffer`, shared with `stream_set_write_buffer`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_set_read_buffer", + area: Io, + params: [stream: Mixed, size: Int], + returns: Int, + lower: lower, + summary: "Sets the read file buffering on a stream.", + php_manual: "function.stream-set-read-buffer", +} + +/// Lowers a `stream_set_read_buffer` call by dispatching to the shared io buffer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_set_buffer(ctx, inst) +} diff --git a/src/builtins/io/stream_set_timeout.rs b/src/builtins/io/stream_set_timeout.rs new file mode 100644 index 0000000000..288ebe4f11 --- /dev/null +++ b/src/builtins/io/stream_set_timeout.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `stream_set_timeout` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the first argument is a stream resource before returning `Bool`. +//! - `microseconds` is optional (defaults to 0). Arguments are pre-inferred by the registry. +//! - `lower` is a thin wrapper over `io::lower_stream_set_timeout` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_set_timeout", + area: Io, + params: [stream: Mixed, seconds: Int, microseconds: Int = DefaultSpec::Int(0)], + returns: Bool, + check: check, + lower: lower, + summary: "Sets timeout period on a stream.", + php_manual: "function.stream-set-timeout", +} + +/// Validates the stream resource argument and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `stream_set_timeout` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_set_timeout(ctx, inst) +} diff --git a/src/builtins/io/stream_set_write_buffer.rs b/src/builtins/io/stream_set_write_buffer.rs new file mode 100644 index 0000000000..9faa174dd5 --- /dev/null +++ b/src/builtins/io/stream_set_write_buffer.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `stream_set_write_buffer` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers both arguments and returns `Int` +//! (0 on success, matching PHP's successful no-op behaviour). +//! - `lower` dispatches to `io::lower_stream_set_buffer`, shared with `stream_set_read_buffer`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_set_write_buffer", + area: Io, + params: [stream: Mixed, size: Int], + returns: Int, + lower: lower, + summary: "Sets the write file buffering on a stream.", + php_manual: "function.stream-set-write-buffer", +} + +/// Lowers a `stream_set_write_buffer` call by dispatching to the shared io buffer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_set_buffer(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_accept.rs b/src/builtins/io/stream_socket_accept.rs new file mode 100644 index 0000000000..26c731c275 --- /dev/null +++ b/src/builtins/io/stream_socket_accept.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Home of the PHP `stream_socket_accept` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates arg[0] is a stream resource and that `peer_name` (arg[2]), if provided, +//! is a plain variable (it is written by reference). +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` dispatches to `io::lower_stream_socket_accept` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "stream_socket_accept", + area: Io, + params: [ + socket: Mixed, + timeout: Mixed = DefaultSpec::Null, + ref peer_name: Mixed = DefaultSpec::Null + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Accept a connection on a socket created by stream_socket_server().", + php_manual: "function.stream-socket-accept", +} + +/// Validates arg[0] is a stream resource and that `peer_name` (arg[2]) is a plain variable. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource(cx.checker, cx.name, &cx.args[0], cx.env)?; + if let Some(peer) = cx.args.get(2) { + if !matches!(peer.kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + peer.span, + "stream_socket_accept() parameter $peer_name must be passed a variable", + )); + } + } + Ok(cx.checker.normalize_union_type(vec![PhpType::stream_resource(), PhpType::Bool])) +} + +/// Lowers a `stream_socket_accept` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_accept(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_client.rs b/src/builtins/io/stream_socket_client.rs new file mode 100644 index 0000000000..ddac788880 --- /dev/null +++ b/src/builtins/io/stream_socket_client.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `stream_socket_client` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(stream_resource, Bool)` reflecting PHP's false-on-failure return. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` dispatches to `io::lower_stream_socket_client` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_socket_client", + area: Io, + params: [address: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Open Internet or Unix domain socket connection.", + php_manual: "function.stream-socket-client", +} + +/// Returns `Union(stream_resource, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::stream_resource(), PhpType::Bool])) +} + +/// Lowers a `stream_socket_client` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_client(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_enable_crypto.rs b/src/builtins/io/stream_socket_enable_crypto.rs new file mode 100644 index 0000000000..55dd1a744b --- /dev/null +++ b/src/builtins/io/stream_socket_enable_crypto.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Home of the PHP `stream_socket_enable_crypto` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates arg[0] is a stream resource and requires the `elephc_tls` library. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` dispatches to `io::lower_stream_socket_enable_crypto` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_socket_enable_crypto", + area: Io, + params: [ + stream: Mixed, + enable: Bool, + crypto_method: Mixed = DefaultSpec::Null, + session_stream: Mixed = DefaultSpec::Null + ], + returns: Bool, + check: check, + lower: lower, + summary: "Turns encryption on/off on an already connected socket.", + php_manual: "function.stream-socket-enable-crypto", +} + +/// Validates arg[0] is a stream resource, links the TLS library, and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource(cx.checker, cx.name, &cx.args[0], cx.env)?; + cx.checker.require_builtin_library("elephc_tls"); + Ok(PhpType::Bool) +} + +/// Lowers a `stream_socket_enable_crypto` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_enable_crypto(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_get_name.rs b/src/builtins/io/stream_socket_get_name.rs new file mode 100644 index 0000000000..ed048712c0 --- /dev/null +++ b/src/builtins/io/stream_socket_get_name.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `stream_socket_get_name` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates arg[0] is a stream resource, then returns `Union(Str, Bool)`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` dispatches to `io::lower_stream_socket_get_name` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_socket_get_name", + area: Io, + params: [socket: Mixed, remote: Bool], + returns: Mixed, + check: check, + lower: lower, + summary: "Retrieve the name of the local or remote sockets.", + php_manual: "function.stream-socket-get-name", +} + +/// Validates arg[0] is a stream resource, then returns `Union(Str, Bool)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource(cx.checker, cx.name, &cx.args[0], cx.env)?; + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `stream_socket_get_name` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_get_name(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_pair.rs b/src/builtins/io/stream_socket_pair.rs new file mode 100644 index 0000000000..dd1316e15a --- /dev/null +++ b/src/builtins/io/stream_socket_pair.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `stream_socket_pair` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers the three Int arguments and returns `Mixed`. +//! - PHP returns `array|false`; the builtin emitter widens the success array's slots through +//! `__rt_array_to_mixed` so the value flows through Mixed pipelines without per-call +//! special-casing. `Mixed` for the static type keeps every consumer happy. +//! - `lower` dispatches to `io::lower_stream_socket_pair` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_socket_pair", + area: Io, + params: [domain: Int, type: Int, protocol: Int], + returns: Mixed, + lower: lower, + summary: "Creates a pair of connected, indistinguishable socket streams.", + php_manual: "function.stream-socket-pair", +} + +/// Lowers a `stream_socket_pair` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_pair(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_recvfrom.rs b/src/builtins/io/stream_socket_recvfrom.rs new file mode 100644 index 0000000000..1561abe696 --- /dev/null +++ b/src/builtins/io/stream_socket_recvfrom.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Home of the PHP `stream_socket_recvfrom` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates arg[0] is a stream resource and that `address` (arg[3]), if provided, +//! is a plain string variable (it is written by reference). The double-infer of arg[3] +//! matches the legacy behavior. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` dispatches to `io::lower_stream_socket_recvfrom` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "stream_socket_recvfrom", + area: Io, + params: [ + socket: Mixed, + length: Int, + flags: Int = DefaultSpec::Int(0), + ref address: Str = DefaultSpec::Str("") + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Receives data from a socket, connected or not.", + php_manual: "function.stream-socket-recvfrom", +} + +/// Validates arg[0] is a stream resource and that `address` (arg[3]) is a plain string variable. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource(cx.checker, cx.name, &cx.args[0], cx.env)?; + if cx.args.len() == 4 { + let addr = &cx.args[3]; + if !matches!(addr.kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + addr.span, + "stream_socket_recvfrom() parameter $address must be passed a variable", + )); + } + let ty = cx.checker.infer_type(addr, cx.env)?; + if ty != PhpType::Str { + return Err(CompileError::new( + addr.span, + "stream_socket_recvfrom() parameter $address must be a string", + )); + } + } + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `stream_socket_recvfrom` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_recvfrom(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_sendto.rs b/src/builtins/io/stream_socket_sendto.rs new file mode 100644 index 0000000000..426822db42 --- /dev/null +++ b/src/builtins/io/stream_socket_sendto.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `stream_socket_sendto` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates arg[0] is a stream resource, then returns `Union(Int, Bool)`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` dispatches to `io::lower_stream_socket_sendto` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_socket_sendto", + area: Io, + params: [ + socket: Mixed, + data: Str, + flags: Int = DefaultSpec::Int(0), + address: Str = DefaultSpec::Str("") + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Sends a message to a socket, whether it is connected or not.", + php_manual: "function.stream-socket-sendto", +} + +/// Validates arg[0] is a stream resource, then returns `Union(Int, Bool)`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource(cx.checker, cx.name, &cx.args[0], cx.env)?; + Ok(cx.checker.normalize_union_type(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `stream_socket_sendto` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_sendto(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_server.rs b/src/builtins/io/stream_socket_server.rs new file mode 100644 index 0000000000..1f58a4cf5f --- /dev/null +++ b/src/builtins/io/stream_socket_server.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `stream_socket_server` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(stream_resource, Bool)` reflecting PHP's false-on-failure return. +//! - `returns: Mixed` is used because the union cannot be expressed through the scalar field. +//! - `lower` dispatches to `io::lower_stream_socket_server` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_socket_server", + area: Io, + params: [address: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Create an Internet or Unix domain server socket.", + php_manual: "function.stream-socket-server", +} + +/// Returns `Union(stream_resource, Bool)` reflecting PHP's false-on-failure return. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx.checker.normalize_union_type(vec![PhpType::stream_resource(), PhpType::Bool])) +} + +/// Lowers a `stream_socket_server` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_server(ctx, inst) +} diff --git a/src/builtins/io/stream_socket_shutdown.rs b/src/builtins/io/stream_socket_shutdown.rs new file mode 100644 index 0000000000..90f7840881 --- /dev/null +++ b/src/builtins/io/stream_socket_shutdown.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `stream_socket_shutdown` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates arg[0] is a stream resource, then returns `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` dispatches to `io::lower_stream_socket_shutdown` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_socket_shutdown", + area: Io, + params: [stream: Mixed, mode: Int], + returns: Bool, + check: check, + lower: lower, + summary: "Shutdown a full-duplex connection.", + php_manual: "function.stream-socket-shutdown", +} + +/// Validates arg[0] is a stream resource, then returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource(cx.checker, cx.name, &cx.args[0], cx.env)?; + Ok(PhpType::Bool) +} + +/// Lowers a `stream_socket_shutdown` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_socket_shutdown(ctx, inst) +} diff --git a/src/builtins/io/stream_support.rs b/src/builtins/io/stream_support.rs new file mode 100644 index 0000000000..55831d4ae2 --- /dev/null +++ b/src/builtins/io/stream_support.rs @@ -0,0 +1,114 @@ +//! Purpose: +//! Shared helpers for stream wrapper/filter registration validation and stream builtin +//! int-argument validation in the io builtin homes. +//! Provides class existence checks used by `stream_filter_register` and `stream_wrapper_register`, +//! and `ensure_int`/`ensure_optional_int` used by `stream_get_contents` and `stream_copy_to_stream`. +//! +//! Called from: +//! - `crate::builtins::io::stream_filter_register` (check hook) +//! - `crate::builtins::io::stream_wrapper_register` (check hook) +//! - `crate::builtins::io::stream_get_contents` (check hook) +//! - `crate::builtins::io::stream_copy_to_stream` (check hook) +//! +//! Key details: +//! - `validate_registered_stream_class` checks that a string-literal class argument refers to a +//! declared class; non-literal arguments pass through unchecked (dynamic dispatch at runtime). +//! - `stream_registered_class_exists` uses PHP's case-insensitive class key for lookup. +//! - `ensure_int` and `ensure_optional_int` validate stream builtin length/offset arguments. + +use crate::names::php_symbol_key; +use crate::parser::ast::{Expr, ExprKind}; +use crate::errors::CompileError; +use crate::types::{PhpType, TypeEnv}; +use crate::types::checker::Checker; + +/// Validates a literal stream wrapper/filter class name against declared classes. +/// +/// If the class argument is a string literal and the named class is not declared, +/// returns a compile error at `span`. Non-literal arguments are accepted without +/// checking (the class name is resolved at runtime). +pub(crate) fn validate_registered_stream_class( + checker: &Checker, + builtin: &str, + class_arg: &Expr, + span: crate::span::Span, +) -> Result<(), CompileError> { + let ExprKind::StringLiteral(class_name) = &class_arg.kind else { + return Ok(()); + }; + if stream_registered_class_exists(checker, class_name) { + return Ok(()); + } + Err(CompileError::new( + span, + &format!("{}(): undefined class '{}'", builtin, class_name), + )) +} + +/// Returns true when `class_name` exists under PHP's case-insensitive class lookup. +/// +/// Strips a leading backslash from `class_name` before the key comparison so that +/// both `\Foo` and `Foo` resolve to the same class. +pub(crate) fn stream_registered_class_exists(checker: &Checker, class_name: &str) -> bool { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + checker + .classes + .keys() + .any(|existing| php_symbol_key(existing) == class_key) +} + +/// Ensures a stream builtin argument is an `int`, emitting a parameter-specific +/// compile error otherwise. +pub(crate) fn ensure_int( + checker: &mut Checker, + builtin: &str, + param: &str, + arg: &Expr, + env: &TypeEnv, +) -> Result<(), CompileError> { + let ty = checker.infer_type(arg, env)?; + if accepts_int(&ty) { + return Ok(()); + } + Err(CompileError::new( + arg.span, + &format!("{}() {} must be int", builtin, param), + )) +} + +/// Ensures a stream builtin length argument is `int|null`, matching PHP's +/// nullable `$length` parameter while keeping codegen from seeing strings/floats. +pub(crate) fn ensure_optional_int( + checker: &mut Checker, + builtin: &str, + param: &str, + arg: &Expr, + env: &TypeEnv, +) -> Result<(), CompileError> { + let ty = checker.infer_type(arg, env)?; + if accepts_int_or_null(&ty) { + return Ok(()); + } + Err(CompileError::new( + arg.span, + &format!("{}() {} must be int or null", builtin, param), + )) +} + +/// Returns true when a type is statically compatible with an `int` parameter. +fn accepts_int(ty: &PhpType) -> bool { + match ty { + PhpType::Int => true, + PhpType::Union(members) => members.iter().all(accepts_int), + _ => false, + } +} + +/// Returns true when a type is statically compatible with an `int|null` parameter. +fn accepts_int_or_null(ty: &PhpType) -> bool { + match ty { + PhpType::Int | PhpType::Void => true, + PhpType::Union(members) => members.iter().all(accepts_int_or_null), + _ => false, + } +} diff --git a/src/builtins/io/stream_supports_lock.rs b/src/builtins/io/stream_supports_lock.rs new file mode 100644 index 0000000000..039dd516c7 --- /dev/null +++ b/src/builtins/io/stream_supports_lock.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `stream_supports_lock` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the stream argument is a stream resource before returning `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs. +//! - `lower` is a thin wrapper over `io::lower_stream_supports_lock` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_supports_lock", + area: Io, + params: [stream: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Tells whether the stream supports locking.", + php_manual: "function.stream-supports-lock", +} + +/// Validates the stream resource argument and returns `Bool`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `stream_supports_lock` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_supports_lock(ctx, inst) +} diff --git a/src/builtins/io/stream_wrapper_register.rs b/src/builtins/io/stream_wrapper_register.rs new file mode 100644 index 0000000000..3a209012f3 --- /dev/null +++ b/src/builtins/io/stream_wrapper_register.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `stream_wrapper_register` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the class argument names a declared class and returns `Bool`. +//! - Arguments are pre-inferred by the registry before the hook runs; the hook does NOT +//! re-infer them. +//! - `lower` is a thin wrapper over `io::lower_stream_wrapper_register` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "stream_wrapper_register", + area: Io, + params: [protocol: Str, class: Str, flags: Int = DefaultSpec::Int(0)], + returns: Bool, + check: check, + lower: lower, + summary: "Registers a URL wrapper implemented as a PHP class.", + php_manual: "function.stream-wrapper-register", +} + +/// Validates the class argument names a declared class and returns `Bool`. +/// +/// Arguments are pre-inferred by the registry; this hook validates the class +/// registration using the shared `validate_registered_stream_class` helper. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::builtins::io::stream_support::validate_registered_stream_class( + cx.checker, + cx.name, + &cx.args[1], + cx.span, + )?; + Ok(PhpType::Bool) +} + +/// Lowers a `stream_wrapper_register` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_wrapper_register(ctx, inst) +} diff --git a/src/builtins/io/stream_wrapper_restore.rs b/src/builtins/io/stream_wrapper_restore.rs new file mode 100644 index 0000000000..7b1af84a27 --- /dev/null +++ b/src/builtins/io/stream_wrapper_restore.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `stream_wrapper_restore` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers the protocol argument and returns `Bool`. +//! - `lower` is a thin wrapper over `io::lower_stream_wrapper_restore` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_wrapper_restore", + area: Io, + params: [protocol: Str], + returns: Bool, + lower: lower, + summary: "Restores a previously unregistered built-in wrapper.", + php_manual: "function.stream-wrapper-restore", +} + +/// Lowers a `stream_wrapper_restore` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_wrapper_restore(ctx, inst) +} diff --git a/src/builtins/io/stream_wrapper_unregister.rs b/src/builtins/io/stream_wrapper_unregister.rs new file mode 100644 index 0000000000..26282896e7 --- /dev/null +++ b/src/builtins/io/stream_wrapper_unregister.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `stream_wrapper_unregister` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook: the common registry path infers the protocol argument and returns `Bool`. +//! - `lower` is a thin wrapper over `io::lower_stream_wrapper_unregister` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stream_wrapper_unregister", + area: Io, + params: [protocol: Str], + returns: Bool, + lower: lower, + summary: "Unregisters a previously registered URL wrapper.", + php_manual: "function.stream-wrapper-unregister", +} + +/// Lowers a `stream_wrapper_unregister` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_stream_wrapper_unregister(ctx, inst) +} diff --git a/src/builtins/io/symlink.rs b/src/builtins/io/symlink.rs new file mode 100644 index 0000000000..78682e17dd --- /dev/null +++ b/src/builtins/io/symlink.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `symlink` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `symlink` is a pure-data builtin whose `Bool` return type is +//! fully determined by its declaration. The registry common path infers the +//! arguments and enforces the exactly-2-argument arity before falling back to +//! `returns`. +//! - `lower` is a thin wrapper over `io::lower_symlink` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "symlink", + area: Io, + params: [target: Str, link: Str], + returns: Bool, + lower: lower, + summary: "Creates a symbolic link.", + php_manual: "function.symlink", +} + +/// Lowers a `symlink` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_symlink(ctx, inst) +} diff --git a/src/builtins/io/sys_get_temp_dir.rs b/src/builtins/io/sys_get_temp_dir.rs new file mode 100644 index 0000000000..6fd381a2be --- /dev/null +++ b/src/builtins/io/sys_get_temp_dir.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `sys_get_temp_dir` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `sys_get_temp_dir` is a pure-data builtin whose `Str` return +//! type is fully determined by its declaration. The registry common path enforces +//! its 0-argument arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `io::lower_sys_get_temp_dir` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "sys_get_temp_dir", + area: Io, + params: [], + returns: Str, + lower: lower, + summary: "Returns the directory path used for temporary files.", + php_manual: "function.sys-get-temp-dir", +} + +/// Lowers a `sys_get_temp_dir` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_sys_get_temp_dir(ctx, inst) +} diff --git a/src/builtins/io/tempnam.rs b/src/builtins/io/tempnam.rs new file mode 100644 index 0000000000..cb9b74bbce --- /dev/null +++ b/src/builtins/io/tempnam.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `tempnam` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `tempnam` is a pure-data builtin whose `Str` return type is +//! fully determined by its declaration. The registry common path infers the +//! arguments and enforces the exactly-2-argument arity before falling back to +//! `returns`. +//! - `lower` is a thin wrapper over `io::lower_tempnam` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "tempnam", + area: Io, + params: [directory: Str, prefix: Str], + returns: Str, + lower: lower, + summary: "Creates a file with a unique filename.", + php_manual: "function.tempnam", +} + +/// Lowers a `tempnam` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_tempnam(ctx, inst) +} diff --git a/src/builtins/io/tmpfile.rs b/src/builtins/io/tmpfile.rs new file mode 100644 index 0000000000..6cd0621b20 --- /dev/null +++ b/src/builtins/io/tmpfile.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Home of the PHP `tmpfile` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `tmpfile` takes no PHP-visible arguments but the legacy allows `tmpfile(...[])`, +//! i.e. spreading an empty array literal, as a valid zero-argument call. `max_args: 1` +//! prevents the registry's `check_arity` from rejecting that single-spread form; +//! `arity_error` overrides the error message for 2+-arg calls to match the legacy text. +//! The check hook rejects any non-empty spread or any real argument explicitly. +//! - `max_args` affects only `check_arity`; `function_sig`/`arity_bounds` still derive +//! `(0, Some(0))` from the zero-param list, keeping parity green. +//! - `is_empty_static_array_spread` is relocated here from `streams.rs` (its only caller). +//! - `returns: Mixed` is used because the union involves a resource type that the +//! scalar `returns:` field cannot express. +//! - `lower` is a thin wrapper over `io::lower_tmpfile` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "tmpfile", + area: Io, + params: [], + max_args: 1, + arity_error: "tmpfile() takes no arguments", + returns: Mixed, + check: check, + lower: lower, + summary: "Creates a temporary file.", + php_manual: "function.tmpfile", +} + +/// Accepts `tmpfile()` and `tmpfile(...[])` (empty static-array spread) but rejects +/// any real argument. Returns `Union(stream_resource, Bool)` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if !cx.args.is_empty() && !is_empty_static_array_spread(cx.args) { + return Err(CompileError::new(cx.span, "tmpfile() takes no arguments")); + } + Ok(cx.checker.normalize_union_type(vec![ + PhpType::stream_resource(), + PhpType::Bool, + ])) +} + +/// Returns `true` if `args` contains exactly one element that is a `...[...]` spread +/// of an empty array literal. +/// +/// PHP allows `tmpfile(...[])` as a no-argument call. This helper distinguishes that +/// valid form from a real argument by checking for a single `Spread` node wrapping an +/// `ArrayLiteral([])`. Returns `false` for all other argument shapes. +fn is_empty_static_array_spread(args: &[crate::parser::ast::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()) +} + +/// Lowers a `tmpfile` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_tmpfile(ctx, inst) +} diff --git a/src/builtins/io/touch.rs b/src/builtins/io/touch.rs new file mode 100644 index 0000000000..827ff04dfc --- /dev/null +++ b/src/builtins/io/touch.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Home of the PHP `touch` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` delegates to the relocated `check_touch` helper, which validates that +//! the optional `mtime`/`atime` timestamp arguments are `int` or `null` and that +//! `mtime` is not `null` when `atime` is provided. +//! - `arity_error` is overridden to preserve the legacy message +//! "touch() takes 1, 2, or 3 arguments" (the registry default for a 1-required, +//! 3-max builtin produces "1 to 3 arguments"). +//! - `lower` is a thin wrapper over `io::lower_touch` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::Expr; +use crate::types::checker::Checker; +use crate::types::{PhpType, TypeEnv}; + +builtin! { + name: "touch", + area: Io, + params: [filename: Str, mtime: Int = DefaultSpec::Null, atime: Int = DefaultSpec::Null], + arity_error: "touch() takes 1, 2, or 3 arguments", + returns: Bool, + check: check, + lower: lower, + summary: "Sets access and modification time of a file.", + php_manual: "function.touch", +} + +/// Returns `Bool` after validating `touch()` timestamp arguments via `check_touch`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + check_touch(cx.checker, cx.args, cx.span, cx.env) +} + +/// Validates `touch()` arity (1–3 args) and timestamp argument types. +/// Timestamp args must be `int` (a Unix timestamp) or `null` (omit to use current time). +/// +/// # Errors +/// Returns an error if: +/// - Arity is 0 or greater than 3 +/// - Any timestamp arg is neither `int` nor `null` +/// - `atime` is `null` but `mtime` is non-null (atime implies current time, so mtime cannot be set separately) +/// +/// # Returns +/// `Ok(PhpType::Bool)` on success. +fn check_touch( + checker: &mut Checker, + args: &[Expr], + span: crate::span::Span, + env: &TypeEnv, +) -> Result { + if args.is_empty() || args.len() > 3 { + return Err(CompileError::new(span, "touch() takes 1, 2, or 3 arguments")); + } + checker.infer_type(&args[0], env)?; + let mut timestamp_types = Vec::new(); + for arg in args.iter().skip(1) { + let ty = checker.infer_type(arg, env)?; + if !matches!(ty, PhpType::Int | PhpType::Void) { + return Err(CompileError::new( + arg.span, + "touch() timestamp arguments must be int or null", + )); + } + timestamp_types.push(ty); + } + if matches!(timestamp_types.first(), Some(PhpType::Void)) + && matches!(timestamp_types.get(1), Some(ty) if !matches!(ty, PhpType::Void)) + { + return Err(CompileError::new( + span, + "touch() mtime cannot be null when atime is provided", + )); + } + Ok(PhpType::Bool) +} + +/// Lowers a `touch` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_touch(ctx, inst) +} diff --git a/src/builtins/io/umask.rs b/src/builtins/io/umask.rs new file mode 100644 index 0000000000..2333f5c498 --- /dev/null +++ b/src/builtins/io/umask.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Home of the PHP `umask` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook: `umask` is a pure-data builtin whose `Int` return type is +//! fully determined by its declaration. The registry common path infers the +//! optional argument and enforces arity before falling back to `returns`. +//! - `arity_error` is overridden to preserve the legacy message +//! "umask() takes 0 or 1 arguments" (the registry default for a 0-required, +//! 1-optional builtin produces "takes at most 1 argument"). +//! - `lower` is a thin wrapper over `io::lower_umask` in the EIR backend. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "umask", + area: Io, + params: [mask: Int = DefaultSpec::Null], + arity_error: "umask() takes 0 or 1 arguments", + returns: Int, + lower: lower, + summary: "Changes the current umask.", + php_manual: "function.umask", +} + +/// Lowers a `umask` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_umask(ctx, inst) +} diff --git a/src/builtins/io/unlink.rs b/src/builtins/io/unlink.rs new file mode 100644 index 0000000000..90e50c5407 --- /dev/null +++ b/src/builtins/io/unlink.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Home of the PHP `unlink` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Bool`. Unlike `mkdir`/`rmdir`/`chdir`, `unlink` carries a PHAR +//! side effect: a literal `phar://` URL or any non-literal path links `elephc_phar` +//! because deletion may target an entry inside a PHAR archive. +//! - `lower` is a thin wrapper over `io::lower_unlink` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "unlink", + area: Io, + params: [filename: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Deletes a file.", + php_manual: "function.unlink", +} + +/// Returns `Bool` and links `elephc_phar` when the target may live in a PHAR archive. +/// +/// A literal `phar://` URL links `elephc_phar`; a non-literal path also links it +/// because the scheme is unknown at compile time. A literal non-`phar://` path +/// needs no PHAR bridge. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(ExprKind::StringLiteral(url)) = cx.args.first().map(|a| &a.kind) { + if url.starts_with("phar://") { + cx.checker.require_builtin_library("elephc_phar"); + } + } else { + cx.checker.require_builtin_library("elephc_phar"); + } + cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(PhpType::Bool) +} + +/// Lowers an `unlink` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_unlink(ctx, inst) +} diff --git a/src/builtins/io/var_dump.rs b/src/builtins/io/var_dump.rs new file mode 100644 index 0000000000..5bb33265f9 --- /dev/null +++ b/src/builtins/io/var_dump.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `var_dump` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `var_dump` is a pure-data builtin whose return type +//! (`Void`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. +//! - `lower` is a thin wrapper over `debug::lower_var_dump` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "var_dump", + area: Io, + params: [value: Mixed], + returns: Void, + lower: lower, + summary: "Dumps information about a variable.", + php_manual: "function.var-dump", +} + +/// Lowers a `var_dump` call by dispatching to the shared debug emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::debug::lower_var_dump(ctx, inst) +} diff --git a/src/builtins/io/vfprintf.rs b/src/builtins/io/vfprintf.rs new file mode 100644 index 0000000000..99b5b17950 --- /dev/null +++ b/src/builtins/io/vfprintf.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `vfprintf` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` calls `ensure_stream_resource` on the stream argument for validation and +//! returns `Int`. Arguments are pre-inferred by the registry before the hook runs. +//! - `arity_error` is overridden to preserve the legacy message suffix +//! "(stream, format, values)" that the standard derived message omits. +//! - `lower` is a thin wrapper over `io::lower_vfprintf` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "vfprintf", + area: Io, + params: [stream: Mixed, format: Str, values: Mixed], + arity_error: "vfprintf() takes exactly 3 arguments (stream, format, values)", + returns: Int, + check: check, + lower: lower, + summary: "Write a formatted string to a stream.", + php_manual: "function.vfprintf", +} + +/// Validates the stream argument is a stream resource and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + crate::types::checker::builtins::io::common::ensure_stream_resource( + cx.checker, + cx.name, + &cx.args[0], + cx.env, + )?; + Ok(PhpType::Int) +} + +/// Lowers a `vfprintf` call by dispatching to the shared io emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::io::lower_vfprintf(ctx, inst) +} diff --git a/src/builtins/macros.rs b/src/builtins/macros.rs new file mode 100644 index 0000000000..18133e1ae4 --- /dev/null +++ b/src/builtins/macros.rs @@ -0,0 +1,220 @@ +//! Purpose: +//! Provides the `builtin!` declarative macro used to register PHP builtin function +//! descriptors into the inventory-based registry at link time. +//! +//! Called from: +//! - Each `crate::builtins::::` leaf file via `#[macro_use]` on this module. +//! +//! Key details: +//! - This module is included with `#[macro_use]` so the macro is available crate-wide +//! without explicit import at every call site. +//! - Fields must appear in the CANONICAL ORDER listed below; optional fields may be omitted. +//! - The `params` list syntax is `[name: TypeSpec, name: TypeSpec = DefaultSpec::Variant, ...]`. +//! Defaults are written as full `DefaultSpec::` paths (unit or data-carrying), e.g. +//! `= DefaultSpec::Null`, `= DefaultSpec::Int(5)`, `= DefaultSpec::Bool(false)`. +//! This avoids `macro_rules!`' limitation that `expr` fragments cannot be spliced after `::`. +//! - An optional leading `ref` per parameter marks it as by-reference (`by_ref: true`). +//! Syntax: `params: [ref array: Mixed, offset: Int]`. Parameters without `ref` are by-value. +//! - A trailing comma after the last field is optional. +//! +//! Canonical field order: +//! name, area, params, variadic?, min_args?, max_args?, arity_error?, returns, by_ref_return?, +//! check?, lower, summary, examples?, php_manual?, deprecation?, internal? +//! +//! Example: +//! ```ignore +//! builtin! { +//! name: "strlen", +//! area: String, +//! params: [string: Str], +//! returns: Int, +//! lower: strlen_lower, +//! summary: "Returns the length of a string.", +//! php_manual: "function.strlen", +//! } +//! ``` + +/// Registers a PHP builtin descriptor into the `inventory`-based registry. +/// +/// Fields must appear in canonical order (optional fields may be omitted): +/// `name`, `area`, `params`, `variadic`?, `min_args`?, `max_args`?, `arity_error`?, +/// `returns`, `by_ref_return`?, `check`?, `lower`, `summary`, `examples`?, `php_manual`?, +/// `deprecation`?, `internal`? +/// +/// `max_args` (optional `usize`) caps the maximum argument count enforced by the +/// registry's `check_arity` only; it does not affect `function_sig` or the parity gate. +/// `min_args` (optional `usize`) raises the enforced minimum in `check_arity` only. +/// `arity_error` (optional `&'static str`) overrides the standard arity error message. +/// `lazy_check` (optional `bool`, default `false`) skips the registry's standard pre-inference +/// loop before calling the `check` hook. Use when the check hook must control argument +/// inference order (e.g., to pass object-element type hints to an unannotated closure before +/// `infer_type` is called on it). When `true`, the check hook is responsible for calling +/// `infer_type` on each argument as needed. +/// +/// A trailing comma after the last field is optional. +/// +/// The `params` list uses `[name: TypeSpec]` or `[name: TypeSpec = DefaultSpec::Variant]` +/// syntax. Defaults are full `DefaultSpec` paths: `DefaultSpec::Null`, `DefaultSpec::Int(5)`, +/// `DefaultSpec::Bool(false)`, etc. Unit and data-carrying variants are both supported. +/// An optional leading `ref` per parameter marks it as by-reference: `params: [ref array: Mixed, ...]` +/// emits `by_ref: true` for that parameter. Parameters without `ref` are by-value (`by_ref: false`). +#[macro_export] +macro_rules! builtin { + // Entry rule: all fields in canonical order; optional fields handled via helper rules. + // The last optional field `internal` has no required trailing comma so that + // `internal: true }` works without a final comma in the invocation. + ( + name: $name:expr, + area: $area:ident, + params: [ $($params:tt)* ], + $(variadic: $variadic:expr,)? + $(min_args: $min_args:expr,)? + $(max_args: $max_args:expr,)? + $(arity_error: $arity_error:expr,)? + returns: $returns:ident, + $(by_ref_return: $by_ref_return:expr,)? + $(check: $check:expr,)? + $(lazy_check: $lazy_check:expr,)? + lower: $lower:expr, + summary: $summary:expr, + $(examples: $examples:expr,)? + $(php_manual: $php_manual:expr,)? + $(deprecation: $deprecation:expr,)? + $(internal: $internal:expr)? + $(,)? + ) => { + inventory::submit! { + $crate::builtins::spec::BuiltinSpec { + name: $name, + area: $crate::builtins::spec::Area::$area, + params: { + const PARAMS: &[$crate::builtins::spec::ParamSpec] = + builtin!(@params [ $($params)* ] -> []); + PARAMS + }, + variadic: builtin!(@opt_str $($variadic)?), + max_args: builtin!(@opt_usize $($max_args)?), + min_args: builtin!(@opt_usize $($min_args)?), + arity_error: builtin!(@opt_str $($arity_error)?), + returns: $crate::builtins::spec::TypeSpec::$returns, + by_ref_return: builtin!(@opt_bool $($by_ref_return)?), + check: builtin!(@opt_fn $($check)?), + lazy_check: builtin!(@opt_bool $($lazy_check)?), + lower: $lower, + summary: $summary, + examples: builtin!(@opt_examples $($examples)?), + php_manual: builtin!(@opt_str $($php_manual)?), + deprecation: builtin!(@opt_str $($deprecation)?), + internal: builtin!(@opt_bool $($internal)?), + } + } + }; + + // @params muncher: accumulator-style recursive parser for the params list. + // Arms with a leading `ref` keyword must appear BEFORE the normal arms so the + // keyword is consumed as the by-reference marker before the normal name-match fires. + + // Done: emit the accumulated ParamSpec list as a const-promotable slice. + (@params [] -> [$($acc:tt)*]) => { &[ $($acc)* ] }; + + // by-ref param WITH default. + (@params [ ref $pname:tt : $pty:ident = $pdefault:expr $(, $($rest:tt)*)? ] -> [$($acc:tt)*]) => { + builtin!(@params [ $($($rest)*)? ] -> [ $($acc)* + $crate::builtins::spec::ParamSpec { + name: builtin!(@name_str $pname), + ty: $crate::builtins::spec::TypeSpec::$pty, + default: Some($pdefault), + by_ref: true, + }, + ]) + }; + + // by-ref param WITHOUT default. + (@params [ ref $pname:tt : $pty:ident $(, $($rest:tt)*)? ] -> [$($acc:tt)*]) => { + builtin!(@params [ $($($rest)*)? ] -> [ $($acc)* + $crate::builtins::spec::ParamSpec { + name: builtin!(@name_str $pname), + ty: $crate::builtins::spec::TypeSpec::$pty, + default: None, + by_ref: true, + }, + ]) + }; + + // normal param WITH default. + (@params [ $pname:tt : $pty:ident = $pdefault:expr $(, $($rest:tt)*)? ] -> [$($acc:tt)*]) => { + builtin!(@params [ $($($rest)*)? ] -> [ $($acc)* + $crate::builtins::spec::ParamSpec { + name: builtin!(@name_str $pname), + ty: $crate::builtins::spec::TypeSpec::$pty, + default: Some($pdefault), + by_ref: false, + }, + ]) + }; + + // normal param WITHOUT default. + (@params [ $pname:tt : $pty:ident $(, $($rest:tt)*)? ] -> [$($acc:tt)*]) => { + builtin!(@params [ $($($rest)*)? ] -> [ $($acc)* + $crate::builtins::spec::ParamSpec { + name: builtin!(@name_str $pname), + ty: $crate::builtins::spec::TypeSpec::$pty, + default: None, + by_ref: false, + }, + ]) + }; + + // Helper: convert a param-name token to a &'static str. + // Handles raw identifiers (e.g. r#break → "break") and explicit string literals. + // Raw identifiers that are Rust keywords must be listed explicitly because + // `stringify!(r#keyword)` preserves the `r#` prefix on some Rust toolchains. + (@name_str r#break) => { "break" }; + (@name_str r#continue) => { "continue" }; + (@name_str r#type) => { "type" }; + (@name_str r#match) => { "match" }; + (@name_str r#return) => { "return" }; + (@name_str r#use) => { "use" }; + (@name_str r#mod) => { "mod" }; + (@name_str r#fn) => { "fn" }; + (@name_str r#let) => { "let" }; + (@name_str r#move) => { "move" }; + (@name_str r#ref) => { "ref" }; + (@name_str r#static) => { "static" }; + (@name_str r#const) => { "const" }; + (@name_str r#trait) => { "trait" }; + (@name_str r#impl) => { "impl" }; + (@name_str r#where) => { "where" }; + (@name_str r#as) => { "as" }; + (@name_str r#in) => { "in" }; + (@name_str r#loop) => { "loop" }; + // Fallback: a normal identifier, stringified as-is. + (@name_str $name:ident) => { stringify!($name) }; + // Explicit string literal (for future use if needed). + (@name_str $name:literal) => { $name }; + + // Helper: optional DefaultSpec — present wraps in Some, absent yields None. + // Users write the full DefaultSpec path: `DefaultSpec::Null`, `DefaultSpec::Int(5)`, etc. + (@default $val:expr) => { Some($val) }; + (@default) => { None }; + + // Helper: optional &'static str — present yields Some, absent yields None. + (@opt_str $val:expr) => { Some($val) }; + (@opt_str) => { None }; + + // Helper: optional usize (max_args override) — present yields Some, absent yields None. + (@opt_usize $val:expr) => { Some($val) }; + (@opt_usize) => { None }; + + // Helper: optional bool — present yields the value, absent yields false. + (@opt_bool $val:expr) => { $val }; + (@opt_bool) => { false }; + + // Helper: optional CheckFn — present yields Some, absent yields None. + (@opt_fn $val:expr) => { Some($val) }; + (@opt_fn) => { None }; + + // Helper: optional examples slice — present yields the value, absent yields empty slice. + (@opt_examples $val:expr) => { $val }; + (@opt_examples) => { &[] }; +} diff --git a/src/builtins/math/abs.rs b/src/builtins/math/abs.rs new file mode 100644 index 0000000000..03ad887245 --- /dev/null +++ b/src/builtins/math/abs.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Home of the PHP `abs` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required because the return type depends on the argument type: +//! `Float` input returns `Float`, `Mixed`/Union-containing-Float returns `Mixed`, +//! and all other inputs return `Int`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "abs", + area: Math, + params: [num: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Absolute value.", + php_manual: "https://www.php.net/manual/en/function.abs.php", +} + +/// Returns the most precise result type for `abs($num)` based on the argument type. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(match ty { + PhpType::Float => PhpType::Float, + PhpType::Mixed => PhpType::Mixed, + PhpType::Union(ref members) if members.iter().any(|m| *m == PhpType::Float) => { + PhpType::Mixed + } + PhpType::Union(ref members) if members.iter().any(|m| *m == PhpType::Mixed) => { + PhpType::Mixed + } + _ => PhpType::Int, + }) +} + +/// Lowers an `abs` call by dispatching to the shared absolute-value emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_abs(ctx, inst) +} diff --git a/src/builtins/math/acos.rs b/src/builtins/math/acos.rs new file mode 100644 index 0000000000..d3eaf1532d --- /dev/null +++ b/src/builtins/math/acos.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `acos` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `acos` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "acos", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the arccosine of a number in radians.", + php_manual: "https://www.php.net/manual/en/function.acos.php", +} + +/// Lowers a `acos` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "acos") +} diff --git a/src/builtins/math/asin.rs b/src/builtins/math/asin.rs new file mode 100644 index 0000000000..da4b30efe7 --- /dev/null +++ b/src/builtins/math/asin.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `asin` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `asin` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "asin", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the arcsine of a number in radians.", + php_manual: "https://www.php.net/manual/en/function.asin.php", +} + +/// Lowers a `asin` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "asin") +} diff --git a/src/builtins/math/atan.rs b/src/builtins/math/atan.rs new file mode 100644 index 0000000000..34d05a72b8 --- /dev/null +++ b/src/builtins/math/atan.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `atan` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `atan` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "atan", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the arctangent of a number in radians.", + php_manual: "https://www.php.net/manual/en/function.atan.php", +} + +/// Lowers a `atan` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "atan") +} diff --git a/src/builtins/math/atan2.rs b/src/builtins/math/atan2.rs new file mode 100644 index 0000000000..5c683a884b --- /dev/null +++ b/src/builtins/math/atan2.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `atan2` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `atan2` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "atan2", + area: Math, + params: [y: Float, x: Float], + returns: Float, + lower: lower, + summary: "Returns the arc tangent of two variables.", + php_manual: "https://www.php.net/manual/en/function.atan2.php", +} + +/// Lowers an `atan2` call by dispatching to the shared libm two-argument emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_atan2(ctx, inst) +} diff --git a/src/builtins/math/ceil.rs b/src/builtins/math/ceil.rs new file mode 100644 index 0000000000..7803b0d6d6 --- /dev/null +++ b/src/builtins/math/ceil.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `ceil` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `ceil` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ceil", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Rounds a number up to the nearest integer.", + php_manual: "https://www.php.net/manual/en/function.ceil.php", +} + +/// Lowers a `ceil` call by dispatching to the shared float-rounding emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_ceil(ctx, inst) +} diff --git a/src/builtins/math/clamp.rs b/src/builtins/math/clamp.rs new file mode 100644 index 0000000000..ff66bb30af --- /dev/null +++ b/src/builtins/math/clamp.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Home of the PHP `clamp` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required because the return type depends on all three argument +//! types: all-Str returns Str, all-Int returns Int, Int/Float mix returns Float, +//! anything else returns Mixed. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "clamp", + area: Math, + params: [value: Mixed, min: Mixed, max: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Clamps a value to be within a specified range.", + php_manual: "https://www.php.net/manual/en/function.clamp.php", +} + +/// Returns the most precise result type for `clamp($value, $min, $max)`. +/// +/// All-string operands return `Str`; all-int return `Int`; int/float mix returns +/// `Float`; any other combination returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let mut arg_types = Vec::with_capacity(cx.args.len()); + for arg in cx.args { + arg_types.push(cx.checker.infer_type(arg, cx.env)?); + } + if arg_types.iter().all(|ty| *ty == PhpType::Str) { + Ok(PhpType::Str) + } else if arg_types.iter().all(|ty| *ty == PhpType::Int) { + Ok(PhpType::Int) + } else if arg_types + .iter() + .all(|ty| matches!(ty, PhpType::Int | PhpType::Float)) + { + Ok(PhpType::Float) + } else { + Ok(PhpType::Mixed) + } +} + +/// Lowers a `clamp` call by dispatching to the shared numeric-clamp emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_clamp(ctx, inst) +} diff --git a/src/builtins/math/cos.rs b/src/builtins/math/cos.rs new file mode 100644 index 0000000000..8e3066ddb8 --- /dev/null +++ b/src/builtins/math/cos.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `cos` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `cos` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "cos", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the cosine of a number (radians).", + php_manual: "https://www.php.net/manual/en/function.cos.php", +} + +/// Lowers a `cos` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "cos") +} diff --git a/src/builtins/math/cosh.rs b/src/builtins/math/cosh.rs new file mode 100644 index 0000000000..e9125d5291 --- /dev/null +++ b/src/builtins/math/cosh.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `cosh` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `cosh` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "cosh", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the hyperbolic cosine of a number.", + php_manual: "https://www.php.net/manual/en/function.cosh.php", +} + +/// Lowers a `cosh` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "cosh") +} diff --git a/src/builtins/math/deg2rad.rs b/src/builtins/math/deg2rad.rs new file mode 100644 index 0000000000..076c8b044e --- /dev/null +++ b/src/builtins/math/deg2rad.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `deg2rad` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `deg2rad` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "deg2rad", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Converts a degree value to radians.", + php_manual: "https://www.php.net/manual/en/function.deg2rad.php", +} + +/// Lowers a `deg2rad` call by multiplying with the PI/180 conversion factor. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_deg2rad(ctx, inst) +} diff --git a/src/builtins/math/exp.rs b/src/builtins/math/exp.rs new file mode 100644 index 0000000000..c4e26d3c1e --- /dev/null +++ b/src/builtins/math/exp.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `exp` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `exp` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "exp", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns e raised to the power of a number.", + php_manual: "https://www.php.net/manual/en/function.exp.php", +} + +/// Lowers a `exp` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "exp") +} diff --git a/src/builtins/math/fdiv.rs b/src/builtins/math/fdiv.rs new file mode 100644 index 0000000000..b0095b7006 --- /dev/null +++ b/src/builtins/math/fdiv.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `fdiv` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `fdiv` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "fdiv", + area: Math, + params: [num1: Float, num2: Float], + returns: Float, + lower: lower, + summary: "Divides two numbers, according to IEEE 754.", + php_manual: "https://www.php.net/manual/en/function.fdiv.php", +} + +/// Lowers a `fdiv` call by dispatching to the shared IEEE-754 division emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_fdiv(ctx, inst) +} diff --git a/src/builtins/math/floor.rs b/src/builtins/math/floor.rs new file mode 100644 index 0000000000..1dafb6490a --- /dev/null +++ b/src/builtins/math/floor.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `floor` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `floor` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "floor", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Rounds a number down to the nearest integer.", + php_manual: "https://www.php.net/manual/en/function.floor.php", +} + +/// Lowers a `floor` call by dispatching to the shared float-rounding emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_floor(ctx, inst) +} diff --git a/src/builtins/math/fmod.rs b/src/builtins/math/fmod.rs new file mode 100644 index 0000000000..d11efe6f05 --- /dev/null +++ b/src/builtins/math/fmod.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `fmod` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `fmod` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "fmod", + area: Math, + params: [num1: Float, num2: Float], + returns: Float, + lower: lower, + summary: "Returns the floating point remainder of the division of the arguments.", + php_manual: "https://www.php.net/manual/en/function.fmod.php", +} + +/// Lowers an `fmod` call by dispatching to the shared floating-remainder emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_fmod(ctx, inst) +} diff --git a/src/builtins/math/hypot.rs b/src/builtins/math/hypot.rs new file mode 100644 index 0000000000..ef47b23a7c --- /dev/null +++ b/src/builtins/math/hypot.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `hypot` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `hypot` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "hypot", + area: Math, + params: [x: Float, y: Float], + returns: Float, + lower: lower, + summary: "Calculates the length of the hypotenuse of a right-angle triangle.", + php_manual: "https://www.php.net/manual/en/function.hypot.php", +} + +/// Lowers a `hypot` call by dispatching to the shared libm two-argument emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_hypot(ctx, inst) +} diff --git a/src/builtins/math/intdiv.rs b/src/builtins/math/intdiv.rs new file mode 100644 index 0000000000..39114f56cd --- /dev/null +++ b/src/builtins/math/intdiv.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `intdiv` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `intdiv` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "intdiv", + area: Math, + params: [num1: Int, num2: Int], + returns: Int, + lower: lower, + summary: "Integer division.", + php_manual: "https://www.php.net/manual/en/function.intdiv.php", +} + +/// Lowers an `intdiv` call by dispatching to the shared integer-division emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_intdiv(ctx, inst) +} diff --git a/src/builtins/math/log.rs b/src/builtins/math/log.rs new file mode 100644 index 0000000000..8f6f1bc60f --- /dev/null +++ b/src/builtins/math/log.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `log` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `log` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. +//! - The second parameter `base` is optional with a default of `M_E`, matching +//! PHP's `log(num, base = M_E)` signature. The registry enforces 1-2 args. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "log", + area: Math, + params: [num: Float, base: Float = DefaultSpec::Float(std::f64::consts::E)], + returns: Float, + lower: lower, + summary: "Natural logarithm.", + php_manual: "https://www.php.net/manual/en/function.log.php", +} + +/// Lowers a `log` call by dispatching to the shared logarithm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_log(ctx, inst) +} diff --git a/src/builtins/math/log10.rs b/src/builtins/math/log10.rs new file mode 100644 index 0000000000..64c2d123dd --- /dev/null +++ b/src/builtins/math/log10.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `log10` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `log10` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "log10", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the base-10 logarithm of a number.", + php_manual: "https://www.php.net/manual/en/function.log10.php", +} + +/// Lowers a `log10` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "log10") +} diff --git a/src/builtins/math/log2.rs b/src/builtins/math/log2.rs new file mode 100644 index 0000000000..693acbd8f9 --- /dev/null +++ b/src/builtins/math/log2.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `log2` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `log2` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "log2", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the base-2 logarithm of a number.", + php_manual: "https://www.php.net/manual/en/function.log2.php", +} + +/// Lowers a `log2` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "log2") +} diff --git a/src/builtins/math/max.rs b/src/builtins/math/max.rs new file mode 100644 index 0000000000..f29aa8073a --- /dev/null +++ b/src/builtins/math/max.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Home of the PHP `max` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required because the return type depends on argument types: +//! any Float argument widens the result to Float; otherwise the result is Int. +//! - `min_args: 2` enforces the legacy requirement that at least two values be provided. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "max", + area: Math, + params: [value: Mixed], + variadic: "values", + min_args: 2, + arity_error: "max() requires at least 2 arguments", + returns: Mixed, + check: check, + lower: lower, + summary: "Find highest value.", + php_manual: "https://www.php.net/manual/en/function.max.php", +} + +/// Returns Float when any argument is Float, otherwise returns Int. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let mut has_float = false; + for arg in cx.args { + let t = cx.checker.infer_type(arg, cx.env)?; + if t == PhpType::Float { + has_float = true; + } + } + if has_float { + Ok(PhpType::Float) + } else { + Ok(PhpType::Int) + } +} + +/// Lowers a `max` call by dispatching to the shared min/max emitter with `want_max = true`. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_min_max(ctx, inst, true) +} diff --git a/src/builtins/math/min.rs b/src/builtins/math/min.rs new file mode 100644 index 0000000000..7ba5c84b58 --- /dev/null +++ b/src/builtins/math/min.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Home of the PHP `min` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required because the return type depends on argument types: +//! any Float argument widens the result to Float; otherwise the result is Int. +//! - `min_args: 2` enforces the legacy requirement that at least two values be provided. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "min", + area: Math, + params: [value: Mixed], + variadic: "values", + min_args: 2, + arity_error: "min() requires at least 2 arguments", + returns: Mixed, + check: check, + lower: lower, + summary: "Find lowest value.", + php_manual: "https://www.php.net/manual/en/function.min.php", +} + +/// Returns Float when any argument is Float, otherwise returns Int. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let mut has_float = false; + for arg in cx.args { + let t = cx.checker.infer_type(arg, cx.env)?; + if t == PhpType::Float { + has_float = true; + } + } + if has_float { + Ok(PhpType::Float) + } else { + Ok(PhpType::Int) + } +} + +/// Lowers a `min` call by dispatching to the shared min/max emitter with `want_max = false`. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_min_max(ctx, inst, false) +} diff --git a/src/builtins/math/mod.rs b/src/builtins/math/mod.rs new file mode 100644 index 0000000000..c5c8d3cf88 --- /dev/null +++ b/src/builtins/math/mod.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Groups all `math`-area builtin homes into this module so the registry can +//! collect them in one place. Each submodule declares exactly one builtin via +//! `builtin!` and provides its lowering hook. +//! +//! Called from: +//! - `crate::builtins` (`mod math;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - Add `pub mod ;` here for every new math builtin home. +//! - Pure-data builtins (no `check` hook) rely on the registry common path to +//! infer each argument and enforce arity before falling back to the declared +//! `returns` type. +//! - Builtins with argument-type-dependent returns (`abs`, `clamp`, `min`, `max`) +//! supply a `check` hook that computes the precise return type. + +pub mod abs; +pub mod acos; +pub mod asin; +pub mod atan; +pub mod atan2; +pub mod ceil; +pub mod clamp; +pub mod cos; +pub mod cosh; +pub mod deg2rad; +pub mod exp; +pub mod fdiv; +pub mod floor; +pub mod fmod; +pub mod hypot; +pub mod intdiv; +pub mod log; +pub mod log10; +pub mod log2; +pub mod max; +pub mod min; +pub mod mt_rand; +pub mod pi; +pub mod pow; +pub mod rad2deg; +pub mod rand; +pub mod random_bytes; +pub mod random_int; +pub mod round; +pub mod sin; +pub mod sinh; +pub mod sqrt; +pub mod tan; +pub mod tanh; diff --git a/src/builtins/math/mt_rand.rs b/src/builtins/math/mt_rand.rs new file mode 100644 index 0000000000..2badbde296 --- /dev/null +++ b/src/builtins/math/mt_rand.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Home of the PHP `mt_rand` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `min_args: 0` allows 0-arg calls (returns a raw random u32) in addition to +//! the 2-arg range form. +//! - A `check` hook rejects exactly 1 argument, matching PHP's "0 or 2 arguments" rule. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "mt_rand", + area: Math, + params: [min: Int, max: Int], + min_args: 0, + returns: Int, + check: check, + lower: lower, + summary: "Generate a random value via the Mersenne Twister Random Number Generator.", + php_manual: "https://www.php.net/manual/en/function.mt-rand.php", +} + +/// Rejects exactly 1 argument, matching PHP's "0 or 2 arguments" arity rule. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if cx.args.len() == 1 { + return Err(CompileError::new(cx.span, "mt_rand() takes 0 or 2 arguments")); + } + Ok(PhpType::Int) +} + +/// Lowers an `mt_rand` call by dispatching to the shared random-integer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_rand(ctx, inst, "mt_rand") +} diff --git a/src/builtins/math/pi.rs b/src/builtins/math/pi.rs new file mode 100644 index 0000000000..5f2c5afb7f --- /dev/null +++ b/src/builtins/math/pi.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `pi` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `pi` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. It takes no arguments. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "pi", + area: Math, + params: [], + returns: Float, + lower: lower, + summary: "Gets value of pi.", + php_manual: "https://www.php.net/manual/en/function.pi.php", +} + +/// Lowers a `pi` call by dispatching to the shared pi-constant emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_pi(ctx, inst) +} diff --git a/src/builtins/math/pow.rs b/src/builtins/math/pow.rs new file mode 100644 index 0000000000..73c87c863b --- /dev/null +++ b/src/builtins/math/pow.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `pow` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `pow` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "pow", + area: Math, + params: [num: Mixed, exponent: Mixed], + returns: Float, + lower: lower, + summary: "Exponential expression.", + php_manual: "https://www.php.net/manual/en/function.pow.php", +} + +/// Lowers a `pow` call by dispatching to the shared exponentiation emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_pow(ctx, inst) +} diff --git a/src/builtins/math/rad2deg.rs b/src/builtins/math/rad2deg.rs new file mode 100644 index 0000000000..0c4a2534b4 --- /dev/null +++ b/src/builtins/math/rad2deg.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `rad2deg` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `rad2deg` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "rad2deg", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Converts a radian value to degrees.", + php_manual: "https://www.php.net/manual/en/function.rad2deg.php", +} + +/// Lowers a `rad2deg` call by multiplying with the 180/PI conversion factor. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_rad2deg(ctx, inst) +} diff --git a/src/builtins/math/rand.rs b/src/builtins/math/rand.rs new file mode 100644 index 0000000000..e42cce1a5c --- /dev/null +++ b/src/builtins/math/rand.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Home of the PHP `rand` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `min_args: 0` allows 0-arg calls (returns a raw random u32) in addition to +//! the 2-arg range form. +//! - A `check` hook rejects exactly 1 argument, matching PHP's "0 or 2 arguments" rule. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "rand", + area: Math, + params: [min: Int, max: Int], + min_args: 0, + returns: Int, + check: check, + lower: lower, + summary: "Generate a random integer.", + php_manual: "https://www.php.net/manual/en/function.rand.php", +} + +/// Rejects exactly 1 argument, matching PHP's "0 or 2 arguments" arity rule. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if cx.args.len() == 1 { + return Err(CompileError::new(cx.span, "rand() takes 0 or 2 arguments")); + } + Ok(PhpType::Int) +} + +/// Lowers a `rand` call by dispatching to the shared random-integer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_rand(ctx, inst, "rand") +} diff --git a/src/builtins/math/random_bytes.rs b/src/builtins/math/random_bytes.rs new file mode 100644 index 0000000000..d14286b97d --- /dev/null +++ b/src/builtins/math/random_bytes.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Home of the PHP `random_bytes` builtin: its declaration, compile-time +//! length guard, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the +//! EIR backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required only to reject a statically-known length below 1 +//! at compile time. PHP throws a `ValueError` for such a length; elephc has no +//! catchable path out of the runtime helper, so a constant literal below 1 +//! (folded `0` or a negative) is rejected here. Runtime-unknown lengths are +//! guarded in the `__rt_random_bytes` runtime helper instead. +//! - Arity (exactly 1 argument) is enforced by the registry from `params`, so the +//! check hook does not re-check it; the return type is always `Str`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen_ir::context::FunctionContext; +use crate::codegen_ir::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "random_bytes", + area: Math, + params: [length: Int], + returns: Str, + check: check, + lower: lower, + summary: "Get a cryptographically secure random string of the given length.", + php_manual: "https://www.php.net/manual/en/function.random-bytes.php", +} + +/// Rejects a statically-known `length` below 1 at compile time and returns `Str`. +/// +/// A constant integer literal argument that folds to `0` or a negative value is a +/// guaranteed PHP `ValueError`; since the runtime helper cannot surface a catchable +/// exception, that case is rejected here. Runtime-unknown lengths pass through and +/// are guarded by the `__rt_random_bytes` runtime helper. Arity and per-argument +/// inference are handled by the registry common path before this hook runs. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let ExprKind::IntLiteral(length) = cx.args[0].kind { + if length < 1 { + return Err(CompileError::new( + cx.span, + "random_bytes(): Argument #1 ($length) must be greater than 0", + )); + } + } + Ok(PhpType::Str) +} + +/// Lowers a `random_bytes` call by dispatching to the shared CSPRNG byte-string emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen_ir::lower_inst::builtins::math::lower_random_bytes(ctx, inst) +} diff --git a/src/builtins/math/random_int.rs b/src/builtins/math/random_int.rs new file mode 100644 index 0000000000..208fb2d8ae --- /dev/null +++ b/src/builtins/math/random_int.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `random_int` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `random_int` is a pure-data builtin returning `Int`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "random_int", + area: Math, + params: [min: Int, max: Int], + returns: Int, + lower: lower, + summary: "Get a cryptographically secure, uniformly selected integer.", + php_manual: "https://www.php.net/manual/en/function.random-int.php", +} + +/// Lowers a `random_int` call by dispatching to the shared cryptographic-random emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_random_int(ctx, inst) +} diff --git a/src/builtins/math/round.rs b/src/builtins/math/round.rs new file mode 100644 index 0000000000..69a8c4e216 --- /dev/null +++ b/src/builtins/math/round.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `round` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `round` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. +//! - The second parameter `precision` is optional with a default of `0`, matching +//! PHP's `round(num, precision = 0)` signature. The registry enforces 1-2 args. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "round", + area: Math, + params: [num: Float, precision: Int = DefaultSpec::Int(0)], + returns: Float, + lower: lower, + summary: "Rounds a float.", + php_manual: "https://www.php.net/manual/en/function.round.php", +} + +/// Lowers a `round` call by dispatching to the shared float-rounding emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_round(ctx, inst) +} diff --git a/src/builtins/math/sin.rs b/src/builtins/math/sin.rs new file mode 100644 index 0000000000..b367c6d935 --- /dev/null +++ b/src/builtins/math/sin.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `sin` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `sin` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "sin", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the sine of a number (radians).", + php_manual: "https://www.php.net/manual/en/function.sin.php", +} + +/// Lowers a `sin` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "sin") +} diff --git a/src/builtins/math/sinh.rs b/src/builtins/math/sinh.rs new file mode 100644 index 0000000000..3a28c0d649 --- /dev/null +++ b/src/builtins/math/sinh.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `sinh` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `sinh` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "sinh", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the hyperbolic sine of a number.", + php_manual: "https://www.php.net/manual/en/function.sinh.php", +} + +/// Lowers a `sinh` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "sinh") +} diff --git a/src/builtins/math/sqrt.rs b/src/builtins/math/sqrt.rs new file mode 100644 index 0000000000..63f3534564 --- /dev/null +++ b/src/builtins/math/sqrt.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `sqrt` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `sqrt` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "sqrt", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the square root of a number.", + php_manual: "https://www.php.net/manual/en/function.sqrt.php", +} + +/// Lowers a `sqrt` call by dispatching to the native square-root emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_sqrt(ctx, inst) +} diff --git a/src/builtins/math/tan.rs b/src/builtins/math/tan.rs new file mode 100644 index 0000000000..bc1c9b2fee --- /dev/null +++ b/src/builtins/math/tan.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `tan` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `tan` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "tan", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the tangent of a number (radians).", + php_manual: "https://www.php.net/manual/en/function.tan.php", +} + +/// Lowers a `tan` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "tan") +} diff --git a/src/builtins/math/tanh.rs b/src/builtins/math/tanh.rs new file mode 100644 index 0000000000..dae5599901 --- /dev/null +++ b/src/builtins/math/tanh.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `tanh` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `tanh` is a pure-data builtin whose return type +//! (`Float`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "tanh", + area: Math, + params: [num: Float], + returns: Float, + lower: lower, + summary: "Returns the hyperbolic tangent of a number.", + php_manual: "https://www.php.net/manual/en/function.tanh.php", +} + +/// Lowers a `tanh` call by dispatching to the libm emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "tanh") +} diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs new file mode 100644 index 0000000000..d1b03d19b1 --- /dev/null +++ b/src/builtins/mod.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Single source of truth for PHP builtin functions: each builtin is declared +//! once via `builtin!` and collected through `inventory` into a lazy registry +//! that drives the catalog, signatures, type-check, lowering dispatch, and docs. +//! +//! Called from: +//! - `crate::types::checker::builtins`, `crate::types::signatures`, +//! `crate::codegen::lower_inst::builtins`, and `gen_builtins` (doc export). +//! +//! Key details: +//! - Homes live under `/.rs`; the legacy dispatch points fall back to +//! their old paths until every area has migrated. + +#[macro_use] +mod macros; +pub mod spec; +pub mod registry; +pub mod docs; +mod convert; +mod array; +mod callables; +mod io; +mod string; +mod math; +mod spl; +mod pointers; +mod system; +mod types; +#[cfg(test)] +mod parity_tests; diff --git a/src/builtins/parity_tests.rs b/src/builtins/parity_tests.rs new file mode 100644 index 0000000000..1871a0b098 --- /dev/null +++ b/src/builtins/parity_tests.rs @@ -0,0 +1,133 @@ +//! Purpose: +//! Parity gate between registry-derived builtin signatures and the legacy +//! `legacy_builtin_call_sig()` golden table. For every builtin registered in +//! the inventory that also has a legacy table entry, this file asserts that the +//! behavior-bearing fields of the two `FunctionSig`s agree. +//! +//! Called from: +//! - `cargo test` through Rust's test harness (unit test module). +//! +//! Key details: +//! - Placed here (not in `tests/`) because `legacy_builtin_call_sig` is +//! `pub(crate)` and cannot be reached from an integration test without +//! widening visibility. +//! - Type fields (`params[*].1`, `return_type`, `declared_return`, +//! `declared_params`) are intentionally excluded from comparison: the registry +//! derives precise types while the legacy table uses `PhpType::Mixed` +//! placeholders. That precision is an intended improvement and is +//! behavior-neutral (call-arg planning reads only param NAMES, never types). +//! - The gate compares against `legacy_builtin_call_sig` (not `builtin_call_sig`) +//! so that the comparison is non-vacuous: `builtin_call_sig` checks the registry +//! first, so comparing registry::function_sig against builtin_call_sig would +//! simply compare a value against itself for migrated builtins. +//! - Migration rule: when a builtin is moved into `src/builtins/`, its arm is +//! KEPT in `legacy_builtin_call_sig` as the parity golden. Remove the arm only +//! after the parity gate has verified the registry matches and the golden is no +//! longer needed. + +use crate::builtins::registry; +use crate::types::{legacy_builtin_call_sig, FunctionSig}; + +/// Asserts that the behavior-bearing fields of `derived` and `legacy` agree. +/// +/// Fields compared: +/// - param names (`.0` of each `(String, PhpType)` pair, in order) +/// - defaults (rendered via `{:?}` for a stable comparison of `Option`) +/// - ref_params (per-parameter by-reference flags) +/// - variadic (the variadic parameter name, if any) +/// - by_ref_return +/// - total param count and required param count (arity) +/// +/// Panics with a message naming the builtin and the diverging field. +fn assert_behavior_fields_match(name: &str, derived: &FunctionSig, legacy: &FunctionSig) { + // Arity: total param count. + assert_eq!( + derived.params.len(), + legacy.params.len(), + "signature drift for {name}: param count differs (derived={}, legacy={})", + derived.params.len(), + legacy.params.len(), + ); + + // Required param count: params with no default. + let derived_required = derived.defaults.iter().filter(|d| d.is_none()).count(); + let legacy_required = legacy.defaults.iter().filter(|d| d.is_none()).count(); + assert_eq!( + derived_required, + legacy_required, + "signature drift for {name}: required param count differs (derived={derived_required}, legacy={legacy_required})", + ); + + // Param names (in order). + let derived_names: Vec<&str> = derived.params.iter().map(|(n, _)| n.as_str()).collect(); + let legacy_names: Vec<&str> = legacy.params.iter().map(|(n, _)| n.as_str()).collect(); + assert_eq!( + derived_names, + legacy_names, + "signature drift for {name}: param names differ (derived={derived_names:?}, legacy={legacy_names:?})", + ); + + // Defaults (stable debug representation for Option). + let derived_defaults = format!("{:?}", derived.defaults); + let legacy_defaults = format!("{:?}", legacy.defaults); + assert_eq!( + derived_defaults, + legacy_defaults, + "signature drift for {name}: defaults differ\n derived={derived_defaults}\n legacy={legacy_defaults}", + ); + + // Per-parameter by-reference flags. + assert_eq!( + derived.ref_params, + legacy.ref_params, + "signature drift for {name}: ref_params differ (derived={:?}, legacy={:?})", + derived.ref_params, + legacy.ref_params, + ); + + // Variadic parameter name. + assert_eq!( + derived.variadic, + legacy.variadic, + "signature drift for {name}: variadic differs (derived={:?}, legacy={:?})", + derived.variadic, + legacy.variadic, + ); + + // By-reference return flag. + assert_eq!( + derived.by_ref_return, + legacy.by_ref_return, + "signature drift for {name}: by_ref_return differs (derived={}, legacy={})", + derived.by_ref_return, + legacy.by_ref_return, + ); +} + +/// Verifies that every registry-derived builtin signature agrees with the legacy +/// `legacy_builtin_call_sig()` golden table on all behavior-bearing fields. +/// +/// Iterates all names registered in the inventory. For each name that also has +/// a golden legacy entry, runs `assert_behavior_fields_match`. Names with no +/// legacy entry (internal test probes, or builtins not yet assigned a golden) +/// are skipped — the gate activates incrementally as migration tasks register +/// real builtins and retain their legacy arms as goldens. +/// +/// The comparison uses `legacy_builtin_call_sig` (NOT `builtin_call_sig`) so that +/// the test is non-vacuous: `builtin_call_sig` checks the registry first, so for +/// any migrated builtin both sides would resolve to the same registry value and +/// the assertion would always trivially pass. +#[test] +fn derived_signatures_match_legacy() { + for name in registry::names() { + // Skip internal test probes and builtins not yet assigned a legacy golden. + let Some(legacy) = legacy_builtin_call_sig(name) else { + continue; + }; + + let derived = registry::function_sig(name) + .unwrap_or_else(|| panic!("registry::names() yielded {name} but function_sig returned None")); + + assert_behavior_fields_match(name, &derived, &legacy); + } +} diff --git a/src/builtins/pointers/mod.rs b/src/builtins/pointers/mod.rs new file mode 100644 index 0000000000..a7e46e42cb --- /dev/null +++ b/src/builtins/pointers/mod.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Groups all `pointers`-area builtin homes into this module so the registry can +//! collect them in one place. Each submodule declares exactly one builtin via +//! `builtin!` and provides its check hook and lowering hook. +//! +//! Called from: +//! - `crate::builtins` (`mod pointers;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - All 15 pointer builtins require a `check` hook because they validate +//! pointer/argument types at compile time and some return `PhpType::Pointer(None)` +//! which `TypeSpec` cannot express statically. +//! - Add `pub mod ;` here for every new pointer builtin home. + +pub mod ptr; +pub mod ptr_get; +pub mod ptr_is_null; +pub mod ptr_null; +pub mod ptr_offset; +pub mod ptr_read16; +pub mod ptr_read32; +pub mod ptr_read8; +pub mod ptr_read_string; +pub mod ptr_set; +pub mod ptr_sizeof; +pub mod ptr_write16; +pub mod ptr_write32; +pub mod ptr_write8; +pub mod ptr_write_string; diff --git a/src/builtins/pointers/ptr.rs b/src/builtins/pointers/ptr.rs new file mode 100644 index 0000000000..1042b0746e --- /dev/null +++ b/src/builtins/pointers/ptr.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Home of the PHP `ptr` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a variable (not an arbitrary expression) +//! and returns `PhpType::Pointer(None)`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "ptr", + area: Pointers, + params: [value: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns a raw pointer to the given variable.", +} + +/// Validates that the argument is a variable and returns `PhpType::Pointer(None)`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +/// `ptr()` requires a variable as its argument because taking the address of an +/// arbitrary expression has no well-defined meaning in the pointer model. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + match &cx.args[0].kind { + ExprKind::Variable(_) => { + cx.checker.infer_type(&cx.args[0], cx.env)?; + } + _ => { + return Err(CompileError::new( + cx.span, + "ptr() argument must be a variable", + )); + } + } + Ok(PhpType::Pointer(None)) +} + +/// Lowers a `ptr` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_get.rs b/src/builtins/pointers/ptr_get.rs new file mode 100644 index 0000000000..09ea07714e --- /dev/null +++ b/src/builtins/pointers/ptr_get.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `ptr_get` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a pointer type and returns `PhpType::Int`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_get` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_get", + area: Pointers, + params: [pointer: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Reads one machine word through a raw pointer and returns it as an integer.", +} + +/// Validates that the argument is a pointer type and returns `PhpType::Int`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ty, cx.span, &format!("{}()", cx.name))?; + Ok(PhpType::Int) +} + +/// Lowers a `ptr_get` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_get(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_is_null.rs b/src/builtins/pointers/ptr_is_null.rs new file mode 100644 index 0000000000..0bb8b9806e --- /dev/null +++ b/src/builtins/pointers/ptr_is_null.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `ptr_is_null` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a pointer type and returns `PhpType::Bool`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_is_null` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_is_null", + area: Pointers, + params: [pointer: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Returns true if the pointer is null.", +} + +/// Validates that the argument is a pointer type and returns `PhpType::Bool`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ty, cx.span, "ptr_is_null()")?; + Ok(PhpType::Bool) +} + +/// Lowers a `ptr_is_null` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_is_null(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_null.rs b/src/builtins/pointers/ptr_null.rs new file mode 100644 index 0000000000..ad96f2a589 --- /dev/null +++ b/src/builtins/pointers/ptr_null.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `ptr_null` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` takes no arguments and returns `PhpType::Pointer(None)`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_null` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_null", + area: Pointers, + params: [], + arity_error: "ptr_null() takes 0 arguments", + returns: Mixed, + check: check, + lower: lower, + summary: "Returns a null raw pointer.", +} + +/// Returns `PhpType::Pointer(None)` unconditionally (no arguments to validate). +/// +/// The registry's `check_arity` handles arity enforcement (exactly 0 arguments). +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Pointer(None)) +} + +/// Lowers a `ptr_null` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_null(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_offset.rs b/src/builtins/pointers/ptr_offset.rs new file mode 100644 index 0000000000..16ed75ea98 --- /dev/null +++ b/src/builtins/pointers/ptr_offset.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Home of the PHP `ptr_offset` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the first argument is a pointer and the second is an +//! integer-compatible offset, preserving the pointer's inner type annotation. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_offset` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_offset", + area: Pointers, + params: [pointer: Mixed, offset: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns a new pointer offset from the given pointer by the given byte count.", +} + +/// Validates pointer and integer-compatible offset arguments and returns the pointer type. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// Returns the type of the first argument (the pointer) so that pointer type annotations +/// are propagated through the offset expression. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ptr_ty, cx.span, "ptr_offset()")?; + let offset_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!( + offset_ty, + PhpType::Int | PhpType::Mixed | PhpType::Union(_) + ) { + return Err(CompileError::new( + cx.span, + "ptr_offset() second argument must be integer", + )); + } + Ok(ptr_ty) +} + +/// Lowers a `ptr_offset` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_offset(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_read16.rs b/src/builtins/pointers/ptr_read16.rs new file mode 100644 index 0000000000..0ffb9916bc --- /dev/null +++ b/src/builtins/pointers/ptr_read16.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `ptr_read16` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a pointer type and returns `PhpType::Int`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_read16` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_read16", + area: Pointers, + params: [pointer: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", +} + +/// Validates that the argument is a pointer type and returns `PhpType::Int`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ty, cx.span, &format!("{}()", cx.name))?; + Ok(PhpType::Int) +} + +/// Lowers a `ptr_read16` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_read16(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_read32.rs b/src/builtins/pointers/ptr_read32.rs new file mode 100644 index 0000000000..7d6c646bac --- /dev/null +++ b/src/builtins/pointers/ptr_read32.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `ptr_read32` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a pointer type and returns `PhpType::Int`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_read32` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_read32", + area: Pointers, + params: [pointer: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", +} + +/// Validates that the argument is a pointer type and returns `PhpType::Int`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ty, cx.span, &format!("{}()", cx.name))?; + Ok(PhpType::Int) +} + +/// Lowers a `ptr_read32` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_read32(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_read8.rs b/src/builtins/pointers/ptr_read8.rs new file mode 100644 index 0000000000..15a3392f1a --- /dev/null +++ b/src/builtins/pointers/ptr_read8.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `ptr_read8` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a pointer type and returns `PhpType::Int`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_read8` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_read8", + area: Pointers, + params: [pointer: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Reads one unsigned byte through a raw pointer and returns it as an integer.", +} + +/// Validates that the argument is a pointer type and returns `PhpType::Int`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ty, cx.span, &format!("{}()", cx.name))?; + Ok(PhpType::Int) +} + +/// Lowers a `ptr_read8` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_read8(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_read_string.rs b/src/builtins/pointers/ptr_read_string.rs new file mode 100644 index 0000000000..97b4b0aeb4 --- /dev/null +++ b/src/builtins/pointers/ptr_read_string.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `ptr_read_string` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the first argument is a pointer and the second is an integer +//! length, and returns `PhpType::Str`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_read_string` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_read_string", + area: Pointers, + params: [pointer: Mixed, length: Mixed], + returns: Str, + check: check, + lower: lower, + summary: "Copies raw bytes from a pointer into a PHP string of the given length.", +} + +/// Validates pointer and integer length arguments and returns `PhpType::Str`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ptr_ty, cx.span, "ptr_read_string()")?; + let len_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if len_ty != PhpType::Int { + return Err(CompileError::new( + cx.span, + "ptr_read_string() length must be int", + )); + } + Ok(PhpType::Str) +} + +/// Lowers a `ptr_read_string` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_read_string(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_set.rs b/src/builtins/pointers/ptr_set.rs new file mode 100644 index 0000000000..692a2d1be3 --- /dev/null +++ b/src/builtins/pointers/ptr_set.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `ptr_set` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates pointer and word-sized value arguments and returns `PhpType::Void`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_set` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_set", + area: Pointers, + params: [pointer: Mixed, value: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Writes one machine word through a raw pointer.", +} + +/// Validates pointer and word-compatible value arguments and returns `PhpType::Void`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// The value argument must be a word-pointer-compatible type (int, bool, pointer, etc.). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ptr_ty, cx.span, "ptr_set()")?; + let value_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + cx.checker.ensure_word_pointer_value(&value_ty, cx.span)?; + Ok(PhpType::Void) +} + +/// Lowers a `ptr_set` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_set(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_sizeof.rs b/src/builtins/pointers/ptr_sizeof.rs new file mode 100644 index 0000000000..a11a4e74c1 --- /dev/null +++ b/src/builtins/pointers/ptr_sizeof.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Home of the PHP `ptr_sizeof` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a known string literal type name and +//! returns `PhpType::Int` (the byte size of the named type). +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_sizeof` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "ptr_sizeof", + area: Pointers, + params: [r#type: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Returns the byte size of the named pointer target type.", +} + +/// Validates that the argument is a known string literal type name and returns `PhpType::Int`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 1 argument). +/// The argument must be a string literal (not a variable) containing a recognized +/// pointer target type name such as `"int"`, `"float"`, `"string"`, or a class name. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + match &cx.args[0].kind { + ExprKind::StringLiteral(type_name) => { + if cx.checker.normalize_pointer_target_type(type_name).is_none() { + return Err(CompileError::new( + cx.span, + &format!("Unknown type for ptr_sizeof(): {}", type_name), + )); + } + } + _ => { + return Err(CompileError::new( + cx.span, + "ptr_sizeof() argument must be a string literal", + )); + } + } + Ok(PhpType::Int) +} + +/// Lowers a `ptr_sizeof` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_sizeof(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_write16.rs b/src/builtins/pointers/ptr_write16.rs new file mode 100644 index 0000000000..e8ef09f4a2 --- /dev/null +++ b/src/builtins/pointers/ptr_write16.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `ptr_write16` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates pointer and integer value arguments and returns `PhpType::Void`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_write16` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_write16", + area: Pointers, + params: [pointer: Mixed, value: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Writes one 16-bit word through a raw pointer.", +} + +/// Validates pointer and integer value arguments and returns `PhpType::Void`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// The value argument must be an integer (16-bit writes do not accept pointer values). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ptr_ty, cx.span, &format!("{}()", cx.name))?; + let value_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if value_ty != PhpType::Int { + return Err(CompileError::new( + cx.span, + &format!("{}() value must be int", cx.name), + )); + } + Ok(PhpType::Void) +} + +/// Lowers a `ptr_write16` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_write16(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_write32.rs b/src/builtins/pointers/ptr_write32.rs new file mode 100644 index 0000000000..f8db123f21 --- /dev/null +++ b/src/builtins/pointers/ptr_write32.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `ptr_write32` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates pointer and integer value arguments and returns `PhpType::Void`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_write32` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_write32", + area: Pointers, + params: [pointer: Mixed, value: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Writes one 32-bit word through a raw pointer.", +} + +/// Validates pointer and integer value arguments and returns `PhpType::Void`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// The value argument must be an integer (32-bit writes do not accept pointer values). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ptr_ty, cx.span, &format!("{}()", cx.name))?; + let value_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if value_ty != PhpType::Int { + return Err(CompileError::new( + cx.span, + &format!("{}() value must be int", cx.name), + )); + } + Ok(PhpType::Void) +} + +/// Lowers a `ptr_write32` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_write32(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_write8.rs b/src/builtins/pointers/ptr_write8.rs new file mode 100644 index 0000000000..6168b770a3 --- /dev/null +++ b/src/builtins/pointers/ptr_write8.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `ptr_write8` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates pointer and integer value arguments and returns `PhpType::Void`. +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_write8` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_write8", + area: Pointers, + params: [pointer: Mixed, value: Mixed], + returns: Void, + check: check, + lower: lower, + summary: "Writes one byte through a raw pointer.", +} + +/// Validates pointer and integer value arguments and returns `PhpType::Void`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// The value argument must be an integer (byte writes do not accept pointer values). +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ptr_ty, cx.span, &format!("{}()", cx.name))?; + let value_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if value_ty != PhpType::Int { + return Err(CompileError::new( + cx.span, + &format!("{}() value must be int", cx.name), + )); + } + Ok(PhpType::Void) +} + +/// Lowers a `ptr_write8` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_write8(ctx, inst) +} diff --git a/src/builtins/pointers/ptr_write_string.rs b/src/builtins/pointers/ptr_write_string.rs new file mode 100644 index 0000000000..42d357f394 --- /dev/null +++ b/src/builtins/pointers/ptr_write_string.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Home of the PHP `ptr_write_string` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates pointer and string arguments and returns `PhpType::Int` +//! (the number of bytes written). +//! - `lower` is a thin wrapper over the shared `pointers::lower_ptr_write_string` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ptr_write_string", + area: Pointers, + params: [pointer: Mixed, string: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Copies PHP string bytes into raw memory at the given pointer.", +} + +/// Validates pointer and string arguments and returns `PhpType::Int`. +/// +/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). +/// Returns the number of bytes written as an integer. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ptr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.ensure_pointer_type(&ptr_ty, cx.span, "ptr_write_string()")?; + let str_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if str_ty != PhpType::Str { + return Err(CompileError::new( + cx.span, + "ptr_write_string() string argument must be string", + )); + } + Ok(PhpType::Int) +} + +/// Lowers a `ptr_write_string` call by dispatching to the shared pointer emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::pointers::lower_ptr_write_string(ctx, inst) +} diff --git a/src/builtins/registry.rs b/src/builtins/registry.rs new file mode 100644 index 0000000000..7bcd83a66b --- /dev/null +++ b/src/builtins/registry.rs @@ -0,0 +1,598 @@ +//! Purpose: +//! Collects all `BuiltinSpec` entries submitted via `builtin!` into a lazy registry, +//! and exposes lookup helpers used by the catalog, type checker, and codegen dispatcher. +//! +//! Called from: +//! - `crate::types::checker::builtins::catalog` for name-based lookup. +//! - `crate::codegen::lower_inst::builtins` for lowering-hook dispatch. +//! +//! Key details: +//! - Registry is initialized once at first access via a `OnceLock`; subsequent calls +//! are read-only and lock-free. +//! - Lookup is case-insensitive to match PHP's builtin name semantics. +//! - Duplicate builtin names panic at registry initialization time (link-time guard). + +use std::collections::HashMap; +use std::sync::OnceLock; + +use crate::builtins::convert::{default_spec_to_expr, type_spec_to_php}; +use crate::builtins::spec::BuiltinSpec; +use crate::errors::CompileError; +use crate::parser::ast::{Expr, ExprKind}; +use crate::span::Span; +use crate::types::{callable_wrapper_sig, FunctionSig, PhpType}; + +/// The rich runtime form of a PHP builtin function descriptor. +/// +/// Built from a `BuiltinSpec` by the registry at first access. The spec's static +/// `TypeSpec`/`DefaultSpec` fields are converted into `PhpType`/`Expr` via `convert.rs`. +/// The variadic parameter (if any) is appended to `params`/`defaults`/`ref_params`. +pub struct BuiltinDef { + /// The canonical PHP function name (case-preserved, no leading backslash). + pub name: &'static str, + /// The PHP-level parameter list: `(name, type)` pairs in source order. + /// Includes the variadic parameter (if any) appended as the last entry. + pub params: Vec<(String, PhpType)>, + /// Default values in the same order as `params`. + /// `None` = required; `Some(expr)` = optional with that default. + /// The variadic parameter always carries `Some(ArrayLiteral([]))`. + pub defaults: Vec>, + /// Per-parameter by-reference flag, in the same order as `params`. + /// The variadic parameter is never by-reference (`false`). + pub ref_params: Vec, + /// Name of the variadic parameter, if any. + pub variadic: Option, + /// The PHP-level return type, derived from the spec's `TypeSpec` via `type_spec_to_php`. + pub return_type: PhpType, + /// Whether this function returns by reference. + pub by_ref_return: bool, + /// Reference back to the original static `BuiltinSpec` for hooks and metadata. + pub spec: &'static BuiltinSpec, +} + +/// Global lazy registry: ASCII-lowercase-keyed map from builtin name to `BuiltinDef`. +static REGISTRY: OnceLock> = OnceLock::new(); + +/// Builds the registry by iterating all `BuiltinSpec`s collected by `inventory`. +/// +/// Panics immediately if two specs register the same name (case-insensitive comparison), +/// so duplicate registrations are caught at program startup. +fn build_registry() -> HashMap { + let mut map: HashMap = HashMap::new(); + for spec in inventory::iter:: { + let key = spec.name.to_ascii_lowercase(); + if map.contains_key(&key) { + panic!( + "duplicate builtin name registered in inventory: \"{}\"", + spec.name + ); + } + + // Convert the fixed parameter list. + let param_count = spec.params.len(); + let variadic_count = if spec.variadic.is_some() { 1 } else { 0 }; + let total = param_count + variadic_count; + + let mut params: Vec<(String, PhpType)> = Vec::with_capacity(total); + let mut defaults: Vec> = Vec::with_capacity(total); + let mut ref_params: Vec = Vec::with_capacity(total); + + for p in spec.params { + params.push((p.name.to_string(), type_spec_to_php(&p.ty))); + defaults.push(p.default.as_ref().map(default_spec_to_expr)); + ref_params.push(p.by_ref); + } + + // Append the variadic parameter with an empty-array default, matching the + // convention used by the legacy `variadic()` helper in `src/types/signatures.rs`. + if let Some(var_name) = spec.variadic { + params.push((var_name.to_string(), PhpType::Mixed)); + defaults.push(Some(Expr::new( + ExprKind::ArrayLiteral(Vec::new()), + Span::dummy(), + ))); + ref_params.push(false); + } + + let def = BuiltinDef { + name: spec.name, + params, + defaults, + ref_params, + variadic: spec.variadic.map(str::to_string), + return_type: type_spec_to_php(&spec.returns), + by_ref_return: spec.by_ref_return, + spec, + }; + map.insert(key, def); + } + map +} + +/// Returns the global registry, initializing it on first call. +/// +/// The registry is built exactly once (via `OnceLock`); all subsequent accesses +/// are read-only and lock-free. +fn registry() -> &'static HashMap { + REGISTRY.get_or_init(build_registry) +} + +/// Looks up a PHP builtin by name, using case-insensitive matching. +/// +/// Returns `None` if the name is not registered in the inventory. +pub fn lookup(name: &str) -> Option<&'static BuiltinDef> { + let lower = name.to_ascii_lowercase(); + registry().get(&lower) +} + +/// Returns `true` if the given name is a known PHP builtin. +pub fn is_supported(name: &str) -> bool { + lookup(name).is_some() +} + +/// Returns an iterator over all registered canonical builtin names in sorted order. +/// +/// Names are returned in stable lexicographic order (sorted by `&'static str`) +/// with case-preserved spelling (i.e., as originally supplied to `builtin!`). +/// Sorting ensures deterministic assembly layout across compiler builds. +/// Used primarily from test and documentation-generation contexts. +#[allow(dead_code)] +pub fn names() -> impl Iterator { + let mut sorted: Vec<&str> = registry().values().map(|def| def.name).collect(); + sorted.sort_unstable(); + sorted.into_iter() +} + +/// Derives a `FunctionSig` for the named builtin from the registry. +/// +/// The returned sig matches the field layout the legacy `builtin_call_sig()` arms +/// produce via `make_sig`, with the following field mapping: +/// +/// | `FunctionSig` field | Source | +/// |------------------------|------------------------------------------------| +/// | `params` | `BuiltinDef.params` (typed via `TypeSpec`) | +/// | `defaults` | `BuiltinDef.defaults` (via `DefaultSpec`) | +/// | `return_type` | `BuiltinDef.return_type` (via `TypeSpec`) | +/// | `declared_return` | `false` (matching `make_sig` convention) | +/// | `by_ref_return` | `BuiltinDef.by_ref_return` (from spec) | +/// | `ref_params` | `BuiltinDef.ref_params` (from spec) | +/// | `declared_params` | `vec![false; N]` (matching `make_sig`) | +/// | `variadic` | `BuiltinDef.variadic` (from spec) | +/// | `deprecation` | `spec.deprecation` mapped to `Option` | +/// +/// Returns `None` if the builtin is not registered. +pub fn function_sig(name: &str) -> Option { + let def = lookup(name)?; + Some(FunctionSig { + params: def.params.clone(), + defaults: def.defaults.clone(), + return_type: def.return_type.clone(), + declared_return: false, + by_ref_return: def.by_ref_return, + ref_params: def.ref_params.clone(), + declared_params: vec![false; def.params.len()], + variadic: def.variadic.clone(), + deprecation: def.spec.deprecation.map(str::to_string), + }) +} + +/// Derives a first-class-callable `FunctionSig` for the named builtin. +/// +/// Applies `callable_wrapper_sig` to the base `function_sig`, upgrading the +/// variadic parameter (if any) to `Array` as required for first-class use. +/// This reuses the same upgrade logic applied by the legacy `callable_wrapper_sig` +/// helper in `src/types/signatures.rs` rather than reinventing it. +/// +/// Sets `declared_return: true` on the resulting signature, mirroring the +/// `typed_first_class_builtin_sig` convention used by the legacy table. First-class +/// callable sigs have a known, declared return type (they are typed wrappers, not +/// type-erased callables), so `declared_return` must be `true`. +/// +/// Returns `None` if the builtin is not registered. +pub fn first_class_callable_sig(name: &str) -> Option { + let sig = function_sig(name)?; + let mut fcc_sig = callable_wrapper_sig(&sig); + refine_first_class_callable_sig(name, &mut fcc_sig); + fcc_sig.declared_return = true; + Some(fcc_sig) +} + +/// Applies first-class-callable refinements that are broader in the direct builtin spec. +fn refine_first_class_callable_sig(name: &str, sig: &mut FunctionSig) { + match crate::names::php_symbol_key(name.trim_start_matches('\\')).as_str() { + "preg_replace_callback" => { + if let Some((_, callback_ty)) = sig.params.get_mut(1) { + *callback_ty = PhpType::Callable; + } + } + _ => {} + } +} + +/// Returns the minimum and maximum arity for the named builtin. +/// +/// - `min`: count of parameters with no default (i.e., required). +/// - `max`: `None` for variadic functions, `Some(n)` for fixed-arity functions +/// where `n` is the total parameter count including optional ones. +/// +/// Returns `None` if the builtin is not registered. +pub fn arity_bounds(name: &str) -> Option<(usize, Option)> { + let def = lookup(name)?; + let min = def.defaults.iter().filter(|d| d.is_none()).count(); + let max = if def.variadic.is_some() { + None + } else { + Some(def.params.len()) + }; + Some((min, max)) +} + +/// Validates the argument count for a named builtin and returns a standard arity error on mismatch. +/// +/// Uses `arity_bounds(name)` to determine the expected arity and compares it against +/// `arg_count`. Returns `Ok(())` when the count is in range. Returns a `CompileError` +/// with `span` and a message matching the dominant legacy `"() takes …"` phrasing: +/// +/// - `min == max == 0`: `"() takes no arguments"` +/// - `min == max == 1`: `"() takes exactly 1 argument"` (singular) +/// - `min == max > 1`: `"() takes exactly N arguments"` (plural) +/// - `max == None` (variadic), `min == 1`: `"() takes at least 1 argument"` (singular) +/// - `max == None` (variadic), `min > 1`: `"() takes at least N arguments"` (plural) +/// - `max == Some(M)`, `min == 0`, `M == 1`: `"() takes at most 1 argument"` (singular) +/// - `max == Some(M)`, `min == 0`, `M > 1`: `"() takes at most M arguments"` (plural) +/// - `max == Some(M)`, `min < M`, `M == min + 1`: `"() takes N or M arguments"` (e.g., `"substr() takes 2 or 3 arguments"`) +/// - `max == Some(M)`, `min < M`, `M > min + 1`: `"() takes N to M arguments"` (e.g., `"str_pad() takes 2 to 4 arguments"`) +/// +/// Returns `Ok(())` without error if `name` is not registered (unknown builtins are handled +/// upstream by the catalog / type checker, which provides its own unknown-name diagnostic). +/// +/// When the registered spec carries a `max_args` override, that value caps the maximum +/// accepted argument count for this check only. `function_sig`, `arity_bounds`, and the +/// parity gate keep the full param-derived bounds, so the override never affects argument +/// normalization or the registry/legacy signature parity comparison. +pub fn check_arity(name: &str, arg_count: usize, span: Span) -> Result<(), CompileError> { + // Param-derived bounds, identical to what the parity gate compares against. + // `min` is the count of required params; `param_max` is the declared maximum. + let (min, param_max) = match arity_bounds(name) { + Some(bounds) => bounds, + None => return Ok(()), + }; + // Apply the `min_args` override (if any) to the minimum only. + let min = match lookup(name).and_then(|def| def.spec.min_args) { + Some(raised) => raised, + None => min, + }; + // Apply the `max_args` override (if any) to the maximum only. The minimum stays + // param-derived; the override exists to tighten the accepted maximum for builtins + // whose legacy CHECK arm was stricter than their declared (golden) signature. + let max = match lookup(name).and_then(|def| def.spec.max_args) { + Some(capped) => Some(capped), + None => param_max, + }; + + let in_range = match max { + None => arg_count >= min, + Some(m) => arg_count >= min && arg_count <= m, + }; + + if in_range { + return Ok(()); + } + + // Use a custom verbatim message if the spec provides one; otherwise derive + // the standard "() takes …" phrasing from the enforced arity bounds. + if let Some(msg) = lookup(name).and_then(|def| def.spec.arity_error) { + return Err(CompileError::new(span, msg)); + } + + let msg = match (min, max) { + (0, Some(0)) => format!("{}() takes no arguments", name), + (n, Some(m)) if n == m && n == 1 => { + format!("{}() takes exactly 1 argument", name) + } + (n, Some(m)) if n == m => { + format!("{}() takes exactly {} arguments", name, n) + } + (n, None) if n == 1 => format!("{}() takes at least 1 argument", name), + (n, None) => format!("{}() takes at least {} arguments", name, n), + (0, Some(1)) => format!("{}() takes at most 1 argument", name), + (0, Some(m)) => format!("{}() takes at most {} arguments", name, m), + // Consecutive two-value range: use "N or M" to match PHP's natural phrasing + // (e.g. "substr() takes 2 or 3 arguments", "substr_replace() takes 3 or 4 arguments"). + (n, Some(m)) if m == n + 1 => format!("{}() takes {} or {} arguments", name, n, m), + (n, Some(m)) => format!("{}() takes {} to {} arguments", name, n, m), + }; + + Err(CompileError::new(span, &msg)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builtins::spec::DefaultSpec; + + /// No-op lowering hook used by test probe builtins; does nothing and succeeds. + fn noop_lower( + _c: &mut crate::codegen::context::FunctionContext, + _i: &crate::ir::Instruction, + ) -> Result<(), crate::codegen::CodegenIrError> { + Ok(()) + } + + // Register a registry-specific probe so tests do not depend solely on the + // spec-module probe (which lives in a different cfg(test) module). + builtin! { + name: "__registry_probe_opt", + area: Internal, + params: [a: Int, b: Str = DefaultSpec::Null], + returns: Bool, + lower: noop_lower, + summary: "registry arity probe", + internal: true, + } + + builtin! { + name: "__registry_probe_variadic", + area: Internal, + params: [fmt: Str], + variadic: "__registry_values", + returns: Str, + lower: noop_lower, + summary: "registry variadic probe", + internal: true, + } + + // Probe whose `max_args` (2) is smaller than its declared param count (3, since + // `c` is optional). Used to verify the override caps `check_arity` without + // affecting `function_sig`'s full param count. + builtin! { + name: "__registry_probe_capped", + area: Internal, + params: [a: Int, b: Int, c: Int = DefaultSpec::Int(0)], + max_args: 2, + returns: Int, + lower: noop_lower, + summary: "registry capped-arity probe", + internal: true, + } + + builtin! { + name: "__registry_probe_raised_min", + area: Internal, + params: [], + variadic: "__registry_arrays", + min_args: 2, + returns: Mixed, + lower: noop_lower, + summary: "registry raised-min probe", + internal: true, + } + + builtin! { + name: "__registry_probe_arity_error", + area: Internal, + params: [algo: Str, flags: Int = DefaultSpec::Int(0)], + min_args: 1, + max_args: 1, + arity_error: "custom arity error message for probe", + returns: Mixed, + lower: noop_lower, + summary: "registry arity_error probe", + internal: true, + } + + builtin! { + name: "__registry_probe_byref", + area: Internal, + params: [ref target: Mixed, value: Int], + returns: Mixed, + lower: noop_lower, + summary: "registry by-ref param probe", + internal: true, + } + + /// Verifies the registry derives FunctionSig arity/return for a registered builtin. + #[test] + fn registry_derives_signature() { + // assumes a `substr`-shaped probe is registered in this build + let sig = function_sig("__macro_probe").expect("probe registered"); + assert_eq!(sig.params.len(), 1); + assert_eq!(sig.return_type, crate::types::PhpType::Int); + } + + /// Verifies `lookup` returns a `BuiltinDef` for a registered builtin. + #[test] + fn lookup_finds_registered_builtin() { + let def = lookup("__macro_probe").expect("probe must be in registry"); + assert_eq!(def.name, "__macro_probe"); + } + + /// Verifies case-insensitive lookup works (PHP builtin name semantics). + #[test] + fn lookup_is_case_insensitive() { + assert!(lookup("__MACRO_PROBE").is_some()); + assert!(lookup("__Macro_Probe").is_some()); + } + + /// Verifies `is_supported` returns true for registered builtins. + #[test] + fn is_supported_returns_true_for_known_builtin() { + assert!(is_supported("__macro_probe")); + } + + /// Verifies `is_supported` returns false for unknown names. + #[test] + fn is_supported_returns_false_for_unknown() { + assert!(!is_supported("__not_a_real_builtin_xyz")); + } + + /// Verifies `names()` includes the probe builtin. + #[test] + fn names_includes_registered_builtin() { + let all: Vec<&str> = names().collect(); + assert!( + all.contains(&"__macro_probe"), + "names() must yield all registered builtins" + ); + } + + /// Verifies `names()` returns builtin names in sorted order for determinism. + #[test] + fn names_returns_sorted_order() { + let names_vec: Vec<&str> = names().collect(); + let mut sorted_vec = names_vec.clone(); + sorted_vec.sort(); + assert_eq!( + names_vec, sorted_vec, + "names() must return sorted order for deterministic assembly layout" + ); + } + + /// Verifies the derived arity error mirrors the legacy "() takes …" messages. + #[test] + fn arity_messages_match_legacy() { + // probe: exactly 1 arg + let err = check_arity("__macro_probe", 2, crate::span::Span::dummy()).unwrap_err(); + assert!(err.message.contains("__macro_probe() takes exactly 1 argument")); + } + + /// Verifies the `max_args` override caps `check_arity` (here to 2, below the + /// 3-param declared signature) while `function_sig` still reports the full + /// param count. The override must affect only arity validation, never the + /// derived signature consumed by argument normalization and the parity gate. + #[test] + fn max_args_caps_check_arity_but_not_function_sig() { + // __registry_probe_capped: params [a, b, c=0], max_args=2. + // Calling with 3 args exceeds the capped max → arity error. + let err = check_arity("__registry_probe_capped", 3, crate::span::Span::dummy()) + .expect_err("3 args must exceed the max_args=2 cap"); + assert!( + err.message + .contains("__registry_probe_capped() takes exactly 2 arguments"), + "capped arity error mismatch: {}", + err.message, + ); + // function_sig is unaffected by the override: it reports all 3 params. + let sig = function_sig("__registry_probe_capped").expect("probe registered"); + assert_eq!( + sig.params.len(), + 3, + "function_sig must report the full param count, ignoring max_args", + ); + // A call within the cap (2 args) is accepted. + assert!(check_arity("__registry_probe_capped", 2, crate::span::Span::dummy()).is_ok()); + } + + /// Verifies `min_args` raises the enforced minimum above the param-derived count, + /// and `arity_error` overrides the standard message. Both affect ONLY `check_arity`; + /// `function_sig` keeps the full param-derived shape unaffected. + #[test] + fn min_args_and_arity_error_affect_only_check_arity() { + // __registry_probe_raised_min: variadic (min=0 param-derived), min_args=2. + // Calling with 1 arg is below the raised minimum → arity error. + let err = check_arity("__registry_probe_raised_min", 1, crate::span::Span::dummy()) + .expect_err("1 arg must be below the raised min_args=2"); + assert!( + err.message.contains("__registry_probe_raised_min() takes at least 2 arguments"), + "raised-min error mismatch: {}", + err.message, + ); + // 2 args passes. + assert!(check_arity("__registry_probe_raised_min", 2, crate::span::Span::dummy()).is_ok()); + // function_sig is unaffected: variadic, 1 param (the variadic param). + let sig = function_sig("__registry_probe_raised_min").expect("probe registered"); + assert!(sig.variadic.is_some(), "function_sig must show variadic unchanged"); + + // __registry_probe_arity_error: params [algo, flags=0], min_args=1, max_args=1. + // Calling with 0 args → arity error using the custom message. + let err = check_arity("__registry_probe_arity_error", 0, crate::span::Span::dummy()) + .expect_err("0 args must trigger the custom arity error"); + assert_eq!( + err.message, "custom arity error message for probe", + "arity_error message mismatch: {}", + err.message, + ); + // Calling with 2 args also triggers the custom error (exceeds max_args=1). + let err = check_arity("__registry_probe_arity_error", 2, crate::span::Span::dummy()) + .expect_err("2 args must exceed max_args=1"); + assert_eq!( + err.message, "custom arity error message for probe", + "arity_error must be used for all mismatches: {}", + err.message, + ); + // 1 arg is in range. + assert!(check_arity("__registry_probe_arity_error", 1, crate::span::Span::dummy()).is_ok()); + // function_sig is unaffected: 2 params (algo + flags). + let sig = function_sig("__registry_probe_arity_error").expect("probe registered"); + assert_eq!(sig.params.len(), 2, "function_sig must report 2 params unchanged"); + } + + /// Verifies arity_bounds for a fixed-arity builtin with one optional param. + #[test] + fn arity_bounds_fixed_with_optional() { + // __registry_probe_opt: params [a: Int, b: Str = Null], variadic: None + // min = 1 (a is required), max = Some(2) + let (min, max) = arity_bounds("__registry_probe_opt").expect("probe registered"); + assert_eq!(min, 1, "one required param"); + assert_eq!(max, Some(2), "two params total, not variadic"); + } + + /// Verifies arity_bounds for a variadic builtin returns None as max. + #[test] + fn arity_bounds_variadic() { + // __registry_probe_variadic: fixed [fmt: Str], variadic: "__registry_values" + // min = 1 (fmt required), max = None + let (min, max) = arity_bounds("__registry_probe_variadic").expect("probe registered"); + assert_eq!(min, 1, "one required fixed param"); + assert_eq!(max, None, "variadic → unbounded max"); + } + + /// Verifies `function_sig` fields match the expected FunctionSig layout. + #[test] + fn function_sig_fields_match_layout() { + let sig = function_sig("__registry_probe_opt").expect("probe registered"); + assert_eq!(sig.params.len(), 2); + assert!(!sig.declared_return, "declared_return must be false for builtins"); + assert_eq!(sig.declared_params, vec![false, false]); + assert_eq!(sig.ref_params, vec![false, false]); + assert!(sig.variadic.is_none()); + assert!(sig.deprecation.is_none()); + assert_eq!(sig.return_type, PhpType::Bool); + } + + /// Verifies `first_class_callable_sig` applies the variadic-upgrade for variadic builtins. + #[test] + fn first_class_callable_sig_upgrades_variadic() { + let sig = first_class_callable_sig("__registry_probe_variadic") + .expect("probe registered"); + // After callable_wrapper_sig, the variadic param type becomes Array. + let variadic_name = sig.variadic.as_deref().expect("variadic name preserved"); + let var_param = sig.params.iter().find(|(n, _)| n == variadic_name); + let (_, var_ty) = var_param.expect("variadic param must be in params"); + assert_eq!(*var_ty, PhpType::Array(Box::new(PhpType::Mixed))); + } + + /// Verifies `function_sig` returns None for an unknown builtin. + #[test] + fn function_sig_returns_none_for_unknown() { + assert!(function_sig("__nonexistent_builtin_xyz").is_none()); + } + + /// Verifies `arity_bounds` returns None for an unknown builtin. + #[test] + fn arity_bounds_returns_none_for_unknown() { + assert!(arity_bounds("__nonexistent_builtin_xyz").is_none()); + } + + /// Verifies the `ref` param marker sets `ParamSpec.by_ref`, so the derived + /// `FunctionSig.ref_params` reports the first param as by-reference and the rest not. + #[test] + fn ref_marker_sets_ref_params() { + let sig = function_sig("__registry_probe_byref").expect("probe registered"); + assert_eq!(sig.ref_params, vec![true, false]); + // Param names/defaults unaffected by the marker. + assert_eq!(sig.params.len(), 2); + assert_eq!(sig.params[0].0, "target"); + assert_eq!(sig.params[1].0, "value"); + } +} diff --git a/src/builtins/spec.rs b/src/builtins/spec.rs new file mode 100644 index 0000000000..e053708187 --- /dev/null +++ b/src/builtins/spec.rs @@ -0,0 +1,267 @@ +//! Purpose: +//! Defines the `BuiltinSpec` type that describes a single PHP builtin function: +//! its name, arity, type signature, purity, and codegen lowering hook. +//! +//! Called from: +//! - `crate::builtins::registry` (collected via `inventory`). +//! - `crate::types::checker::builtins` and `crate::codegen::lower_inst::builtins` +//! (consumed during type-check and codegen dispatch). +//! +//! Key details: +//! - Every builtin must submit exactly one `BuiltinSpec` via the `builtin!` macro; +//! duplicate names are detected at registry init time. +//! - All `BuiltinSpec` fields are `'static` so the struct can be used in `const` context +//! and stored in the `inventory`-collected registry without allocation. + +// These new types reference pub(crate) types (Checker, FunctionContext) through their +// pub interfaces; that mismatch is intentional and will be resolved when the migration +// elevates or unifies those visibilities. Dead-code warnings are expected during the +// multi-task migration before the registry wires the types into active code paths. +#![allow(dead_code, private_interfaces)] + +/// Categorises a builtin by functional area, used for documentation grouping +/// and future area-scoped registry queries. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Area { + /// String manipulation builtins (`strlen`, `substr`, …). + String, + /// Array manipulation builtins (`count`, `array_map`, …). + Array, + /// Mathematical builtins (`abs`, `pow`, …). + Math, + /// I/O builtins (`echo`, `file_put_contents`, …). + Io, + /// System / process builtins (`exit`, `getenv`, …). + System, + /// Type-inspection and conversion builtins (is_int, gettype, settype, …). + Types, + /// Callable / closure builtins (`call_user_func`, …). + Callables, + /// SPL data-structure builtins. + Spl, + /// Pointer and buffer builtins (elephc extensions). + Pointers, + /// Internal compiler builtins not exposed as PHP-visible functions. + Internal, +} + +/// Describes the PHP-level type of a parameter or return value at the `BuiltinSpec` +/// level. Uses only `'static` storage so it can appear in `const` items. +/// +/// Add variants here only as the builtin migration surfaces the need; do not +/// pre-populate variants that are not yet referenced by any registered builtin. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum TypeSpec { + /// PHP `int`. + Int, + /// PHP `float`. + Float, + /// PHP `string`. + Str, + /// PHP `bool`. + Bool, + /// PHP `mixed`. + Mixed, + /// PHP `null`. + Null, + /// PHP `void` (return position only). + Void, + /// A homogeneous PHP array with element type `T` (`T[]`). + ArrayOf(&'static TypeSpec), + /// A PHP associative array with value type `T`. + AssocOf(&'static TypeSpec), + /// A union of two or more PHP types. + Union(&'static [TypeSpec]), +} + +/// Describes the default value for an optional parameter at the `BuiltinSpec` +/// level. Uses only `'static` and `Copy` types so it can appear in `const` items. +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum DefaultSpec { + /// PHP `null` default. + Null, + /// A literal integer default. + Int(i64), + /// A literal boolean default. + Bool(bool), + /// A literal float default. + Float(f64), + /// A literal string default. + Str(&'static str), + /// `PHP_INT_MAX` sentinel. + IntMax, + /// `PHP_INT_MIN` sentinel. + IntMin, + /// An empty array `[]` default. + EmptyArray, +} + +/// Describes a single named parameter of a PHP builtin function. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct ParamSpec { + /// The PHP-level parameter name (used for named-argument matching). + pub name: &'static str, + /// The PHP-level type of the parameter. + pub ty: TypeSpec, + /// The default value for optional parameters, or `None` for required parameters. + pub default: Option, + /// Whether the parameter is passed by reference (mutating builtins). + pub by_ref: bool, +} + +/// Context passed to a builtin's optional `check` hook during type-checking. +/// +/// Gives the hook access to the checker state, the call site name, the argument +/// list, the source span, and the current type environment so it can emit +/// diagnostics and return a refined return type. +pub struct BuiltinCheckCtx<'a> { + /// The active type checker (mutable so the hook can emit warnings and errors). + pub checker: &'a mut crate::types::checker::Checker, + /// The canonical lower-cased builtin name at the call site. + pub name: &'a str, + /// The unevaluated argument expressions passed to the builtin. + pub args: &'a [crate::parser::ast::Expr], + /// Source span of the call expression, for diagnostic messages. + pub span: crate::span::Span, + /// The type environment active at the call site. + pub env: &'a crate::types::TypeEnv, +} + +/// A type-checking hook for a builtin that needs logic beyond the static parameter list. +/// +/// The hook receives a mutable `BuiltinCheckCtx` and returns the refined return +/// `PhpType` for the call, or a `CompileError` if the call is ill-typed. +pub type CheckFn = for<'ctx, 'a> fn( + &'ctx mut BuiltinCheckCtx<'a>, +) -> Result; + +/// The assembly-lowering hook for a builtin, called by the EIR backend. +/// +/// Receives the active per-function backend context and the `BuiltinCall` instruction, +/// and emits the required assembly. Returns a `CodegenIrError` if the lowering path +/// is not yet implemented for this target. +pub type LowerFn = for<'ctx, 'f, 'i> fn( + &'ctx mut crate::codegen::context::FunctionContext<'f>, + &'i crate::ir::Instruction, +) -> Result<(), crate::codegen::CodegenIrError>; + +/// Complete static descriptor for one PHP builtin function. +/// +/// All fields are `'static` so the spec can be declared as a `const` item and +/// collected into the inventory-based registry at link time without heap allocation. +pub struct BuiltinSpec { + /// The canonical PHP function name (case-preserved, no leading backslash). + pub name: &'static str, + /// The functional area this builtin belongs to. + pub area: Area, + /// The declared parameter list, in PHP source order. + pub params: &'static [ParamSpec], + /// The PHP-level name of the variadic parameter, if any. + pub variadic: Option<&'static str>, + /// An optional override for the maximum argument count enforced by the + /// registry's `check_arity`. When `Some(n)`, `check_arity` rejects calls with + /// more than `n` arguments even though the declared parameter list (including + /// optional params) would otherwise permit more. This affects ONLY + /// `check_arity`; it does not change `function_sig`, `arity_bounds`, or the + /// parity gate, which all keep the full param-derived bounds. It exists to + /// preserve a migrated builtin whose legacy CHECK arm enforced a tighter arity + /// than its declared (golden) signature allowed. + pub max_args: Option, + /// An optional override for the minimum argument count enforced by the + /// registry's `check_arity`. When `Some(n)`, `check_arity` rejects calls + /// with fewer than `n` arguments even though the declared parameter list + /// would otherwise permit fewer (e.g. a variadic golden with min=0 but the + /// legacy CHECK arm required ≥2). This affects ONLY `check_arity`; it does + /// not change `function_sig`, `arity_bounds`, or the parity gate. + pub min_args: Option, + /// A verbatim error message used by `check_arity` instead of the standard + /// derived `"() takes …"` phrasing when an arity mismatch is detected. + /// When `None`, `check_arity` uses the standard derived message. + /// Affects ONLY `check_arity`; `function_sig`, `arity_bounds`, and the parity + /// gate are unaffected. + pub arity_error: Option<&'static str>, + + /// The PHP-level return type used by the type CHECKER when no `check` hook is + /// present (a `check` hook, when present, overrides this — see `check`). + /// + /// NOTE: this drives the *type checker* only. The EIR backend derives call + /// return types independently in `call_return_type` + /// (`src/ir_lower/expr/mod.rs`) from `sig.return_type` plus the + /// `*_builtin_return_type` override chain; it does NOT consult `returns` or + /// `check`. A builtin declared `returns: Mixed` with a precise `check` hook + /// (the standard pattern for non-scalar returns) therefore also needs a + /// matching EIR return-type arm, or the checker and EIR will disagree on the + /// value's type at the use site. + pub returns: TypeSpec, + /// Whether the function returns by reference. + pub by_ref_return: bool, + /// An optional type-checking hook for builtins whose return type depends + /// on the argument types or values. When present, its returned `PhpType` is + /// authoritative for the type CHECKER (it overrides `returns`). It is NOT + /// consulted by the EIR backend — see the note on `returns`. + pub check: Option, + /// When `true`, the registry's `check_builtin` dispatcher skips the standard + /// argument pre-inference loop before calling the `check` hook. The check hook + /// is then responsible for calling `infer_type` on each argument as needed. + /// + /// This is required for builtins like `usort`/`uasort` whose check hook uses + /// `infer_closure_type_with_param_hints` to supply object-element type hints to + /// an unannotated callback closure: the standard pre-inference loop would call + /// `infer_type` on the closure without hints first, causing the closure body to + /// be checked with default `Int` parameter types — making `$a->property` fail + /// before the hook can supply the correct hints. + pub lazy_check: bool, + /// The assembly-lowering hook called by the EIR backend for this builtin. + pub lower: LowerFn, + /// A short one-line summary for generated documentation. + pub summary: &'static str, + /// Example PHP snippets demonstrating the builtin, for generated documentation. + pub examples: &'static [&'static str], + /// The PHP manual URL fragment (e.g. `"function.strlen"`), if applicable. + pub php_manual: Option<&'static str>, + /// A deprecation message, or `None` if the builtin is not deprecated. + pub deprecation: Option<&'static str>, + /// When `true`, the builtin is not PHP-visible and is not emitted in catalogs + /// or documentation; it is only used internally by the compiler. + pub internal: bool, +} + +inventory::collect!(BuiltinSpec); + +#[cfg(test)] +mod macro_tests { + use crate::builtins::spec::*; + /// No-op `LowerFn` used to satisfy the `builtin!` macro in this test module. + fn lower(_c: &mut crate::codegen::context::FunctionContext, _i: &crate::ir::Instruction) + -> Result<(), crate::codegen::CodegenIrError> { Ok(()) } + builtin! { name: "__macro_probe", area: Internal, params: [x: Int], returns: Int, lower: lower, summary: "probe", internal: true } + + /// Verifies a builtin! declaration is collected by inventory. + #[test] + fn macro_registers_builtin() { + assert!(inventory::iter::.into_iter().any(|s| s.name == "__macro_probe")); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies a const BuiltinSpec can be built and read (const-friendly shape). + #[test] + fn const_spec_is_constructible() { + const P: &[ParamSpec] = &[ParamSpec { name: "string", ty: TypeSpec::Str, default: None, by_ref: false }]; + const S: BuiltinSpec = BuiltinSpec { + name: "strlen", area: Area::String, params: P, variadic: None, + max_args: None, min_args: None, arity_error: None, + returns: TypeSpec::Int, by_ref_return: false, check: None, lazy_check: false, + lower: noop_lower, summary: "len", examples: &[], php_manual: None, + deprecation: None, internal: false, + }; + assert_eq!(S.name, "strlen"); + assert_eq!(S.params.len(), 1); + } + /// No-op `LowerFn` used to satisfy the `BuiltinSpec` struct literal in this test module. + fn noop_lower(_c: &mut crate::codegen::context::FunctionContext, _i: &crate::ir::Instruction) + -> Result<(), crate::codegen::CodegenIrError> { Ok(()) } +} diff --git a/src/builtins/spl/iterator_apply.rs b/src/builtins/spl/iterator_apply.rs new file mode 100644 index 0000000000..baf0c6f27f --- /dev/null +++ b/src/builtins/spl/iterator_apply.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Home of the PHP `iterator_apply` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required to validate the Traversable source, resolve the callback +//! signature, and validate the optional args array. Returns `Int` (the iteration count). +//! - The `lazy_check: true` flag skips pre-inference so the hook can control inference +//! order when the callback signature drives argument type narrowing. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; +use crate::types::checker::builtins::spl as checker_spl; + +builtin! { + name: "iterator_apply", + area: Spl, + params: [iterator: Mixed, callback: Mixed, args: Mixed = DefaultSpec::Null], + returns: Int, + check: check, + lazy_check: true, + lower: lower, + summary: "Call a function for every element in an iterator.", + php_manual: "https://www.php.net/manual/en/function.iterator-apply.php", +} + +/// Validates the source, resolves callback arity from the args array, and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + checker_spl::check_iterator_apply_source( + cx.checker, + &cx.args[0], + cx.span, + cx.env, + )?; + match checker_spl::iterator_apply_callback_args( + cx.checker, + cx.args.get(2), + cx.span, + cx.env, + )? { + checker_spl::IteratorApplyArgs::Static(callback_args) => { + checker_spl::check_iterator_apply_static_callback( + cx.checker, + &cx.args[1], + callback_args, + cx.span, + cx.env, + )?; + } + checker_spl::IteratorApplyArgs::Dynamic { associative } => { + checker_spl::check_iterator_apply_dynamic_callback( + cx.checker, + &cx.args[1], + associative, + cx.span, + cx.env, + )?; + } + } + Ok(PhpType::Int) +} + +/// Lowers `iterator_apply()` by delegating to the iterator-apply emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_iterator_apply(ctx, inst) +} diff --git a/src/builtins/spl/iterator_count.rs b/src/builtins/spl/iterator_count.rs new file mode 100644 index 0000000000..7bab57fc62 --- /dev/null +++ b/src/builtins/spl/iterator_count.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Home of the PHP `iterator_count` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required to validate that the argument is a statically known +//! array or Traversable (not an arbitrary value); returns `Int`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; +use crate::types::checker::builtins::spl as checker_spl; + +builtin! { + name: "iterator_count", + area: Spl, + params: [iterator: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Count the elements in an iterator.", + php_manual: "https://www.php.net/manual/en/function.iterator-count.php", +} + +/// Validates the iterator source type and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + checker_spl::check_iterator_source( + cx.checker, + &cx.args[0], + cx.span, + cx.env, + "iterator_count()", + )?; + Ok(PhpType::Int) +} + +/// Lowers `iterator_count()` by delegating to the iterator-count emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_iterator_count(ctx, inst) +} diff --git a/src/builtins/spl/iterator_to_array.rs b/src/builtins/spl/iterator_to_array.rs new file mode 100644 index 0000000000..f8d3a1a799 --- /dev/null +++ b/src/builtins/spl/iterator_to_array.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Home of the PHP `iterator_to_array` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required because the return type depends on the source type and +//! the `preserve_keys` argument (static bool narrows to `AssocArray` or `Array`). +//! - The `returns: Mixed` macro field is a conservative fallback; the check hook always +//! returns the precise array type. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; +use crate::types::checker::builtins::spl as checker_spl; + +builtin! { + name: "iterator_to_array", + area: Spl, + params: [iterator: Mixed, preserve_keys: Bool = DefaultSpec::Bool(true)], + returns: Mixed, + check: check, + lower: lower, + summary: "Copy the iterator into an array.", + php_manual: "https://www.php.net/manual/en/function.iterator-to-array.php", +} + +/// Validates the source and computes the precise array return type based on `preserve_keys`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let source_ty = checker_spl::check_iterator_source( + cx.checker, + &cx.args[0], + cx.span, + cx.env, + "iterator_to_array()", + )?; + let preserve_keys = if let Some(arg) = cx.args.get(1) { + checker_spl::check_iterator_to_array_preserve_keys(cx.checker, arg, cx.env)? + } else { + Some(true) + }; + Ok(checker_spl::iterator_to_array_return_type( + cx.checker, + &source_ty, + preserve_keys, + )) +} + +/// Lowers `iterator_to_array()` by delegating to the iterator-to-array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_iterator_to_array(ctx, inst) +} diff --git a/src/builtins/spl/mod.rs b/src/builtins/spl/mod.rs new file mode 100644 index 0000000000..f8662406ca --- /dev/null +++ b/src/builtins/spl/mod.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Groups all `spl`-area builtin homes into this module so the registry can +//! collect them in one place. Each submodule declares exactly one builtin via +//! `builtin!` and provides its lowering hook. +//! +//! Called from: +//! - `crate::builtins` (`mod spl;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - Add `pub mod ;` here for every new SPL builtin home. +//! - Pure-data builtins (no `check` hook) rely on the registry common path to +//! infer each argument and enforce arity before falling back to the declared +//! `returns` type. +//! - Builtins with argument-type-dependent behaviour (`spl_object_id`, +//! `iterator_count`, etc.) supply a `check` hook that computes the return type +//! and validates the argument types. + +pub mod iterator_apply; +pub mod iterator_count; +pub mod iterator_to_array; +pub mod spl_autoload; +pub mod spl_autoload_call; +pub mod spl_autoload_extensions; +pub mod spl_autoload_functions; +pub mod spl_autoload_register; +pub mod spl_autoload_unregister; +pub mod spl_classes; +pub mod spl_object_hash; +pub mod spl_object_id; diff --git a/src/builtins/spl/spl_autoload.rs b/src/builtins/spl/spl_autoload.rs new file mode 100644 index 0000000000..04ece558ee --- /dev/null +++ b/src/builtins/spl/spl_autoload.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `spl_autoload` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts 1 required argument (`class`) and 1 optional argument (`file_extensions`). +//! - The AOT stub evaluates arguments for side effects and returns void. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "spl_autoload", + area: Spl, + params: [class: Mixed, file_extensions: Mixed = DefaultSpec::Null], + returns: Void, + lower: lower, + summary: "Default implementation for __autoload().", + php_manual: "https://www.php.net/manual/en/function.spl-autoload.php", +} + +/// Lowers `spl_autoload` by evaluating arguments for side effects and returning null. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_autoload_void( + ctx, + inst, + "spl_autoload", + ) +} diff --git a/src/builtins/spl/spl_autoload_call.rs b/src/builtins/spl/spl_autoload_call.rs new file mode 100644 index 0000000000..a4ffcbe151 --- /dev/null +++ b/src/builtins/spl/spl_autoload_call.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `spl_autoload_call` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - The AOT stub accepts exactly one class-name argument and returns void. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "spl_autoload_call", + area: Spl, + params: [class: Mixed], + returns: Void, + lower: lower, + summary: "Try all registered __autoload() functions to load the requested class.", + php_manual: "https://www.php.net/manual/en/function.spl-autoload-call.php", +} + +/// Lowers `spl_autoload_call` by evaluating the argument for side effects and returning null. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_autoload_void( + ctx, + inst, + "spl_autoload_call", + ) +} diff --git a/src/builtins/spl/spl_autoload_extensions.rs b/src/builtins/spl/spl_autoload_extensions.rs new file mode 100644 index 0000000000..5b53e3764e --- /dev/null +++ b/src/builtins/spl/spl_autoload_extensions.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `spl_autoload_extensions` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required to validate that the optional argument, when present, +//! is a string literal or null (the runtime only handles AOT-known extension strings). +//! - Returns the current extension string (`Str`) in all cases. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "spl_autoload_extensions", + area: Spl, + params: [file_extensions: Mixed = DefaultSpec::Null], + returns: Str, + check: check, + lower: lower, + summary: "Register and return default file extensions for spl_autoload.", + php_manual: "https://www.php.net/manual/en/function.spl-autoload-extensions.php", +} + +/// Validates the optional argument is a string literal or null; returns `Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(arg) = cx.args.first() { + cx.checker.infer_type(arg, cx.env)?; + if !matches!(arg.kind, ExprKind::StringLiteral(_) | ExprKind::Null) { + return Err(CompileError::new( + cx.span, + "spl_autoload_extensions() argument must be a string literal or null", + )); + } + } + Ok(PhpType::Str) +} + +/// Lowers `spl_autoload_extensions()` by delegating to the extension-globals emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_autoload_extensions(ctx, inst) +} diff --git a/src/builtins/spl/spl_autoload_functions.rs b/src/builtins/spl/spl_autoload_functions.rs new file mode 100644 index 0000000000..9421295997 --- /dev/null +++ b/src/builtins/spl/spl_autoload_functions.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `spl_autoload_functions` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required because the return type `Array` cannot be +//! expressed as a plain `TypeSpec` ident in the `builtin!` macro. +//! - The function takes no arguments; arity is enforced by the registry. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "spl_autoload_functions", + area: Spl, + params: [], + returns: Mixed, + check: check, + lower: lower, + summary: "Return all registered __autoload() functions.", + php_manual: "https://www.php.net/manual/en/function.spl-autoload-functions.php", +} + +/// Returns `Array` as the precise return type for `spl_autoload_functions()`. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Mixed))) +} + +/// Lowers `spl_autoload_functions()` by delegating to the AOT autoload-functions emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_autoload_functions(ctx, inst) +} diff --git a/src/builtins/spl/spl_autoload_register.rs b/src/builtins/spl/spl_autoload_register.rs new file mode 100644 index 0000000000..b61c27ecaa --- /dev/null +++ b/src/builtins/spl/spl_autoload_register.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Home of the PHP `spl_autoload_register` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - The autoload registration is an AOT stub: all three parameters are optional +//! and any combination of 0–3 arguments is accepted. Returns `true` always. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "spl_autoload_register", + area: Spl, + params: [ + callback: Mixed = DefaultSpec::Null, + throw: Bool = DefaultSpec::Bool(true), + prepend: Bool = DefaultSpec::Bool(false), + ], + returns: Bool, + lower: lower, + summary: "Register given function as __autoload() implementation.", + php_manual: "https://www.php.net/manual/en/function.spl-autoload-register.php", +} + +/// Lowers `spl_autoload_register` by evaluating arguments for side effects and returning true. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_autoload_bool( + ctx, + inst, + "spl_autoload_register", + ) +} diff --git a/src/builtins/spl/spl_autoload_unregister.rs b/src/builtins/spl/spl_autoload_unregister.rs new file mode 100644 index 0000000000..e0b96e9fc4 --- /dev/null +++ b/src/builtins/spl/spl_autoload_unregister.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `spl_autoload_unregister` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - The AOT stub accepts exactly one callable argument and returns `true`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "spl_autoload_unregister", + area: Spl, + params: [callback: Mixed], + returns: Bool, + lower: lower, + summary: "Unregister given function as __autoload() implementation.", + php_manual: "https://www.php.net/manual/en/function.spl-autoload-unregister.php", +} + +/// Lowers `spl_autoload_unregister` by evaluating the argument for side effects and returning true. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_autoload_bool( + ctx, + inst, + "spl_autoload_unregister", + ) +} diff --git a/src/builtins/spl/spl_classes.rs b/src/builtins/spl/spl_classes.rs new file mode 100644 index 0000000000..77b69e73b2 --- /dev/null +++ b/src/builtins/spl/spl_classes.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `spl_classes` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required because the return type `Array` cannot be +//! expressed as a plain `TypeSpec` ident in the `builtin!` macro. +//! - The function takes no arguments; arity is enforced by the registry. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "spl_classes", + area: Spl, + params: [], + returns: Mixed, + check: check, + lower: lower, + summary: "Return available SPL classes.", + php_manual: "https://www.php.net/manual/en/function.spl-classes.php", +} + +/// Returns `Array` as the precise return type for `spl_classes()`. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers `spl_classes()` by delegating to the static SPL class-name array emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_classes(ctx, inst) +} diff --git a/src/builtins/spl/spl_object_hash.rs b/src/builtins/spl/spl_object_hash.rs new file mode 100644 index 0000000000..1e6cb0e1e8 --- /dev/null +++ b/src/builtins/spl/spl_object_hash.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `spl_object_hash` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required to validate that the argument is an object; returns `Str`. +//! - The hash is derived from the object's heap pointer stringified via `__rt_itoa`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "spl_object_hash", + area: Spl, + params: [object: Mixed], + returns: Str, + check: check, + lower: lower, + summary: "Return hash id for given object.", + php_manual: "https://www.php.net/manual/en/function.spl-object-hash.php", +} + +/// Validates that the argument is an object and returns `Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Object(_)) { + return Err(CompileError::new( + cx.span, + "spl_object_hash() argument must be an object", + )); + } + Ok(PhpType::Str) +} + +/// Lowers `spl_object_hash()` by delegating to the object-pointer-to-string emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_object_hash(ctx, inst) +} diff --git a/src/builtins/spl/spl_object_id.rs b/src/builtins/spl/spl_object_id.rs new file mode 100644 index 0000000000..ce5b6e5e69 --- /dev/null +++ b/src/builtins/spl/spl_object_id.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `spl_object_id` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A `check` hook is required to validate that the argument is an object; returns `Int`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "spl_object_id", + area: Spl, + params: [object: Mixed], + returns: Int, + check: check, + lower: lower, + summary: "Return the integer object handle for given object.", + php_manual: "https://www.php.net/manual/en/function.spl-object-id.php", +} + +/// Validates that the argument is an object and returns `Int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Object(_)) { + return Err(CompileError::new( + cx.span, + "spl_object_id() argument must be an object", + )); + } + Ok(PhpType::Int) +} + +/// Lowers `spl_object_id()` by delegating to the object-pointer identity emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::spl::lower_spl_object_id(ctx, inst) +} diff --git a/src/builtins/string/addslashes.rs b/src/builtins/string/addslashes.rs new file mode 100644 index 0000000000..215f1c79d3 --- /dev/null +++ b/src/builtins/string/addslashes.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `addslashes` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `addslashes` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "addslashes", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Adds backslashes before characters that need to be escaped.", + php_manual: "https://www.php.net/manual/en/function.addslashes.php", +} + +/// Lowers an `addslashes` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "addslashes", + "__rt_addslashes", + ) +} diff --git a/src/builtins/string/base64_decode.rs b/src/builtins/string/base64_decode.rs new file mode 100644 index 0000000000..1594c55f7e --- /dev/null +++ b/src/builtins/string/base64_decode.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `base64_decode` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: the legacy CHECK arm declared a `Str` return type +//! (matching the migration golden), fully determined by this declaration. The +//! registry derives the return type from the `returns:` field without a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_base64_decode` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "base64_decode", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Decodes a Base64-encoded string back into its original data.", + php_manual: "https://www.php.net/manual/en/function.base64-decode.php", +} + +/// Lowers a `base64_decode` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "base64_decode", + "__rt_base64_decode", + ) +} diff --git a/src/builtins/string/base64_encode.rs b/src/builtins/string/base64_encode.rs new file mode 100644 index 0000000000..f1eca88f38 --- /dev/null +++ b/src/builtins/string/base64_encode.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `base64_encode` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `base64_encode` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_base64_encode` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "base64_encode", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Encodes binary data into a Base64 string.", + php_manual: "https://www.php.net/manual/en/function.base64-encode.php", +} + +/// Lowers a `base64_encode` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "base64_encode", + "__rt_base64_encode", + ) +} diff --git a/src/builtins/string/bin2hex.rs b/src/builtins/string/bin2hex.rs new file mode 100644 index 0000000000..1efe5f0f10 --- /dev/null +++ b/src/builtins/string/bin2hex.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `bin2hex` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `bin2hex` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_bin2hex` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "bin2hex", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Converts binary data into its hexadecimal string representation.", + php_manual: "https://www.php.net/manual/en/function.bin2hex.php", +} + +/// Lowers a `bin2hex` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "bin2hex", + "__rt_bin2hex", + ) +} diff --git a/src/builtins/string/chop.rs b/src/builtins/string/chop.rs new file mode 100644 index 0000000000..1d8f17c0d8 --- /dev/null +++ b/src/builtins/string/chop.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Home of the PHP `chop` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - `chop` is a PHP alias for `rtrim`. Both share the same signature, runtime +//! helpers, and parameter defaults. +//! - No `check` hook is needed: `chop` is a pure-data builtin. The registry's arity +//! check (1 required, 1 optional → 1 or 2 args) exactly matches the legacy check-arm +//! constraint, so no additional validation is needed. +//! - `lower` is a thin wrapper over `lower_trim_like` routing to the `__rt_rtrim` +//! family of runtime helpers. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "chop", + area: String, + params: [ + string: Str, + characters: Str = crate::builtins::spec::DefaultSpec::Str(" \n\r\t\u{000b}\u{000c}\0"), + ], + returns: Str, + lower: lower, + summary: "Alias of rtrim: strips whitespace (or other characters) from the end of a string.", + php_manual: "https://www.php.net/manual/en/function.chop.php", +} + +/// Lowers a `chop` call by dispatching to `lower_trim_like` with the rtrim runtime labels. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_trim_like( + ctx, + inst, + "chop", + "__rt_rtrim", + "__rt_rtrim_mask", + ) +} diff --git a/src/builtins/string/chr.rs b/src/builtins/string/chr.rs new file mode 100644 index 0000000000..57b2da6f88 --- /dev/null +++ b/src/builtins/string/chr.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Home of the PHP `chr` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `chr` is a pure-data builtin whose return type +//! (`Str`) is fully determined by its declaration. The registry derives the return +//! type from the `returns:` field without calling a check hook. +//! - The parameter is named `codepoint` (matching the parity golden) and typed `Int`, +//! reflecting PHP's `chr(int $codepoint): string`. The dedicated `lower_chr` emitter +//! coerces the operand to an integer via `load_as_int`, so the declared `Int` type +//! is consistent with the existing lowering. +//! - `lower` is a thin wrapper over the dedicated `lower_chr` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "chr", + area: String, + params: [codepoint: Int], + returns: Str, + lower: lower, + summary: "Returns a one-character string from the given byte code point.", + php_manual: "https://www.php.net/manual/en/function.chr.php", +} + +/// Lowers a `chr` call by dispatching to the dedicated per-arch `lower_chr` emitter. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_chr(ctx, inst) +} diff --git a/src/builtins/string/crc32.rs b/src/builtins/string/crc32.rs new file mode 100644 index 0000000000..6c0c3a7796 --- /dev/null +++ b/src/builtins/string/crc32.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `crc32` builtin: declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), both via +//! `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook needed: `returns: Int` expresses the return type inline and no +//! bridge library is required (crc32 is a pure table-free computation in __rt_crc32). +//! - Arity (exactly 1 arg) is validated by the registry. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "crc32", + area: String, + params: [string: Str], + returns: Int, + lower: lower, + summary: "Calculates the CRC32 polynomial of a string.", + php_manual: "https://www.php.net/manual/en/function.crc32.php", +} + +/// Lowers a `crc32` call by dispatching to the shared `lower_crc32` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_crc32(ctx, inst) +} diff --git a/src/builtins/string/ctype_alnum.rs b/src/builtins/string/ctype_alnum.rs new file mode 100644 index 0000000000..58db761d8c --- /dev/null +++ b/src/builtins/string/ctype_alnum.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `ctype_alnum` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `ctype_alnum` is a pure-data builtin whose return type +//! (`Bool`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_ctype_alnum` emitter in the +//! ctype lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ctype_alnum", + area: String, + params: [text: Str], + returns: Bool, + lower: lower, + summary: "Checks if all characters in the string are alphanumeric.", + php_manual: "https://www.php.net/manual/en/function.ctype-alnum.php", +} + +/// Lowers a `ctype_alnum` call by dispatching to the shared `lower_ctype_alnum` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::ctype::lower_ctype_alnum(ctx, inst) +} diff --git a/src/builtins/string/ctype_alpha.rs b/src/builtins/string/ctype_alpha.rs new file mode 100644 index 0000000000..70ac4c15d3 --- /dev/null +++ b/src/builtins/string/ctype_alpha.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `ctype_alpha` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `ctype_alpha` is a pure-data builtin whose return type +//! (`Bool`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_ctype_alpha` emitter in the +//! ctype lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ctype_alpha", + area: String, + params: [text: Str], + returns: Bool, + lower: lower, + summary: "Checks if all characters in the string are alphabetic.", + php_manual: "https://www.php.net/manual/en/function.ctype-alpha.php", +} + +/// Lowers a `ctype_alpha` call by dispatching to the shared `lower_ctype_alpha` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::ctype::lower_ctype_alpha(ctx, inst) +} diff --git a/src/builtins/string/ctype_digit.rs b/src/builtins/string/ctype_digit.rs new file mode 100644 index 0000000000..31ef1e88ca --- /dev/null +++ b/src/builtins/string/ctype_digit.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `ctype_digit` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `ctype_digit` is a pure-data builtin whose return type +//! (`Bool`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_ctype_digit` emitter in the +//! ctype lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ctype_digit", + area: String, + params: [text: Str], + returns: Bool, + lower: lower, + summary: "Checks if all characters in the string are digits.", + php_manual: "https://www.php.net/manual/en/function.ctype-digit.php", +} + +/// Lowers a `ctype_digit` call by dispatching to the shared `lower_ctype_digit` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::ctype::lower_ctype_digit(ctx, inst) +} diff --git a/src/builtins/string/ctype_space.rs b/src/builtins/string/ctype_space.rs new file mode 100644 index 0000000000..05288eea3c --- /dev/null +++ b/src/builtins/string/ctype_space.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `ctype_space` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `ctype_space` is a pure-data builtin whose return type +//! (`Bool`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_ctype_space` emitter in the +//! ctype lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ctype_space", + area: String, + params: [text: Str], + returns: Bool, + lower: lower, + summary: "Checks if all characters in the string are whitespace characters.", + php_manual: "https://www.php.net/manual/en/function.ctype-space.php", +} + +/// Lowers a `ctype_space` call by dispatching to the shared `lower_ctype_space` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::ctype::lower_ctype_space(ctx, inst) +} diff --git a/src/builtins/string/explode.rs b/src/builtins/string/explode.rs new file mode 100644 index 0000000000..7291a7046d --- /dev/null +++ b/src/builtins/string/explode.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `explode` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature carries the full golden param list (`separator`, `string`, +//! `limit`), but `max_args: 2` caps `check_arity` so a third argument is rejected, +//! matching the legacy CHECK arm which enforced exactly two arguments. +//! - `check` returns `PhpType::Array(Box::new(PhpType::Str))`. A check hook is required +//! because the `builtin!` macro `returns:` field cannot express an array type inline. +//! Argument types are inferred by the common registry dispatch path before the hook +//! fires. +//! - `lower` is a thin wrapper over the shared `lower_explode` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "explode", + area: String, + params: [separator: Str, string: Str, limit: Int = DefaultSpec::IntMax], + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Splits a string by a separator into an array of substrings.", + php_manual: "https://www.php.net/manual/en/function.explode.php", +} + +/// Returns `PhpType::Array(Box::new(PhpType::Str))` for an `explode` call. +/// +/// A check hook is required because the `builtin!` macro cannot express array return +/// types inline. Argument types are inferred by the common registry dispatch path before +/// this hook fires; arity (capped to 2 via `max_args`) is validated by the registry. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers an `explode` call by dispatching to the shared `lower_explode` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_explode(ctx, inst) +} diff --git a/src/builtins/string/grapheme_strrev.rs b/src/builtins/string/grapheme_strrev.rs new file mode 100644 index 0000000000..0e4c5ebc07 --- /dev/null +++ b/src/builtins/string/grapheme_strrev.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Home of the PHP `grapheme_strrev` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `PhpType::Union([Str, Bool])` (the reversed string, or `false`). +//! A check hook is required because the `builtin!` macro `returns:` field cannot +//! express a union inline; `returns: Mixed` is a placeholder overridden by the hook. +//! - The check hook also reproduces the legacy argument-type guard: a statically +//! non-string argument is rejected with `"grapheme_strrev() argument must be string"`. +//! The common registry dispatch path does not enforce parameter types, so the guard +//! must re-infer the argument type here. Arity (exactly 1, from the param list) is +//! pre-validated by the registry's `check_arity` before the hook fires, so the single +//! operand index is always present. +//! - `lower` is a thin wrapper over the dedicated `lower_grapheme_strrev` emitter, which +//! boxes the `string|false` runtime result as `Mixed`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "grapheme_strrev", + area: String, + params: [string: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Reverses a string by grapheme cluster, returning false on failure.", + php_manual: "https://www.php.net/manual/en/function.grapheme-strrev.php", +} + +/// Validates a `grapheme_strrev` call and returns `PhpType::Union([Str, Bool])`. +/// +/// Reproduces the legacy argument-type guard: a statically non-string argument +/// (anything other than `Str`, `Mixed`, or a `Union`) is rejected. Arity is +/// pre-validated by the registry, so `cx.args[0]` is always present. The argument +/// type is re-inferred here because the common registry dispatch path discards the +/// inferred types and does not enforce declared parameter types. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Str | PhpType::Mixed | PhpType::Union(_)) { + return Err(CompileError::new( + cx.span, + "grapheme_strrev() argument must be string", + )); + } + Ok(PhpType::Union(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `grapheme_strrev` call by dispatching to the dedicated emitter. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_grapheme_strrev(ctx, inst) +} diff --git a/src/builtins/string/gzcompress.rs b/src/builtins/string/gzcompress.rs new file mode 100644 index 0000000000..68368426e4 --- /dev/null +++ b/src/builtins/string/gzcompress.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `gzcompress` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the zlib bridge requirement via `require_builtin_library("z")` +//! so the linker pulls in the zlib compression implementation. +//! - Returns a raw string; unlike the decompress variants it never fails. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "gzcompress", + area: String, + params: [data: Str, level: Int = DefaultSpec::Int(-1)], + returns: Str, + check: check, + lower: lower, + summary: "Compress a string using the ZLIB data format.", + php_manual: "https://www.php.net/manual/en/function.gzcompress.php", +} + +/// Returns `PhpType::Str` for a `gzcompress` call and records the zlib bridge requirement. +/// +/// `require_builtin_library("z")` ensures the linker pulls in the zlib implementation. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (1–2 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("z"); + Ok(PhpType::Str) +} + +/// Lowers a `gzcompress` call by dispatching to the shared `lower_gzcompress` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_gzcompress(ctx, inst) +} diff --git a/src/builtins/string/gzdeflate.rs b/src/builtins/string/gzdeflate.rs new file mode 100644 index 0000000000..01610beeed --- /dev/null +++ b/src/builtins/string/gzdeflate.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `gzdeflate` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the zlib bridge requirement via `require_builtin_library("z")` +//! so the linker pulls in the raw-DEFLATE implementation. +//! - Returns a raw string; unlike the inflate variant it never fails with false. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "gzdeflate", + area: String, + params: [data: Str, level: Int = DefaultSpec::Int(-1)], + returns: Str, + check: check, + lower: lower, + summary: "Deflate a string using the DEFLATE data format.", + php_manual: "https://www.php.net/manual/en/function.gzdeflate.php", +} + +/// Returns `PhpType::Str` for a `gzdeflate` call and records the zlib bridge requirement. +/// +/// `require_builtin_library("z")` ensures the linker pulls in the zlib implementation. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (1–2 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("z"); + Ok(PhpType::Str) +} + +/// Lowers a `gzdeflate` call by dispatching to the shared `lower_gzdeflate` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_gzdeflate(ctx, inst) +} diff --git a/src/builtins/string/gzinflate.rs b/src/builtins/string/gzinflate.rs new file mode 100644 index 0000000000..a1a283a544 --- /dev/null +++ b/src/builtins/string/gzinflate.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `gzinflate` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the zlib bridge requirement via `require_builtin_library("z")` and +//! returns the `string|false` union (false on decompression failure). +//! - A check hook is required both for the library requirement and to express the +//! union return type that the `builtin!` macro cannot encode inline. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "gzinflate", + area: String, + params: [data: Str, max_length: Int = DefaultSpec::Int(0)], + returns: Mixed, + check: check, + lower: lower, + summary: "Inflate a deflated string.", + php_manual: "https://www.php.net/manual/en/function.gzinflate.php", +} + +/// Returns `PhpType::Union([Str, Bool])` for a `gzinflate` call and records the zlib bridge requirement. +/// +/// `require_builtin_library("z")` ensures the linker pulls in the zlib implementation. +/// The union return (string on success, false on decompression error) cannot be expressed +/// inline in the `builtin!` macro so a check hook is required. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (1–2 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("z"); + Ok(PhpType::Union(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `gzinflate` call by dispatching to the shared `lower_gzinflate` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_gzinflate(ctx, inst) +} diff --git a/src/builtins/string/gzuncompress.rs b/src/builtins/string/gzuncompress.rs new file mode 100644 index 0000000000..0993bf4d5e --- /dev/null +++ b/src/builtins/string/gzuncompress.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Home of the PHP `gzuncompress` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the zlib bridge requirement via `require_builtin_library("z")` and +//! returns the `string|false` union (false on decompression failure). +//! - A check hook is required both for the library requirement and to express the +//! union return type that the `builtin!` macro cannot encode inline. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "gzuncompress", + area: String, + params: [data: Str, max_length: Int = DefaultSpec::Int(0)], + returns: Mixed, + check: check, + lower: lower, + summary: "Uncompress a compressed string.", + php_manual: "https://www.php.net/manual/en/function.gzuncompress.php", +} + +/// Returns `PhpType::Union([Str, Bool])` for a `gzuncompress` call and records the zlib bridge requirement. +/// +/// `require_builtin_library("z")` ensures the linker pulls in the zlib implementation. +/// The union return (string on success, false on decompression error) cannot be expressed +/// inline in the `builtin!` macro so a check hook is required. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (1–2 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("z"); + Ok(PhpType::Union(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `gzuncompress` call by dispatching to the shared `lower_gzuncompress` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_gzuncompress(ctx, inst) +} diff --git a/src/builtins/string/hash.rs b/src/builtins/string/hash.rs new file mode 100644 index 0000000000..72b5901447 --- /dev/null +++ b/src/builtins/string/hash.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `hash` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the elephc-crypto bridge requirement via `require_builtin_library` +//! so the linker pulls in the full algorithm set (raw $binary output, catchable ValueError). +//! - Argument types are inferred by the common registry dispatch path before the hook fires. +//! - Arity (2–3 args) is validated by the registry's `check_arity` before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "hash", + area: String, + params: [algo: Str, data: Str, binary: Bool = DefaultSpec::Bool(false)], + returns: Str, + check: check, + lower: lower, + summary: "Generates a hash value using the given algorithm.", + php_manual: "https://www.php.net/manual/en/function.hash.php", +} + +/// Returns `PhpType::Str` for a `hash` call and records the elephc-crypto bridge requirement. +/// +/// `require_builtin_library` ensures the linker pulls in the full hash algorithm set. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (2–3 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Str) +} + +/// Lowers a `hash` call by dispatching to the shared `lower_hash` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_hash(ctx, inst) +} diff --git a/src/builtins/string/hash_algos.rs b/src/builtins/string/hash_algos.rs new file mode 100644 index 0000000000..cba724eafd --- /dev/null +++ b/src/builtins/string/hash_algos.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `hash_algos` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A check hook is required because `builtin!`'s `returns:` field cannot express an +//! array return type inline; the hook returns `PhpType::Array(Box::new(PhpType::Str))`. +//! - No bridge library is required (pure compile-time name list, no crypto). +//! - Arity (0 args) is validated by the registry. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "hash_algos", + area: String, + params: [], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns an array of supported hashing algorithm names.", + php_manual: "https://www.php.net/manual/en/function.hash-algos.php", +} + +/// Returns `PhpType::Array(Box::new(PhpType::Str))` for a `hash_algos` call. +/// +/// A check hook is required because the `builtin!` macro cannot express array return +/// types inline. No bridge library is required. Arity (0 args) is pre-validated by +/// the registry before this hook fires. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `hash_algos` call by dispatching to the shared `lower_hash_algos` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_hash_algos(ctx, inst) +} diff --git a/src/builtins/string/hash_copy.rs b/src/builtins/string/hash_copy.rs new file mode 100644 index 0000000000..046834f00c --- /dev/null +++ b/src/builtins/string/hash_copy.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Home of the PHP `hash_copy` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - A check hook is required to record the elephc-crypto bridge requirement AND because +//! the returned hash-context value is `PhpType::Mixed` (the same boxed runtime resource +//! shape produced by `hash_init`). +//! - Argument types are inferred by the common registry dispatch path before the hook fires. +//! - Arity (exactly 1 arg) is validated by the registry's `check_arity` before the hook fires. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "hash_copy", + area: String, + params: [context: Mixed], + returns: Mixed, + check: check, + lower: lower, + summary: "Copies the state of an incremental hashing context.", + php_manual: "https://www.php.net/manual/en/function.hash-copy.php", +} + +/// Returns `PhpType::Mixed` for a `hash_copy` call and records the elephc-crypto bridge requirement. +/// +/// `require_builtin_library` ensures the linker pulls in the hashing context copy implementation. +/// The return type is `PhpType::Mixed` because the copied context is the same boxed runtime +/// resource shape produced by `hash_init`. Arity (exactly 1 arg) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Mixed) +} + +/// Lowers a `hash_copy` call by dispatching to the shared `lower_hash_copy` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_hash_copy(ctx, inst) +} diff --git a/src/builtins/string/hash_equals.rs b/src/builtins/string/hash_equals.rs new file mode 100644 index 0000000000..9a12831785 --- /dev/null +++ b/src/builtins/string/hash_equals.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `hash_equals` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - No check hook is needed: `returns: Bool` expresses the return type inline and no +//! bridge library is required (this is a pure timing-safe byte comparison). +//! - Arity (exactly 2 args) is validated by the registry. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "hash_equals", + area: String, + params: [known_string: Str, user_string: Str], + returns: Bool, + lower: lower, + summary: "Compares two strings using a constant-time algorithm.", + php_manual: "https://www.php.net/manual/en/function.hash-equals.php", +} + +/// Lowers a `hash_equals` call by dispatching to the shared `lower_hash_equals` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_hash_equals(ctx, inst) +} diff --git a/src/builtins/string/hash_final.rs b/src/builtins/string/hash_final.rs new file mode 100644 index 0000000000..2a5a2a7598 --- /dev/null +++ b/src/builtins/string/hash_final.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `hash_final` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the elephc-crypto bridge requirement via `require_builtin_library` +//! so the linker pulls in the incremental hashing finalization implementation. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. +//! - Arity (1–2 args) is validated by the registry's `check_arity` before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "hash_final", + area: String, + params: [context: Mixed, binary: Bool = DefaultSpec::Bool(false)], + returns: Str, + check: check, + lower: lower, + summary: "Finalizes an incremental hash and returns the digest string.", + php_manual: "https://www.php.net/manual/en/function.hash-final.php", +} + +/// Returns `PhpType::Str` for a `hash_final` call and records the elephc-crypto bridge requirement. +/// +/// `require_builtin_library` ensures the linker pulls in the incremental hashing finalization +/// implementation. Argument types are inferred by the common registry dispatch path before +/// this hook fires; arity (1–2 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Str) +} + +/// Lowers a `hash_final` call by dispatching to the shared `lower_hash_final` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_hash_final(ctx, inst) +} diff --git a/src/builtins/string/hash_hmac.rs b/src/builtins/string/hash_hmac.rs new file mode 100644 index 0000000000..b6f8e583bd --- /dev/null +++ b/src/builtins/string/hash_hmac.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `hash_hmac` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the elephc-crypto bridge requirement via `require_builtin_library` +//! so the linker pulls in the HMAC implementation. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. +//! - Arity (3–4 args) is validated by the registry's `check_arity` before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "hash_hmac", + area: String, + params: [algo: Str, data: Str, key: Str, binary: Bool = DefaultSpec::Bool(false)], + returns: Str, + check: check, + lower: lower, + summary: "Generates a keyed hash value using the HMAC method.", + php_manual: "https://www.php.net/manual/en/function.hash-hmac.php", +} + +/// Returns `PhpType::Str` for a `hash_hmac` call and records the elephc-crypto bridge requirement. +/// +/// `require_builtin_library` ensures the linker pulls in the HMAC implementation. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (3–4 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Str) +} + +/// Lowers a `hash_hmac` call by dispatching to the shared `lower_hash_hmac` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_hash_hmac(ctx, inst) +} diff --git a/src/builtins/string/hash_init.rs b/src/builtins/string/hash_init.rs new file mode 100644 index 0000000000..32ac03ce1e --- /dev/null +++ b/src/builtins/string/hash_init.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Home of the PHP `hash_init` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `hash_init` accepts only 1 argument (the algorithm name). The `flags`/`key` +//! parameters from the PHP golden signature are not supported: HASH_HMAC streaming +//! mode requires passing a secret key and is blocked by `arity_error` and `max_args`. +//! - `min_args: 1, max_args: 1` enforces exactly 1 arg in `check_arity`. The custom +//! `arity_error` message explains the HMAC streaming restriction to the caller. +//! - `check` records the elephc-crypto bridge requirement via `require_builtin_library` +//! so the linker pulls in the hash algorithm set. +//! - Arity validation runs before the `check` hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "hash_init", + area: String, + params: [algo: Str, flags: Int = DefaultSpec::Int(0), key: Str = DefaultSpec::Str("")], + min_args: 1, + max_args: 1, + arity_error: "hash_init() flags/HASH_HMAC streaming mode is not supported; use hash_hmac() for HMAC", + returns: Mixed, + check: check, + lower: lower, + summary: "Initialize an incremental hashing context.", + php_manual: "https://www.php.net/manual/en/function.hash-init.php", +} + +/// Records the elephc-crypto bridge requirement and returns `PhpType::Mixed`. +/// +/// Arity (exactly 1 arg) is pre-validated by `check_arity`; the custom `arity_error` +/// message on the spec fires instead of the standard phrasing. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Mixed) +} + +/// Lowers a `hash_init` call by delegating to the shared hash-init emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_hash_init(ctx, inst) +} diff --git a/src/builtins/string/hash_update.rs b/src/builtins/string/hash_update.rs new file mode 100644 index 0000000000..cdb47ce0b2 --- /dev/null +++ b/src/builtins/string/hash_update.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `hash_update` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the elephc-crypto bridge requirement via `require_builtin_library` +//! so the linker pulls in the incremental hashing context implementation. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. +//! - Arity (exactly 2 args) is validated by the registry's `check_arity` before the hook fires. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "hash_update", + area: String, + params: [context: Mixed, data: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Pumps data into an active incremental hashing context.", + php_manual: "https://www.php.net/manual/en/function.hash-update.php", +} + +/// Returns `PhpType::Bool` for a `hash_update` call and records the elephc-crypto bridge requirement. +/// +/// `require_builtin_library` ensures the linker pulls in the incremental hashing context +/// implementation. Argument types are inferred by the common registry dispatch path before +/// this hook fires; arity (exactly 2 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Bool) +} + +/// Lowers a `hash_update` call by dispatching to the shared `lower_hash_update` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_hash_update(ctx, inst) +} diff --git a/src/builtins/string/hex2bin.rs b/src/builtins/string/hex2bin.rs new file mode 100644 index 0000000000..3aa1be9aa8 --- /dev/null +++ b/src/builtins/string/hex2bin.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `hex2bin` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `hex2bin` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_hex2bin` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "hex2bin", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Decodes a hexadecimal string back into its binary representation.", + php_manual: "https://www.php.net/manual/en/function.hex2bin.php", +} + +/// Lowers a `hex2bin` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "hex2bin", + "__rt_hex2bin", + ) +} diff --git a/src/builtins/string/html_entity_decode.rs b/src/builtins/string/html_entity_decode.rs new file mode 100644 index 0000000000..93b6fced99 --- /dev/null +++ b/src/builtins/string/html_entity_decode.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `html_entity_decode` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `html_entity_decode` is a pure-data builtin whose +//! return type (`Str`) is fully determined by its declaration. The registry derives +//! the return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_html_entity_decode` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "html_entity_decode", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Converts HTML entities in a string back into their corresponding characters.", + php_manual: "https://www.php.net/manual/en/function.html-entity-decode.php", +} + +/// Lowers a `html_entity_decode` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "html_entity_decode", + "__rt_html_entity_decode", + ) +} diff --git a/src/builtins/string/htmlentities.rs b/src/builtins/string/htmlentities.rs new file mode 100644 index 0000000000..6a0835ca9d --- /dev/null +++ b/src/builtins/string/htmlentities.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `htmlentities` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `htmlentities` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter. +//! Like the legacy arm, it reuses the `__rt_htmlspecialchars` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "htmlentities", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Converts all applicable characters in a string into their HTML entities.", + php_manual: "https://www.php.net/manual/en/function.htmlentities.php", +} + +/// Lowers a `htmlentities` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "htmlentities", + "__rt_htmlspecialchars", + ) +} diff --git a/src/builtins/string/htmlspecialchars.rs b/src/builtins/string/htmlspecialchars.rs new file mode 100644 index 0000000000..db2b60cc59 --- /dev/null +++ b/src/builtins/string/htmlspecialchars.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `htmlspecialchars` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `htmlspecialchars` is a pure-data builtin whose +//! return type (`Str`) is fully determined by its declaration. The registry derives +//! the return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_htmlspecialchars` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "htmlspecialchars", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Converts the HTML special characters in a string into their entities.", + php_manual: "https://www.php.net/manual/en/function.htmlspecialchars.php", +} + +/// Lowers a `htmlspecialchars` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "htmlspecialchars", + "__rt_htmlspecialchars", + ) +} diff --git a/src/builtins/string/implode.rs b/src/builtins/string/implode.rs new file mode 100644 index 0000000000..94339b2f66 --- /dev/null +++ b/src/builtins/string/implode.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Home of the PHP `implode` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `implode` is the one migrated builtin whose legacy CHECK arm (exactly 2 arguments) +//! was STRICTER than its golden signature's minimum. The golden marks `array` optional +//! (required count 1), which the parity gate compares against, so `array` must keep a +//! default here. `max_args` caps only the maximum, so it cannot raise the minimum; +//! the exact-2 requirement is therefore re-enforced inside the `check` hook to keep the +//! legacy `"implode() takes exactly 2 arguments"` diagnostic for the tested 1-arg call. +//! - `check` returns `PhpType::Str`. +//! - `lower` is a thin wrapper over the shared `lower_implode` emitter, which itself +//! requires exactly two operands. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "implode", + area: String, + params: [separator: Str, array: Mixed = DefaultSpec::Null], + max_args: 2, + returns: Str, + check: check, + lower: lower, + summary: "Joins array elements into a single string using a separator.", + php_manual: "https://www.php.net/manual/en/function.implode.php", +} + +/// Returns `PhpType::Str` for an `implode` call, enforcing the legacy exactly-2 arity. +/// +/// The golden signature marks `array` optional (so the parity gate sees one required +/// param), but the legacy CHECK arm required exactly two arguments. `check_arity`'s +/// `max_args` override caps the maximum only and cannot raise the minimum, so the +/// exact-2 requirement is re-enforced here to preserve the legacy diagnostic. Argument +/// types are inferred by the common registry dispatch path before this hook fires. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if cx.args.len() != 2 { + return Err(CompileError::new( + cx.span, + "implode() takes exactly 2 arguments", + )); + } + Ok(PhpType::Str) +} + +/// Lowers an `implode` call by dispatching to the shared `lower_implode` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_implode(ctx, inst) +} diff --git a/src/builtins/string/inet_ntop.rs b/src/builtins/string/inet_ntop.rs new file mode 100644 index 0000000000..46b8c107d5 --- /dev/null +++ b/src/builtins/string/inet_ntop.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Home of the PHP `inet_ntop` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns the `string|false` union: `inet_ntop` returns `false` for invalid +//! packed IP addresses. A check hook is required because the `builtin!` macro cannot +//! express a union return type inline. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "inet_ntop", + area: String, + params: [ip: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Converts a packed internet address to a human-readable representation.", + php_manual: "https://www.php.net/manual/en/function.inet-ntop.php", +} + +/// Returns `PhpType::Union([Str, Bool])` for an `inet_ntop` call. +/// +/// The union return (string on success, false on invalid input) cannot be expressed +/// inline in the `builtin!` macro so a check hook is required. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (exactly 1 arg) is pre-validated by the registry. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers an `inet_ntop` call by dispatching to the shared `lower_inet` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_inet( + ctx, + inst, + "inet_ntop", + "__rt_inet_ntop", + ) +} diff --git a/src/builtins/string/inet_pton.rs b/src/builtins/string/inet_pton.rs new file mode 100644 index 0000000000..1d28ffb8ff --- /dev/null +++ b/src/builtins/string/inet_pton.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Home of the PHP `inet_pton` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns the `string|false` union: `inet_pton` returns `false` for invalid +//! IP address strings. A check hook is required because the `builtin!` macro cannot +//! express a union return type inline. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "inet_pton", + area: String, + params: [ip: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Converts a human-readable IP address to its packed in_addr representation.", + php_manual: "https://www.php.net/manual/en/function.inet-pton.php", +} + +/// Returns `PhpType::Union([Str, Bool])` for an `inet_pton` call. +/// +/// The union return (string on success, false on invalid input) cannot be expressed +/// inline in the `builtin!` macro so a check hook is required. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (exactly 1 arg) is pre-validated by the registry. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers an `inet_pton` call by dispatching to the shared `lower_inet` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_inet( + ctx, + inst, + "inet_pton", + "__rt_inet_pton", + ) +} diff --git a/src/builtins/string/ip2long.rs b/src/builtins/string/ip2long.rs new file mode 100644 index 0000000000..4964447a8a --- /dev/null +++ b/src/builtins/string/ip2long.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `ip2long` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns the `int|false` union: `ip2long` returns `false` for invalid +//! IPv4 address strings. A check hook is required because the `builtin!` macro cannot +//! express a union return type inline. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "ip2long", + area: String, + params: [ip: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Converts a string containing an IPv4 address into a long integer.", + php_manual: "https://www.php.net/manual/en/function.ip2long.php", +} + +/// Returns `PhpType::Union([Int, Bool])` for an `ip2long` call. +/// +/// The union return (integer on success, false on invalid input) cannot be expressed +/// inline in the `builtin!` macro so a check hook is required. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (exactly 1 arg) is pre-validated by the registry. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers an `ip2long` call by dispatching to the shared `lower_ip2long` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_ip2long(ctx, inst) +} diff --git a/src/builtins/string/lcfirst.rs b/src/builtins/string/lcfirst.rs new file mode 100644 index 0000000000..896d73b357 --- /dev/null +++ b/src/builtins/string/lcfirst.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `lcfirst` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `lcfirst` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_lcfirst` emitter in the +//! strings lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "lcfirst", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Lowercases the first character of a string.", + php_manual: "https://www.php.net/manual/en/function.lcfirst.php", +} + +/// Lowers a `lcfirst` call by dispatching to the dedicated per-arch emitter. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_lcfirst(ctx, inst) +} diff --git a/src/builtins/string/long2ip.rs b/src/builtins/string/long2ip.rs new file mode 100644 index 0000000000..74ceb8f011 --- /dev/null +++ b/src/builtins/string/long2ip.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `long2ip` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `long2ip` is a pure-data builtin whose return type +//! (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_long2ip` emitter in the +//! strings lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "long2ip", + area: String, + params: [ip: Int], + returns: Str, + lower: lower, + summary: "Converts an IPv4 address from long integer to dotted string notation.", + php_manual: "https://www.php.net/manual/en/function.long2ip.php", +} + +/// Lowers a `long2ip` call by dispatching to the shared `lower_long2ip` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_long2ip(ctx, inst) +} diff --git a/src/builtins/string/ltrim.rs b/src/builtins/string/ltrim.rs new file mode 100644 index 0000000000..8fb03825ee --- /dev/null +++ b/src/builtins/string/ltrim.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `ltrim` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `ltrim` is a pure-data builtin. The registry's arity +//! check (1 required, 1 optional → 1 or 2 args) exactly matches the legacy check-arm +//! constraint, so no additional validation is needed. +//! - `lower` is a thin wrapper over `lower_trim_like` which dispatches to the appropriate +//! runtime helper depending on whether a mask argument is provided. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ltrim", + area: String, + params: [ + string: Str, + characters: Str = crate::builtins::spec::DefaultSpec::Str(" \n\r\t\u{000b}\u{000c}\0"), + ], + returns: Str, + lower: lower, + summary: "Strips whitespace (or other characters) from the beginning of a string.", + php_manual: "https://www.php.net/manual/en/function.ltrim.php", +} + +/// Lowers an `ltrim` call by dispatching to `lower_trim_like` with the default and mask runtime labels. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_trim_like( + ctx, + inst, + "ltrim", + "__rt_ltrim", + "__rt_ltrim_mask", + ) +} diff --git a/src/builtins/string/md5.rs b/src/builtins/string/md5.rs new file mode 100644 index 0000000000..16d7560b56 --- /dev/null +++ b/src/builtins/string/md5.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `md5` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the elephc-crypto bridge requirement via `require_builtin_library` +//! so the linker pulls in the MD5 implementation. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. +//! - Arity (1–2 args) is validated by the registry's `check_arity` before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "md5", + area: String, + params: [string: Str, binary: Bool = DefaultSpec::Bool(false)], + returns: Str, + check: check, + lower: lower, + summary: "Calculates the MD5 hash of a string.", + php_manual: "https://www.php.net/manual/en/function.md5.php", +} + +/// Returns `PhpType::Str` for an `md5` call and records the elephc-crypto bridge requirement. +/// +/// `require_builtin_library` ensures the linker pulls in the MD5 implementation. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (1–2 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Str) +} + +/// Lowers an `md5` call by dispatching to the shared `lower_md5` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_md5(ctx, inst) +} diff --git a/src/builtins/string/mod.rs b/src/builtins/string/mod.rs new file mode 100644 index 0000000000..662a56d43a --- /dev/null +++ b/src/builtins/string/mod.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Groups all `string`-area builtin homes into this module so the registry can +//! collect them in one place. Each submodule declares exactly one builtin via +//! `builtin!` and provides its type-check and lowering hooks. +//! +//! Called from: +//! - `crate::builtins` (`mod string;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - Add `pub mod ;` here for every new string builtin home. +//! - Pure-data builtins (no check hook) only need a `lower` fn; the `builtin!` +//! `returns:` field provides the declared return type. + +pub mod addslashes; +pub mod base64_decode; +pub mod base64_encode; +pub mod bin2hex; +pub mod chop; +pub mod chr; +pub mod crc32; +pub mod ctype_alnum; +pub mod ctype_alpha; +pub mod ctype_digit; +pub mod ctype_space; +pub mod explode; +pub mod grapheme_strrev; +pub mod gzcompress; +pub mod gzdeflate; +pub mod gzinflate; +pub mod gzuncompress; +pub mod hash; +pub mod hash_algos; +pub mod hash_copy; +pub mod hash_equals; +pub mod hash_final; +pub mod hash_hmac; +pub mod hash_init; +pub mod hash_update; +pub mod hex2bin; +pub mod html_entity_decode; +pub mod htmlentities; +pub mod htmlspecialchars; +pub mod implode; +pub mod inet_ntop; +pub mod inet_pton; +pub mod ip2long; +pub mod lcfirst; +pub mod long2ip; +pub mod ltrim; +pub mod md5; +pub mod nl2br; +pub mod number_format; +pub mod ord; +pub mod printf; +pub mod rawurldecode; +pub mod rawurlencode; +pub mod rtrim; +pub mod sha1; +pub mod sprintf; +pub mod sscanf; +pub mod str_contains; +pub mod str_ends_with; +pub mod str_ireplace; +pub mod str_pad; +pub mod str_repeat; +pub mod str_replace; +pub mod str_split; +pub mod str_starts_with; +pub mod strcasecmp; +pub mod strcmp; +pub mod stripslashes; +pub mod strlen; +pub mod strpos; +pub mod strrev; +pub mod strrpos; +pub mod strstr; +pub mod strtolower; +pub mod strtoupper; +pub mod substr; +pub mod substr_replace; +pub mod trim; +pub mod ucfirst; +pub mod ucwords; +pub mod urldecode; +pub mod urlencode; +pub mod vprintf; +pub mod vsprintf; +pub mod wordwrap; diff --git a/src/builtins/string/nl2br.rs b/src/builtins/string/nl2br.rs new file mode 100644 index 0000000000..ef462ddd76 --- /dev/null +++ b/src/builtins/string/nl2br.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `nl2br` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `nl2br` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "nl2br", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Inserts HTML line breaks before newlines in a string.", + php_manual: "https://www.php.net/manual/en/function.nl2br.php", +} + +/// Lowers a `nl2br` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "nl2br", + "__rt_nl2br", + ) +} diff --git a/src/builtins/string/number_format.rs b/src/builtins/string/number_format.rs new file mode 100644 index 0000000000..d007193ffe --- /dev/null +++ b/src/builtins/string/number_format.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Home of the PHP `number_format` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts a required `num` float and optional `decimals`, `decimal_separator`, +//! and `thousands_separator` params with PHP-compatible defaults. +//! - `lower` is a thin wrapper over the shared `lower_number_format` emitter. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "number_format", + area: String, + params: [ + num: Float, + decimals: Int = DefaultSpec::Int(0), + decimal_separator: Str = DefaultSpec::Str("."), + thousands_separator: Str = DefaultSpec::Str(",") + ], + returns: Str, + lower: lower, + summary: "Formats a number with grouped thousands.", + php_manual: "https://www.php.net/manual/en/function.number-format.php", +} + +/// Lowers a `number_format` call by dispatching to the shared number-format emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_number_format(ctx, inst) +} diff --git a/src/builtins/string/ord.rs b/src/builtins/string/ord.rs new file mode 100644 index 0000000000..cf8b3c6f4b --- /dev/null +++ b/src/builtins/string/ord.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `ord` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `ord` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_ord` emitter in the +//! strings lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ord", + area: String, + params: [character: Str], + returns: Int, + lower: lower, + summary: "Returns the ASCII value of the first character of a string.", + php_manual: "https://www.php.net/manual/en/function.ord.php", +} + +/// Lowers an `ord` call by dispatching to the shared per-arch emitter. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_ord(ctx, inst) +} diff --git a/src/builtins/string/printf.rs b/src/builtins/string/printf.rs new file mode 100644 index 0000000000..96d0fce6b3 --- /dev/null +++ b/src/builtins/string/printf.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `printf` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts a required `format` string plus a variadic `values` list. +//! - `lower` is a thin wrapper over the shared `lower_printf` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "printf", + area: String, + params: [format: Str], + variadic: "values", + returns: Int, + lower: lower, + summary: "Outputs a formatted string.", + php_manual: "https://www.php.net/manual/en/function.printf.php", +} + +/// Lowers a `printf` call by dispatching to the shared printf emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_printf(ctx, inst) +} diff --git a/src/builtins/string/rawurldecode.rs b/src/builtins/string/rawurldecode.rs new file mode 100644 index 0000000000..a3c2adc389 --- /dev/null +++ b/src/builtins/string/rawurldecode.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `rawurldecode` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `rawurldecode` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter. +//! Like the legacy arm, it reuses the `__rt_urldecode` runtime helper but without +//! treating '+' as a space (RFC 3986 raw decoding is handled inside the helper path). + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "rawurldecode", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Decodes an RFC 3986 percent-encoded string without treating '+' as a space.", + php_manual: "https://www.php.net/manual/en/function.rawurldecode.php", +} + +/// Lowers a `rawurldecode` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "rawurldecode", + "__rt_urldecode", + ) +} diff --git a/src/builtins/string/rawurlencode.rs b/src/builtins/string/rawurlencode.rs new file mode 100644 index 0000000000..02bcd7125d --- /dev/null +++ b/src/builtins/string/rawurlencode.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `rawurlencode` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `rawurlencode` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_rawurlencode` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "rawurlencode", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces).", + php_manual: "https://www.php.net/manual/en/function.rawurlencode.php", +} + +/// Lowers a `rawurlencode` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "rawurlencode", + "__rt_rawurlencode", + ) +} diff --git a/src/builtins/string/rtrim.rs b/src/builtins/string/rtrim.rs new file mode 100644 index 0000000000..c9dd8b93fd --- /dev/null +++ b/src/builtins/string/rtrim.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `rtrim` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `rtrim` is a pure-data builtin. The registry's arity +//! check (1 required, 1 optional → 1 or 2 args) exactly matches the legacy check-arm +//! constraint, so no additional validation is needed. +//! - `lower` is a thin wrapper over `lower_trim_like` which dispatches to the appropriate +//! runtime helper depending on whether a mask argument is provided. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "rtrim", + area: String, + params: [ + string: Str, + characters: Str = crate::builtins::spec::DefaultSpec::Str(" \n\r\t\u{000b}\u{000c}\0"), + ], + returns: Str, + lower: lower, + summary: "Strips whitespace (or other characters) from the end of a string.", + php_manual: "https://www.php.net/manual/en/function.rtrim.php", +} + +/// Lowers an `rtrim` call by dispatching to `lower_trim_like` with the default and mask runtime labels. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_trim_like( + ctx, + inst, + "rtrim", + "__rt_rtrim", + "__rt_rtrim_mask", + ) +} diff --git a/src/builtins/string/sha1.rs b/src/builtins/string/sha1.rs new file mode 100644 index 0000000000..e0219e2f70 --- /dev/null +++ b/src/builtins/string/sha1.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Home of the PHP `sha1` builtin: declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` records the elephc-crypto bridge requirement via `require_builtin_library` +//! so the linker pulls in the SHA-1 implementation. +//! - Argument types are inferred by the common registry dispatch path before the hook fires. +//! - Arity (1–2 args) is validated by the registry's `check_arity` before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "sha1", + area: String, + params: [string: Str, binary: Bool = DefaultSpec::Bool(false)], + returns: Str, + check: check, + lower: lower, + summary: "Calculates the SHA-1 hash of a string.", + php_manual: "https://www.php.net/manual/en/function.sha1.php", +} + +/// Returns `PhpType::Str` for a `sha1` call and records the elephc-crypto bridge requirement. +/// +/// `require_builtin_library` ensures the linker pulls in the SHA-1 implementation. +/// Argument types are inferred by the common registry dispatch path before this hook fires; +/// arity (1–2 args) is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.require_builtin_library("elephc_crypto"); + Ok(PhpType::Str) +} + +/// Lowers a `sha1` call by dispatching to the shared `lower_sha1` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_sha1(ctx, inst) +} diff --git a/src/builtins/string/sprintf.rs b/src/builtins/string/sprintf.rs new file mode 100644 index 0000000000..c0eb7b4b9a --- /dev/null +++ b/src/builtins/string/sprintf.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `sprintf` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts a required `format` string plus a variadic `values` list. +//! - `lower` is a thin wrapper over the shared `lower_sprintf` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "sprintf", + area: String, + params: [format: Str], + variadic: "values", + returns: Str, + lower: lower, + summary: "Returns a formatted string.", + php_manual: "https://www.php.net/manual/en/function.sprintf.php", +} + +/// Lowers a `sprintf` call by dispatching to the shared sprintf emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_sprintf(ctx, inst) +} diff --git a/src/builtins/string/sscanf.rs b/src/builtins/string/sscanf.rs new file mode 100644 index 0000000000..2f39aae9bd --- /dev/null +++ b/src/builtins/string/sscanf.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `sscanf` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts required `string` and `format` params plus a variadic `vars` list. +//! - `check` returns `PhpType::Array(Box::new(PhpType::Str))` because the macro +//! `returns:` field cannot express a parameterized array type inline. +//! - `lower` is a thin wrapper over the shared `lower_sscanf` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "sscanf", + area: String, + params: [string: Str, format: Str], + variadic: "vars", + returns: Mixed, + check: check, + lower: lower, + summary: "Parses a string according to a format.", + php_manual: "https://www.php.net/manual/en/function.sscanf.php", +} + +/// Returns `PhpType::Array(Box::new(PhpType::Str))` for a `sscanf` call. +/// +/// A check hook is required because the `builtin!` macro cannot express a +/// parameterized array return type inline. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `sscanf` call by dispatching to the shared sscanf emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_sscanf(ctx, inst) +} diff --git a/src/builtins/string/str_contains.rs b/src/builtins/string/str_contains.rs new file mode 100644 index 0000000000..97ef0c3434 --- /dev/null +++ b/src/builtins/string/str_contains.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `str_contains` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `str_contains` is a pure-data builtin whose return +//! type (`Bool`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over `lower_str_contains` which uses `__rt_strpos` +//! and normalizes its signed result to a PHP boolean. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "str_contains", + area: String, + params: [haystack: Str, needle: Str], + returns: Bool, + lower: lower, + summary: "Determines if a string contains a given substring.", + php_manual: "https://www.php.net/manual/en/function.str-contains.php", +} + +/// Lowers a `str_contains` call by dispatching to the dedicated strpos-based emitter. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_str_contains(ctx, inst) +} diff --git a/src/builtins/string/str_ends_with.rs b/src/builtins/string/str_ends_with.rs new file mode 100644 index 0000000000..8f0c897b32 --- /dev/null +++ b/src/builtins/string/str_ends_with.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `str_ends_with` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `str_ends_with` is a pure-data builtin whose +//! return type (`Bool`) is fully determined by its declaration. The registry +//! derives the return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over `lower_binary_string_runtime` which dispatches +//! to the shared `__rt_str_ends_with` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "str_ends_with", + area: String, + params: [haystack: Str, needle: Str], + returns: Bool, + lower: lower, + summary: "Checks if a string ends with a given substring.", + php_manual: "https://www.php.net/manual/en/function.str-ends-with.php", +} + +/// Lowers a `str_ends_with` call by dispatching to the shared binary-string runtime helper. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_binary_string_runtime( + ctx, + inst, + "str_ends_with", + "__rt_str_ends_with", + ) +} diff --git a/src/builtins/string/str_ireplace.rs b/src/builtins/string/str_ireplace.rs new file mode 100644 index 0000000000..c03dd3b649 --- /dev/null +++ b/src/builtins/string/str_ireplace.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Home of the PHP `str_ireplace` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature includes an optional `count` param, but `max_args: 3` +//! caps arity so only three arguments are accepted, matching PHP's practical use. +//! - `lower` is a thin wrapper over the shared `lower_string_replace` emitter. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "str_ireplace", + area: String, + params: [search: Str, replace: Str, subject: Str, count: Mixed = DefaultSpec::Null], + max_args: 3, + returns: Str, + lower: lower, + summary: "Case-insensitive version of str_replace().", + php_manual: "https://www.php.net/manual/en/function.str-ireplace.php", +} + +/// Lowers a `str_ireplace` call by dispatching to the shared string-replace emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_string_replace( + ctx, + inst, + "str_ireplace", + "__rt_str_ireplace", + ) +} diff --git a/src/builtins/string/str_pad.rs b/src/builtins/string/str_pad.rs new file mode 100644 index 0000000000..8fc20d0830 --- /dev/null +++ b/src/builtins/string/str_pad.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Home of the PHP `str_pad` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts required `string` and `length` params, plus optional `pad_string` +//! and `pad_type` params with PHP-compatible defaults. +//! - `lower` is a thin wrapper over the shared `lower_str_pad` emitter. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "str_pad", + area: String, + params: [ + string: Str, + length: Int, + pad_string: Str = DefaultSpec::Str(" "), + pad_type: Int = DefaultSpec::Int(1) + ], + returns: Str, + lower: lower, + summary: "Pads a string to a certain length with another string.", + php_manual: "https://www.php.net/manual/en/function.str-pad.php", +} + +/// Lowers a `str_pad` call by dispatching to the shared str-pad emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_str_pad(ctx, inst) +} diff --git a/src/builtins/string/str_repeat.rs b/src/builtins/string/str_repeat.rs new file mode 100644 index 0000000000..c0db786813 --- /dev/null +++ b/src/builtins/string/str_repeat.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `str_repeat` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `str_repeat` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_str_repeat` emitter in the +//! strings lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "str_repeat", + area: String, + params: [string: Str, times: Int], + returns: Str, + lower: lower, + summary: "Repeats a string a given number of times.", + php_manual: "https://www.php.net/manual/en/function.str-repeat.php", +} + +/// Lowers a `str_repeat` call by dispatching to the dedicated per-arch emitter. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_str_repeat(ctx, inst) +} diff --git a/src/builtins/string/str_replace.rs b/src/builtins/string/str_replace.rs new file mode 100644 index 0000000000..ee753cbdd7 --- /dev/null +++ b/src/builtins/string/str_replace.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Home of the PHP `str_replace` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature includes an optional `count` param, but `max_args: 3` +//! caps arity so only three arguments are accepted, matching PHP's practical use. +//! - `lower` is a thin wrapper over the shared `lower_string_replace` emitter. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "str_replace", + area: String, + params: [search: Str, replace: Str, subject: Str, count: Mixed = DefaultSpec::Null], + max_args: 3, + returns: Str, + lower: lower, + summary: "Replaces all occurrences of a search string with a replacement string.", + php_manual: "https://www.php.net/manual/en/function.str-replace.php", +} + +/// Lowers a `str_replace` call by dispatching to the shared string-replace emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_string_replace( + ctx, + inst, + "str_replace", + "__rt_str_replace", + ) +} diff --git a/src/builtins/string/str_split.rs b/src/builtins/string/str_split.rs new file mode 100644 index 0000000000..c9c8937c57 --- /dev/null +++ b/src/builtins/string/str_split.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Home of the PHP `str_split` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `PhpType::Array(Box::new(PhpType::Str))`. A check hook is +//! required because the `builtin!` macro `returns:` field only accepts a simple +//! type identifier and cannot express `ArrayOf(Str)` inline. Argument types are +//! inferred by the common registry dispatch path before the hook fires. +//! - Arity is validated by the registry's `check_arity` before the check hook fires; +//! the inline arity check from the legacy arm is therefore not reproduced here. +//! - `lower` is a thin wrapper over the shared `lower_str_split` emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "str_split", + area: String, + params: [ + string: Str, + length: Int = DefaultSpec::Int(1), + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Converts a string into an array of chunks of the given length.", + php_manual: "https://www.php.net/manual/en/function.str-split.php", +} + +/// Returns `PhpType::Array(Box::new(PhpType::Str))` for a `str_split` call. +/// +/// A check hook is required because the `builtin!` macro cannot express array +/// return types inline. Argument types are inferred by the common registry +/// dispatch path before this hook fires; arity is pre-validated by the registry. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `str_split` call by dispatching to the shared per-arch emitter. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_str_split(ctx, inst) +} diff --git a/src/builtins/string/str_starts_with.rs b/src/builtins/string/str_starts_with.rs new file mode 100644 index 0000000000..7b62812e28 --- /dev/null +++ b/src/builtins/string/str_starts_with.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `str_starts_with` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `str_starts_with` is a pure-data builtin whose +//! return type (`Bool`) is fully determined by its declaration. The registry +//! derives the return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over `lower_binary_string_runtime` which dispatches +//! to the shared `__rt_str_starts_with` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "str_starts_with", + area: String, + params: [haystack: Str, needle: Str], + returns: Bool, + lower: lower, + summary: "Checks if a string starts with a given substring.", + php_manual: "https://www.php.net/manual/en/function.str-starts-with.php", +} + +/// Lowers a `str_starts_with` call by dispatching to the shared binary-string runtime helper. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_binary_string_runtime( + ctx, + inst, + "str_starts_with", + "__rt_str_starts_with", + ) +} diff --git a/src/builtins/string/strcasecmp.rs b/src/builtins/string/strcasecmp.rs new file mode 100644 index 0000000000..be1d85ce29 --- /dev/null +++ b/src/builtins/string/strcasecmp.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `strcasecmp` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `strcasecmp` is a pure-data builtin whose return +//! type (`Int`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over `lower_binary_string_runtime` which dispatches +//! to the shared `__rt_strcasecmp` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "strcasecmp", + area: String, + params: [string1: Str, string2: Str], + returns: Int, + lower: lower, + summary: "Binary safe case-insensitive string comparison. Returns negative, zero, or positive.", + php_manual: "https://www.php.net/manual/en/function.strcasecmp.php", +} + +/// Lowers a `strcasecmp` call by dispatching to the shared binary-string runtime helper. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_binary_string_runtime( + ctx, + inst, + "strcasecmp", + "__rt_strcasecmp", + ) +} diff --git a/src/builtins/string/strcmp.rs b/src/builtins/string/strcmp.rs new file mode 100644 index 0000000000..aa673885b8 --- /dev/null +++ b/src/builtins/string/strcmp.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `strcmp` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `strcmp` is a pure-data builtin whose return +//! type (`Int`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over `lower_binary_string_runtime` which dispatches +//! to the shared `__rt_strcmp` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "strcmp", + area: String, + params: [string1: Str, string2: Str], + returns: Int, + lower: lower, + summary: "Binary safe string comparison. Returns negative, zero, or positive.", + php_manual: "https://www.php.net/manual/en/function.strcmp.php", +} + +/// Lowers a `strcmp` call by dispatching to the shared binary-string runtime helper. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_binary_string_runtime( + ctx, + inst, + "strcmp", + "__rt_strcmp", + ) +} diff --git a/src/builtins/string/stripslashes.rs b/src/builtins/string/stripslashes.rs new file mode 100644 index 0000000000..a8e33634e5 --- /dev/null +++ b/src/builtins/string/stripslashes.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `stripslashes` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `stripslashes` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "stripslashes", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Removes backslashes from a string previously escaped by addslashes.", + php_manual: "https://www.php.net/manual/en/function.stripslashes.php", +} + +/// Lowers a `stripslashes` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "stripslashes", + "__rt_stripslashes", + ) +} diff --git a/src/builtins/string/strlen.rs b/src/builtins/string/strlen.rs new file mode 100644 index 0000000000..c00ddc8bc1 --- /dev/null +++ b/src/builtins/string/strlen.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Home of the PHP `strlen` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `lazy_check: true` so the check hook infers the argument itself (once), matching +//! legacy exactly-once inference without duplicate pre-inference by the common path. +//! - `check` accepts `Str`, `Mixed`, and `Union` types (PHP coerces the argument to a +//! string per standard type-juggling rules); other types are rejected. +//! - `lower` is a thin wrapper over the shared strlen emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "strlen", + area: String, + params: [string: Str], + returns: Int, + check: check, + lazy_check: true, + lower: lower, + summary: "Returns the length of a string.", + php_manual: "function.strlen", +} + +/// Validates the `strlen` argument and returns `Int` for accepted string-like types. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + // Accept Str, Mixed, and Union types — PHP's strlen() coerces its + // argument to a string per the standard PHP type juggling rules + // (numbers become their decimal representation, true → "1", + // false/null → ""). Mixed inputs flow through __rt_mixed_strlen + // at codegen time which reads the cell tag and returns the + // length of the coerced representation. + if !matches!(ty, PhpType::Str | PhpType::Mixed | PhpType::Union(_)) { + return Err(CompileError::new(cx.span, "strlen() argument must be string")); + } + Ok(PhpType::Int) +} + +/// Lowers a `strlen` call by dispatching to the shared strlen emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_strlen(ctx, inst) +} diff --git a/src/builtins/string/strpos.rs b/src/builtins/string/strpos.rs new file mode 100644 index 0000000000..1e4c49a2dd --- /dev/null +++ b/src/builtins/string/strpos.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Home of the PHP `strpos` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature carries the full golden param list (`haystack`, `needle`, +//! `offset`), but `max_args: 2` caps `check_arity` so a third argument is rejected, +//! matching the legacy CHECK arm which enforced exactly two arguments. +//! - `check` returns `PhpType::Union([Int, Bool])` (position, or `false` on no match). +//! A check hook is required because the `builtin!` macro `returns:` field only accepts +//! a simple type identifier and cannot express a union inline. Argument types are +//! inferred by the common registry dispatch path before the hook fires. +//! - `lower` is a thin wrapper over the shared `lower_string_position` emitter, passing +//! the `__rt_strpos` runtime helper. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "strpos", + area: String, + params: [haystack: Str, needle: Str, offset: Int = DefaultSpec::Int(0)], + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Finds the numeric position of the first occurrence of a substring.", + php_manual: "https://www.php.net/manual/en/function.strpos.php", +} + +/// Returns `PhpType::Union([Int, Bool])` for a `strpos` call (position, or `false`). +/// +/// A check hook is required because the `builtin!` macro cannot express a union return +/// type inline. Argument types are inferred by the common registry dispatch path before +/// this hook fires; arity (capped to 2 via `max_args`) is validated by the registry. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `strpos` call by dispatching to the shared string-position emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_string_position( + ctx, + inst, + "strpos", + "__rt_strpos", + ) +} diff --git a/src/builtins/string/strrev.rs b/src/builtins/string/strrev.rs new file mode 100644 index 0000000000..4083054936 --- /dev/null +++ b/src/builtins/string/strrev.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `strrev` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `strrev` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "strrev", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Reverses a string.", + php_manual: "https://www.php.net/manual/en/function.strrev.php", +} + +/// Lowers a `strrev` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "strrev", + "__rt_strrev", + ) +} diff --git a/src/builtins/string/strrpos.rs b/src/builtins/string/strrpos.rs new file mode 100644 index 0000000000..fae524c113 --- /dev/null +++ b/src/builtins/string/strrpos.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Home of the PHP `strrpos` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature carries the full golden param list (`haystack`, `needle`, +//! `offset`), but `max_args: 2` caps `check_arity` so a third argument is rejected, +//! matching the legacy CHECK arm which enforced exactly two arguments. +//! - `check` returns `PhpType::Union([Int, Bool])` (position, or `false` on no match). +//! A check hook is required because the `builtin!` macro `returns:` field only accepts +//! a simple type identifier and cannot express a union inline. Argument types are +//! inferred by the common registry dispatch path before the hook fires. +//! - `lower` is a thin wrapper over the shared `lower_string_position` emitter, passing +//! the `__rt_strrpos` runtime helper. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "strrpos", + area: String, + params: [haystack: Str, needle: Str, offset: Int = DefaultSpec::Int(0)], + max_args: 2, + returns: Mixed, + check: check, + lower: lower, + summary: "Finds the numeric position of the last occurrence of a substring.", + php_manual: "https://www.php.net/manual/en/function.strrpos.php", +} + +/// Returns `PhpType::Union([Int, Bool])` for a `strrpos` call (position, or `false`). +/// +/// A check hook is required because the `builtin!` macro cannot express a union return +/// type inline. Argument types are inferred by the common registry dispatch path before +/// this hook fires; arity (capped to 2 via `max_args`) is validated by the registry. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `strrpos` call by dispatching to the shared string-position emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_string_position( + ctx, + inst, + "strrpos", + "__rt_strrpos", + ) +} diff --git a/src/builtins/string/strstr.rs b/src/builtins/string/strstr.rs new file mode 100644 index 0000000000..d246090724 --- /dev/null +++ b/src/builtins/string/strstr.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `strstr` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature carries the full golden param list (`haystack`, `needle`, +//! `before_needle`), but `max_args: 2` caps `check_arity` so a third argument is +//! rejected, matching the legacy CHECK arm which enforced exactly two arguments. +//! - No `check` hook is needed: the return type (`Str`) is fully determined by the +//! declaration. The registry dispatch still infers each argument unconditionally, so +//! undefined-variable diagnostics fire exactly as the legacy arm produced them. +//! - `lower` is a thin wrapper over the shared `lower_strstr` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "strstr", + area: String, + params: [haystack: Str, needle: Str, before_needle: Bool = crate::builtins::spec::DefaultSpec::Bool(false)], + max_args: 2, + returns: Str, + lower: lower, + summary: "Returns the portion of a string starting at the first occurrence of a substring.", + php_manual: "https://www.php.net/manual/en/function.strstr.php", +} + +/// Lowers a `strstr` call by dispatching to the shared `lower_strstr` emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_strstr(ctx, inst) +} diff --git a/src/builtins/string/strtolower.rs b/src/builtins/string/strtolower.rs new file mode 100644 index 0000000000..14efd864e3 --- /dev/null +++ b/src/builtins/string/strtolower.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `strtolower` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `strtolower` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "strtolower", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Converts a string to lowercase.", + php_manual: "https://www.php.net/manual/en/function.strtolower.php", +} + +/// Lowers a `strtolower` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "strtolower", + "__rt_strtolower", + ) +} diff --git a/src/builtins/string/strtoupper.rs b/src/builtins/string/strtoupper.rs new file mode 100644 index 0000000000..7577c06b25 --- /dev/null +++ b/src/builtins/string/strtoupper.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Home of the PHP `strtoupper` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `strtoupper` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "strtoupper", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Converts a string to uppercase.", + php_manual: "https://www.php.net/manual/en/function.strtoupper.php", +} + +/// Lowers a `strtoupper` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "strtoupper", + "__rt_strtoupper", + ) +} diff --git a/src/builtins/string/substr.rs b/src/builtins/string/substr.rs new file mode 100644 index 0000000000..5ebf19092d --- /dev/null +++ b/src/builtins/string/substr.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Home of the PHP `substr` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `PhpType::Str`. Argument type inference happens in the common +//! registry dispatch path (`check_builtin` in `src/types/checker/builtins/mod.rs`) +//! before the hook fires, so the hook does not need to call `infer_type` again. +//! - `lower` is a thin wrapper over the shared per-arch `lower_substr` emitters. +//! - Arity is validated by the registry's `check_arity` before the check hook fires; +//! the inline arity check from the legacy arm is therefore not reproduced here. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "substr", + area: String, + params: [string: Str, offset: Int, length: Int = crate::builtins::spec::DefaultSpec::Null], + returns: Str, + check: check, + lower: lower, + summary: "Returns a portion of a string specified by the offset and length.", + php_manual: "https://www.php.net/manual/en/function.substr.php", +} + +/// Returns `PhpType::Str` for a `substr` call. +/// +/// Argument types are inferred by the common registry dispatch path in +/// `check_builtin` before this hook fires; the hook only needs to return +/// the correct return type. Arity is pre-validated by the registry before +/// the hook fires, so no inline count check is needed. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Str) +} + +/// Lowers a `substr` call by dispatching to the shared per-arch emitters. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_substr(ctx, inst) +} diff --git a/src/builtins/string/substr_replace.rs b/src/builtins/string/substr_replace.rs new file mode 100644 index 0000000000..f92252f6cf --- /dev/null +++ b/src/builtins/string/substr_replace.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Home of the PHP `substr_replace` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts required `string`, `replace`, and `offset` params, plus an optional +//! `length` param defaulting to null. +//! - `lower` is a thin wrapper over the shared `lower_substr_replace` emitter. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "substr_replace", + area: String, + params: [string: Str, replace: Str, offset: Int, length: Mixed = DefaultSpec::Null], + returns: Str, + lower: lower, + summary: "Replaces text within a portion of a string.", + php_manual: "https://www.php.net/manual/en/function.substr-replace.php", +} + +/// Lowers a `substr_replace` call by dispatching to the shared substr-replace emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_substr_replace(ctx, inst) +} diff --git a/src/builtins/string/trim.rs b/src/builtins/string/trim.rs new file mode 100644 index 0000000000..19d5bfea92 --- /dev/null +++ b/src/builtins/string/trim.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Home of the PHP `trim` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `trim` is a pure-data builtin. The registry's arity +//! check (1 required, 1 optional → 1 or 2 args) exactly matches the legacy check-arm +//! constraint, so no additional validation is needed. +//! - `lower` is a thin wrapper over `lower_trim_like` which dispatches to the appropriate +//! runtime helper depending on whether a mask argument is provided. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "trim", + area: String, + params: [ + string: Str, + characters: Str = crate::builtins::spec::DefaultSpec::Str(" \n\r\t\u{000b}\u{000c}\0"), + ], + returns: Str, + lower: lower, + summary: "Strips whitespace (or other characters) from the beginning and end of a string.", + php_manual: "https://www.php.net/manual/en/function.trim.php", +} + +/// Lowers a `trim` call by dispatching to `lower_trim_like` with the default and mask runtime labels. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_trim_like( + ctx, + inst, + "trim", + "__rt_trim", + "__rt_trim_mask", + ) +} diff --git a/src/builtins/string/ucfirst.rs b/src/builtins/string/ucfirst.rs new file mode 100644 index 0000000000..3bd2756fc0 --- /dev/null +++ b/src/builtins/string/ucfirst.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `ucfirst` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `ucfirst` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the dedicated `lower_ucfirst` emitter in the +//! strings lowering module. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ucfirst", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Uppercases the first character of a string.", + php_manual: "https://www.php.net/manual/en/function.ucfirst.php", +} + +/// Lowers a `ucfirst` call by dispatching to the dedicated per-arch emitter. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_ucfirst(ctx, inst) +} diff --git a/src/builtins/string/ucwords.rs b/src/builtins/string/ucwords.rs new file mode 100644 index 0000000000..32f7a25443 --- /dev/null +++ b/src/builtins/string/ucwords.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `ucwords` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature carries the full golden param list (`string`, `separators`), +//! but `max_args: 1` caps `check_arity` so a second argument is rejected, matching the +//! legacy CHECK arm which enforced exactly one argument. +//! - No `check` hook is needed: the return type (`Str`) is fully determined by the +//! declaration. The registry dispatch still infers each argument unconditionally, so +//! undefined-variable diagnostics fire exactly as the legacy arm produced them. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_ucwords` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "ucwords", + area: String, + params: [string: Str, separators: Str = crate::builtins::spec::DefaultSpec::Str(" \t\r\n\u{0c}\u{0b}")], + max_args: 1, + returns: Str, + lower: lower, + summary: "Uppercases the first character of each word in a string.", + php_manual: "https://www.php.net/manual/en/function.ucwords.php", +} + +/// Lowers a `ucwords` call by dispatching to the shared unary string-runtime emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "ucwords", + "__rt_ucwords", + ) +} diff --git a/src/builtins/string/urldecode.rs b/src/builtins/string/urldecode.rs new file mode 100644 index 0000000000..d34e179d5d --- /dev/null +++ b/src/builtins/string/urldecode.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `urldecode` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `urldecode` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_urldecode` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "urldecode", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "Decodes a URL-encoded string, including '+' as a space.", + php_manual: "https://www.php.net/manual/en/function.urldecode.php", +} + +/// Lowers a `urldecode` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "urldecode", + "__rt_urldecode", + ) +} diff --git a/src/builtins/string/urlencode.rs b/src/builtins/string/urlencode.rs new file mode 100644 index 0000000000..2986397c4d --- /dev/null +++ b/src/builtins/string/urlencode.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `urlencode` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `urlencode` is a pure-data builtin whose return +//! type (`Str`) is fully determined by its declaration. The registry derives the +//! return type from the `returns:` field without calling a check hook. +//! - `lower` is a thin wrapper over the shared `lower_unary_string_runtime` emitter, +//! passing the `__rt_urlencode` runtime helper. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "urlencode", + area: String, + params: [string: Str], + returns: Str, + lower: lower, + summary: "URL-encodes a string using application/x-www-form-urlencoded rules.", + php_manual: "https://www.php.net/manual/en/function.urlencode.php", +} + +/// Lowers a `urlencode` call by dispatching to the shared per-arch unary string runtime. +fn lower( + ctx: &mut FunctionContext, + inst: &Instruction, +) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( + ctx, + inst, + "urlencode", + "__rt_urlencode", + ) +} diff --git a/src/builtins/string/vprintf.rs b/src/builtins/string/vprintf.rs new file mode 100644 index 0000000000..1f9507d343 --- /dev/null +++ b/src/builtins/string/vprintf.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `vprintf` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts a required `format` string and a `values` array. +//! - `lower` is a thin wrapper over the shared `lower_vprintf` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "vprintf", + area: String, + params: [format: Str, values: Mixed], + returns: Int, + lower: lower, + summary: "Outputs a formatted string using an array of values.", + php_manual: "https://www.php.net/manual/en/function.vprintf.php", +} + +/// Lowers a `vprintf` call by dispatching to the shared vprintf emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_vprintf(ctx, inst) +} diff --git a/src/builtins/string/vsprintf.rs b/src/builtins/string/vsprintf.rs new file mode 100644 index 0000000000..b96f011732 --- /dev/null +++ b/src/builtins/string/vsprintf.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `vsprintf` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts a required `format` string and a `values` array. +//! - `lower` is a thin wrapper over the shared `lower_vsprintf` emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "vsprintf", + area: String, + params: [format: Str, values: Mixed], + returns: Str, + lower: lower, + summary: "Returns a formatted string using an array of values.", + php_manual: "https://www.php.net/manual/en/function.vsprintf.php", +} + +/// Lowers a `vsprintf` call by dispatching to the shared vsprintf emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_vsprintf(ctx, inst) +} diff --git a/src/builtins/string/wordwrap.rs b/src/builtins/string/wordwrap.rs new file mode 100644 index 0000000000..ccc7f2b20f --- /dev/null +++ b/src/builtins/string/wordwrap.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Home of the PHP `wordwrap` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! all via `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts a required `string` param plus optional `width`, `break`, and +//! `cut_long_words` params with PHP-compatible defaults. The `break` param +//! uses the raw identifier `r#break` because `break` is a Rust keyword. +//! - `lower` is a thin wrapper over the shared `lower_wordwrap` emitter. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "wordwrap", + area: String, + params: [ + string: Str, + width: Int = DefaultSpec::Int(75), + r#break: Str = DefaultSpec::Str("\n"), + cut_long_words: Bool = DefaultSpec::Bool(false) + ], + returns: Str, + lower: lower, + summary: "Wraps a string to a given number of characters.", + php_manual: "https://www.php.net/manual/en/function.wordwrap.php", +} + +/// Lowers a `wordwrap` call by dispatching to the shared wordwrap emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::strings::lower_wordwrap(ctx, inst) +} diff --git a/src/builtins/system/__elephc_gmmktime_raw.rs b/src/builtins/system/__elephc_gmmktime_raw.rs new file mode 100644 index 0000000000..ded0d5006b --- /dev/null +++ b/src/builtins/system/__elephc_gmmktime_raw.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the internal `__elephc_gmmktime_raw` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - This is an internal builtin (`internal: true`) not exposed as a PHP-visible function. +//! It is used by the synthetic DateTime body as a raw gmmktime alias. +//! - The lower hook delegates to the same emitter as `gmmktime`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "__elephc_gmmktime_raw", + area: System, + params: [hour: Int, minute: Int, second: Int, month: Int, day: Int, year: Int], + returns: Int, + lower: lower, + summary: "Internal raw gmmktime alias used by the synthetic DateTime body.", + internal: true, +} + +/// Lowers an `__elephc_gmmktime_raw` call by delegating to the shared gmmktime emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_gmmktime(ctx, inst) +} diff --git a/src/builtins/system/__elephc_mktime_raw.rs b/src/builtins/system/__elephc_mktime_raw.rs new file mode 100644 index 0000000000..ca58580390 --- /dev/null +++ b/src/builtins/system/__elephc_mktime_raw.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the internal `__elephc_mktime_raw` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - This is an internal builtin (`internal: true`) not exposed as a PHP-visible function. +//! It is used by the synthetic DateTime body as a raw mktime alias. +//! - The lower hook delegates to the same emitter as `mktime`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "__elephc_mktime_raw", + area: System, + params: [hour: Int, minute: Int, second: Int, month: Int, day: Int, year: Int], + returns: Int, + lower: lower, + summary: "Internal raw mktime alias used by the synthetic DateTime body.", + internal: true, +} + +/// Lowers an `__elephc_mktime_raw` call by delegating to the shared mktime emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_mktime(ctx, inst) +} diff --git a/src/builtins/system/__elephc_strtotime_raw.rs b/src/builtins/system/__elephc_strtotime_raw.rs new file mode 100644 index 0000000000..2af3056799 --- /dev/null +++ b/src/builtins/system/__elephc_strtotime_raw.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the internal `__elephc_strtotime_raw` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - This is an internal builtin (`internal: true`) not exposed as a PHP-visible function. +//! It is a raw strtotime alias returning a plain integer rather than int|false. +//! - The `arity_error` override preserves the user-facing `strtotime` error message. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "__elephc_strtotime_raw", + area: System, + params: [datetime: Str, baseTimestamp: Int = DefaultSpec::Null], + arity_error: "strtotime() takes 1 or 2 arguments", + returns: Int, + lower: lower, + summary: "Internal raw strtotime alias returning a plain integer.", + internal: true, +} + +/// Lowers an `__elephc_strtotime_raw` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_elephc_strtotime_raw(ctx, inst) +} diff --git a/src/builtins/system/attr_support.rs b/src/builtins/system/attr_support.rs new file mode 100644 index 0000000000..b07df26dae --- /dev/null +++ b/src/builtins/system/attr_support.rs @@ -0,0 +1,98 @@ +//! Purpose: +//! Shared helper functions for the class-attribute reflection builtins +//! (`class_attribute_names`, `class_attribute_args`, `class_get_attributes`). +//! Relocated from `src/types/checker/builtins/system.rs` into the builtin +//! registry home area so all three attribute homes can import them from one place. +//! +//! Called from: +//! - `crate::builtins::system::class_attribute_names` (check hook) +//! - `crate::builtins::system::class_attribute_args` (check hook) +//! - `crate::builtins::system::class_get_attributes` (check hook) +//! +//! Key details: +//! - `resolve_class_name` performs a case-insensitive PHP-symbol-key lookup. +//! - The two `*_unsupported` helpers inspect class attribute metadata to detect +//! features that the flat helper builtins cannot faithfully represent. + +use crate::names::php_symbol_key; +use crate::types::checker::Checker; + +/// Resolves a class name to its canonical key in the checker's class table. +/// +/// Returns `Some(canonical_name)` if the class exists, `None` otherwise. +/// The lookup is case-insensitive per PHP rules. +pub(crate) fn resolve_class_name<'a>(checker: &'a Checker, class_name: &str) -> Option<&'a str> { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + checker + .classes + .keys() + .find(|existing| php_symbol_key(existing) == class_key) + .map(String::as_str) +} + +/// Returns `true` if the named attribute on the class uses argument metadata +/// that the compiler does not yet support (i.e., `attribute_args` slot is `None`). +pub(crate) fn class_attribute_args_unsupported( + checker: &Checker, + class_name: &str, + attr_name: &str, +) -> bool { + let Some(resolved_class) = resolve_class_name(checker, class_name) else { + return false; + }; + let Some(class_info) = checker.classes.get(resolved_class) else { + return false; + }; + let attr_key = php_symbol_key(attr_name.trim_start_matches('\\')); + class_info + .attribute_names + .iter() + .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. + 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. +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::Array(inner) + if attr_entries_unsupported_by_flat_helper(inner) + ) + }) +} + +/// Returns `true` if the class has any attribute whose argument metadata is not +/// fully supported (slot count mismatch or any `None` slot in `attribute_args`). +pub(crate) fn class_get_attributes_unsupported(checker: &Checker, class_name: &str) -> bool { + let Some(resolved_class) = resolve_class_name(checker, class_name) else { + return false; + }; + checker.classes.get(resolved_class).is_some_and(|class_info| { + class_info.attribute_names.len() != class_info.attribute_args.len() + || class_info.attribute_args.iter().any(Option::is_none) + }) +} diff --git a/src/builtins/system/checkdate.rs b/src/builtins/system/checkdate.rs new file mode 100644 index 0000000000..185e1853ec --- /dev/null +++ b/src/builtins/system/checkdate.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `checkdate` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `checkdate` is a pure-data builtin whose return type +//! (`Bool`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "checkdate", + area: System, + params: [month: Int, day: Int, year: Int], + returns: Bool, + lower: lower, + summary: "Validates a Gregorian date.", +} + +/// Lowers a `checkdate` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_checkdate(ctx, inst) +} diff --git a/src/builtins/system/class_attribute_args.rs b/src/builtins/system/class_attribute_args.rs new file mode 100644 index 0000000000..f0c997e906 --- /dev/null +++ b/src/builtins/system/class_attribute_args.rs @@ -0,0 +1,93 @@ +//! Purpose: +//! Home of the PHP `class_attribute_args` builtin: its declaration, type-check hook, +//! and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that both arguments are string literals, resolves the class at +//! compile time, verifies the attribute is supported by the flat helper, and returns +//! `Array(Mixed)`. +//! - Dynamic class or attribute names are not yet supported; only string literals are accepted. +//! - `lower` delegates to `attributes::lower_class_attribute_args` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::system::attr_support::{class_attribute_args_unsupported, resolve_class_name}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "class_attribute_args", + area: System, + params: [class_name: Str, attribute_name: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns the constructor arguments of a named attribute applied to a class.", +} + +/// Validates both arguments are string literals, resolves the class and attribute, +/// checks support, and returns `Array(Mixed)`. +/// +/// Requires compile-time string literals for both class and attribute names. +/// Rejects attributes whose argument metadata cannot be faithfully represented by +/// the flat helper (keyed arguments, symbolic references); directs users to +/// `ReflectionClass::getAttributes()->getArguments()` for those cases. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let class_arg_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(class_arg_ty, PhpType::Str) { + return Err(CompileError::new( + cx.span, + "class_attribute_args() first argument must be a string class name", + )); + } + let attr_arg_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if !matches!(attr_arg_ty, PhpType::Str) { + return Err(CompileError::new( + cx.span, + "class_attribute_args() second argument must be a string attribute name", + )); + } + let ExprKind::StringLiteral(class_name) = &cx.args[0].kind else { + return Err(CompileError::new( + cx.span, + "class_attribute_args() requires a string literal class name (dynamic lookup is not yet supported)", + )); + }; + if !matches!(cx.args[1].kind, ExprKind::StringLiteral(_)) { + return Err(CompileError::new( + cx.span, + "class_attribute_args() requires a string literal attribute name (dynamic lookup is not yet supported)", + )); + } + if resolve_class_name(cx.checker, class_name).is_none() { + return Err(CompileError::new( + cx.span, + &format!( + "class_attribute_args(): undefined class '{}'", + class_name + ), + )); + } + let ExprKind::StringLiteral(attr_name) = &cx.args[1].kind else { + unreachable!("attribute argument literal checked above"); + }; + if class_attribute_args_unsupported(cx.checker, class_name, attr_name) { + return Err(CompileError::new( + cx.span, + "class_attribute_args(): requested attribute uses argument metadata that is not supported yet", + )); + } + Ok(PhpType::Array(Box::new(PhpType::Mixed))) +} + +/// Lowers a `class_attribute_args` call by delegating to the shared attributes emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::attributes::lower_class_attribute_args(ctx, inst) +} diff --git a/src/builtins/system/class_attribute_names.rs b/src/builtins/system/class_attribute_names.rs new file mode 100644 index 0000000000..bf96d8a690 --- /dev/null +++ b/src/builtins/system/class_attribute_names.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Home of the PHP `class_attribute_names` builtin: its declaration, type-check hook, +//! and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a string literal class name, resolves the +//! class at compile time, and returns `Array(Str)`. +//! - Dynamic class names are not yet supported; only string literals are accepted. +//! - `lower` delegates to `attributes::lower_class_attribute_names` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::system::attr_support::resolve_class_name; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "class_attribute_names", + area: System, + params: [class_name: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns the list of attribute names applied to a class.", +} + +/// Validates that the argument is a string literal class name, resolves the class, +/// and returns `Array(Str)`. +/// +/// Requires a compile-time string literal: dynamic class names are not yet supported. +/// Emits a compile error if the class is not defined. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + // Resolve at compile time: only string-literal class names are + // supported in this iteration. Dynamic class names would require + // a runtime name→class_id lookup table that elephc does not yet + // expose. + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Str) { + return Err(CompileError::new( + cx.span, + "class_attribute_names() argument must be a string class name", + )); + } + let ExprKind::StringLiteral(class_name) = &cx.args[0].kind else { + return Err(CompileError::new( + cx.span, + "class_attribute_names() requires a string literal class name (dynamic lookup is not yet supported)", + )); + }; + if resolve_class_name(cx.checker, class_name).is_none() { + return Err(CompileError::new( + cx.span, + &format!( + "class_attribute_names(): undefined class '{}'", + class_name + ), + )); + } + Ok(PhpType::Array(Box::new(PhpType::Str))) +} + +/// Lowers a `class_attribute_names` call by delegating to the shared attributes emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::attributes::lower_class_attribute_names(ctx, inst) +} diff --git a/src/builtins/system/class_get_attributes.rs b/src/builtins/system/class_get_attributes.rs new file mode 100644 index 0000000000..d7408b1fbb --- /dev/null +++ b/src/builtins/system/class_get_attributes.rs @@ -0,0 +1,78 @@ +//! Purpose: +//! Home of the PHP `class_get_attributes` builtin: its declaration, type-check hook, +//! and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a string literal class name, resolves the +//! class at compile time, checks that all attributes are supported, and returns +//! `Array(Object("ReflectionAttribute"))`. +//! - Dynamic class names are not yet supported; only string literals are accepted. +//! - `lower` delegates to `attributes::lower_class_get_attributes` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::system::attr_support::{class_get_attributes_unsupported, resolve_class_name}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "class_get_attributes", + area: System, + params: [class_name: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns an array of ReflectionAttribute objects for all attributes of a class.", +} + +/// Validates that the argument is a string literal class name, resolves the class, +/// checks that all attributes are supported, and returns `Array(Object("ReflectionAttribute"))`. +/// +/// Requires a compile-time string literal: dynamic class names are not yet supported. +/// Rejects classes where any attribute has unsupported argument metadata (slot count +/// mismatch or `None` slot); directs users to `ReflectionClass::getAttributes()` for those. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(ty, PhpType::Str) { + return Err(CompileError::new( + cx.span, + "class_get_attributes() argument must be a string class name", + )); + } + let ExprKind::StringLiteral(class_name) = &cx.args[0].kind else { + return Err(CompileError::new( + cx.span, + "class_get_attributes() requires a string literal class name (dynamic lookup is not yet supported)", + )); + }; + if resolve_class_name(cx.checker, class_name).is_none() { + return Err(CompileError::new( + cx.span, + &format!( + "class_get_attributes(): undefined class '{}'", + class_name + ), + )); + } + if class_get_attributes_unsupported(cx.checker, class_name) { + return Err(CompileError::new( + cx.span, + "class_get_attributes(): class has attribute argument metadata that is not supported yet", + )); + } + Ok(PhpType::Array(Box::new(PhpType::Object( + "ReflectionAttribute".to_string(), + )))) +} + +/// Lowers a `class_get_attributes` call by delegating to the shared attributes emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::attributes::lower_class_get_attributes(ctx, inst) +} diff --git a/src/builtins/system/date.rs b/src/builtins/system/date.rs new file mode 100644 index 0000000000..791a05968c --- /dev/null +++ b/src/builtins/system/date.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `date` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `date` is a pure-data builtin whose return type +//! (`Str`) is fully determined by its declaration. The `timestamp` parameter +//! is optional and defaults to `null` (current time). + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "date", + area: System, + params: [format: Str, timestamp: Int = DefaultSpec::Null], + returns: Str, + lower: lower, + summary: "Formats a local time/date.", +} + +/// Lowers a `date` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_date(ctx, inst) +} diff --git a/src/builtins/system/date_default_timezone_get.rs b/src/builtins/system/date_default_timezone_get.rs new file mode 100644 index 0000000000..f01fe8539f --- /dev/null +++ b/src/builtins/system/date_default_timezone_get.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `date_default_timezone_get` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `date_default_timezone_get` is a pure-data builtin +//! whose return type (`Str`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "date_default_timezone_get", + area: System, + params: [], + returns: Str, + lower: lower, + summary: "Gets the default timezone.", +} + +/// Lowers a `date_default_timezone_get` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_date_default_timezone_get(ctx, inst) +} diff --git a/src/builtins/system/date_default_timezone_set.rs b/src/builtins/system/date_default_timezone_set.rs new file mode 100644 index 0000000000..4b09477721 --- /dev/null +++ b/src/builtins/system/date_default_timezone_set.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `date_default_timezone_set` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `date_default_timezone_set` is a pure-data builtin +//! whose return type (`Bool`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "date_default_timezone_set", + area: System, + params: [timezoneId: Str], + returns: Bool, + lower: lower, + summary: "Sets the default timezone.", +} + +/// Lowers a `date_default_timezone_set` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_date_default_timezone_set(ctx, inst) +} diff --git a/src/builtins/system/define.rs b/src/builtins/system/define.rs new file mode 100644 index 0000000000..90f2c5ace7 --- /dev/null +++ b/src/builtins/system/define.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Home of the PHP `define` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the first argument is a string literal and registers the +//! constant's type in `checker.constants` as a compile-time side effect. +//! - The hook calls `infer_type` on the value argument to obtain its type for registration. +//! - `lower` delegates to the module-level `lower_define` in `src/codegen/lower_inst/builtins.rs`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "define", + area: System, + params: [constant_name: Str, value: Mixed], + returns: Bool, + check: check, + lower: lower, + summary: "Defines a named constant at compile time.", +} + +/// Validates that the first argument is a string literal and registers the constant. +/// +/// Checks that `constant_name` is a `StringLiteral` expression (AOT requirement); +/// infers the type of `value`; and registers the constant's name→type mapping in +/// `checker.constants` so that subsequent `defined()` and `constant()` calls see it. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let name_str = match &cx.args[0].kind { + ExprKind::StringLiteral(s) => s.clone(), + _ => { + return Err(CompileError::new( + cx.span, + "define() first argument must be a string literal", + )); + } + }; + let ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + cx.checker.constants.entry(name_str).or_insert(ty); + Ok(PhpType::Bool) +} + +/// Lowers a `define` call by delegating to the shared module-level emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_define(ctx, inst) +} diff --git a/src/builtins/system/defined.rs b/src/builtins/system/defined.rs new file mode 100644 index 0000000000..0d5aac91f2 --- /dev/null +++ b/src/builtins/system/defined.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Home of the PHP `defined` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the argument is a string literal (AOT requirement: the +//! constant name must be statically known at compile time). +//! - `lower` delegates to the module-level `lower_defined` in `src/codegen/lower_inst/builtins.rs`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "defined", + area: System, + params: [constant_name: Str], + returns: Bool, + check: check, + lower: lower, + summary: "Checks whether the given named constant exists.", +} + +/// Validates that the argument is a string literal. +/// +/// AOT compilation requires a statically known constant name; dynamic names cannot +/// be resolved at compile time. Returns `PhpType::Bool` on success. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!(cx.args[0].kind, ExprKind::StringLiteral(_)) { + return Err(CompileError::new( + cx.span, + "defined() first argument must be a string literal in AOT mode", + )); + } + Ok(PhpType::Bool) +} + +/// Lowers a `defined` call by delegating to the shared module-level emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_defined(ctx, inst) +} diff --git a/src/builtins/system/exec.rs b/src/builtins/system/exec.rs new file mode 100644 index 0000000000..4a77a33be2 --- /dev/null +++ b/src/builtins/system/exec.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `exec` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Str`) is fully determined by the declaration. +//! - `lower` is a thin wrapper over `system::lower_exec` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "exec", + area: System, + params: [command: Str], + returns: Str, + lower: lower, + summary: "Executes an external program and returns the last line of output.", +} + +/// Lowers an `exec` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_exec(ctx, inst) +} diff --git a/src/builtins/system/getdate.rs b/src/builtins/system/getdate.rs new file mode 100644 index 0000000000..dfbe47f6bd --- /dev/null +++ b/src/builtins/system/getdate.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `getdate` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `getdate` is a pure-data builtin whose return type +//! (`Mixed`) is fully determined by its declaration. The `timestamp` parameter +//! is optional and defaults to `null` (current time). + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "getdate", + area: System, + params: [timestamp: Int = DefaultSpec::Null], + returns: Mixed, + lower: lower, + summary: "Returns date/time information.", +} + +/// Lowers a `getdate` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_getdate(ctx, inst) +} diff --git a/src/builtins/system/getenv.rs b/src/builtins/system/getenv.rs new file mode 100644 index 0000000000..8a7d501041 --- /dev/null +++ b/src/builtins/system/getenv.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Home of the PHP `getenv` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` returns `Union(Str, Bool)` to reflect PHP's behaviour where `getenv` +//! returns the value string on success or `false` if the variable is unset. +//! - `lower` is a thin wrapper over `system::lower_getenv` in the EIR backend. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "getenv", + area: System, + params: [name: Str], + returns: Mixed, + check: check, + lower: lower, + summary: "Gets the value of an environment variable.", +} + +/// Returns `Union(Str, Bool)` reflecting that `getenv` can return a string or `false`. +/// +/// Infers the argument type to trigger type-environment side effects before returning +/// the normalized union type. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + Ok(cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Bool])) +} + +/// Lowers a `getenv` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_getenv(ctx, inst) +} diff --git a/src/builtins/system/gmdate.rs b/src/builtins/system/gmdate.rs new file mode 100644 index 0000000000..82823f0e8f --- /dev/null +++ b/src/builtins/system/gmdate.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `gmdate` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `gmdate` is a pure-data builtin whose return type +//! (`Str`) is fully determined by its declaration. The `timestamp` parameter +//! is optional and defaults to `null` (current time). + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "gmdate", + area: System, + params: [format: Str, timestamp: Int = DefaultSpec::Null], + returns: Str, + lower: lower, + summary: "Formats a GMT/UTC date and time.", +} + +/// Lowers a `gmdate` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_gmdate(ctx, inst) +} diff --git a/src/builtins/system/gmmktime.rs b/src/builtins/system/gmmktime.rs new file mode 100644 index 0000000000..7826764de4 --- /dev/null +++ b/src/builtins/system/gmmktime.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `gmmktime` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `gmmktime` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "gmmktime", + area: System, + params: [hour: Int, minute: Int, second: Int, month: Int, day: Int, year: Int], + returns: Int, + lower: lower, + summary: "Returns the Unix timestamp for a GMT date.", +} + +/// Lowers a `gmmktime` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_gmmktime(ctx, inst) +} diff --git a/src/builtins/system/header.rs b/src/builtins/system/header.rs new file mode 100644 index 0000000000..94b01db407 --- /dev/null +++ b/src/builtins/system/header.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `header` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Void`) is fully determined by the declaration. +//! - `lower` is a thin wrapper over `system::lower_header` in the EIR backend. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "header", + area: System, + params: [header: Str, replace: Bool = DefaultSpec::Bool(true), response_code: Int = DefaultSpec::Int(0)], + returns: Void, + lower: lower, + summary: "Sends a raw HTTP header.", +} + +/// Lowers a `header` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_header(ctx, inst) +} diff --git a/src/builtins/system/hrtime.rs b/src/builtins/system/hrtime.rs new file mode 100644 index 0000000000..00cca14ebf --- /dev/null +++ b/src/builtins/system/hrtime.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `hrtime` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `hrtime` is a pure-data builtin whose return type +//! (`Mixed`) is fully determined by its declaration. The `as_number` parameter +//! is optional and defaults to `false`. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "hrtime", + area: System, + params: [as_number: Bool = DefaultSpec::Bool(false)], + returns: Mixed, + lower: lower, + summary: "Returns the current high-resolution time.", +} + +/// Lowers an `hrtime` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_hrtime(ctx, inst) +} diff --git a/src/builtins/system/http_response_code.rs b/src/builtins/system/http_response_code.rs new file mode 100644 index 0000000000..d4a92fc4b1 --- /dev/null +++ b/src/builtins/system/http_response_code.rs @@ -0,0 +1,32 @@ +//! Purpose: +//! Home of the PHP `http_response_code` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Int`) is fully determined by the declaration. +//! - `arity_error` overrides the default "takes at most 1 argument" message to match +//! the legacy phrasing "takes 0 or 1 arguments". +//! - `lower` is a thin wrapper over `system::lower_http_response_code` in the EIR backend. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "http_response_code", + area: System, + params: [response_code: Int = DefaultSpec::Int(0)], + arity_error: "http_response_code() takes 0 or 1 arguments", + returns: Int, + lower: lower, + summary: "Gets or sets the HTTP response code.", +} + +/// Lowers an `http_response_code` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_http_response_code(ctx, inst) +} diff --git a/src/builtins/system/json_decode.rs b/src/builtins/system/json_decode.rs new file mode 100644 index 0000000000..a0e7192ed9 --- /dev/null +++ b/src/builtins/system/json_decode.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Home of the PHP `json_decode` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook validates the json argument type, the optional associative +//! argument type, and that depth/flags are integers. Type errors are reported at +//! the offending argument's span (not the call span). + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::builtins::system::json_support; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "json_decode", + area: System, + params: [ + json: Str, + associative: Bool = DefaultSpec::Null, + depth: Int = DefaultSpec::Int(512), + flags: Int = DefaultSpec::Int(0), + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Decodes a JSON string.", +} + +/// Validates the json argument is string-compatible, the associative argument is +/// bool-compatible or null, and depth/flags are integers. +/// +/// Reports type errors at the span of the offending argument, not the call span. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let json_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !json_support::is_json_string_arg_type(&json_ty) { + return Err(CompileError::new( + cx.args[0].span, + "json_decode() json argument must be string-compatible", + )); + } + if let Some(assoc) = cx.args.get(1) { + let assoc_ty = cx.checker.infer_type(assoc, cx.env)?; + if !json_support::is_json_associative_arg_type(&assoc_ty) { + return Err(CompileError::new( + assoc.span, + "json_decode() associative argument must be bool-compatible or null", + )); + } + } + for extra in cx.args.iter().skip(2) { + let ty = cx.checker.infer_type(extra, cx.env)?; + if ty != PhpType::Int { + return Err(CompileError::new( + extra.span, + "json_decode() depth and flags must be integers", + )); + } + } + Ok(PhpType::Mixed) +} + +/// Lowers a `json_decode` call by dispatching to the shared JSON emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::json::lower_json_decode(ctx, inst) +} diff --git a/src/builtins/system/json_encode.rs b/src/builtins/system/json_encode.rs new file mode 100644 index 0000000000..8c2f6355c5 --- /dev/null +++ b/src/builtins/system/json_encode.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Home of the PHP `json_encode` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook validates that all flag/depth arguments are integers, reporting +//! each type error at the offending argument's span (not the call span). + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "json_encode", + area: System, + params: [ + value: Mixed, + flags: Int = DefaultSpec::Int(0), + depth: Int = DefaultSpec::Int(512), + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Returns the JSON representation of a value.", +} + +/// Validates that all flag and depth arguments are integers. +/// +/// Reports type errors at the span of the offending argument, not the call span. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + for extra in &cx.args[1..] { + let ty = cx.checker.infer_type(extra, cx.env)?; + if ty != PhpType::Int { + return Err(CompileError::new( + extra.span, + "json_encode() flags and depth must be integers", + )); + } + } + Ok(PhpType::Str) +} + +/// Lowers a `json_encode` call by dispatching to the shared JSON emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::json::lower_json_encode(ctx, inst) +} diff --git a/src/builtins/system/json_last_error.rs b/src/builtins/system/json_last_error.rs new file mode 100644 index 0000000000..9e903b06a2 --- /dev/null +++ b/src/builtins/system/json_last_error.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `json_last_error` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `json_last_error` takes no arguments and always +//! returns `Int`. The registry common path enforces arity before falling back +//! to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "json_last_error", + area: System, + params: [], + returns: Int, + lower: lower, + summary: "Returns the last error (if any) occurred during the last JSON encoding/decoding.", +} + +/// Lowers a `json_last_error` call by dispatching to the shared JSON emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::json::lower_json_last_error(ctx, inst) +} diff --git a/src/builtins/system/json_last_error_msg.rs b/src/builtins/system/json_last_error_msg.rs new file mode 100644 index 0000000000..70ce5512e9 --- /dev/null +++ b/src/builtins/system/json_last_error_msg.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `json_last_error_msg` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `json_last_error_msg` takes no arguments and +//! always returns `Str`. The registry common path enforces arity. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "json_last_error_msg", + area: System, + params: [], + returns: Str, + lower: lower, + summary: "Returns the error string of the last json_encode() or json_decode() call.", +} + +/// Lowers a `json_last_error_msg` call by dispatching to the shared JSON emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::json::lower_json_last_error_msg(ctx, inst) +} diff --git a/src/builtins/system/json_support.rs b/src/builtins/system/json_support.rs new file mode 100644 index 0000000000..677042b818 --- /dev/null +++ b/src/builtins/system/json_support.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Shared helper functions for the JSON builtins +//! (`json_encode`, `json_decode`, `json_validate`, `unserialize`). +//! Relocated from `src/types/checker/builtins/system.rs` into the builtin +//! registry home area so all JSON homes can import them from one place. +//! +//! Called from: +//! - `crate::builtins::system::json_encode` (check hook) +//! - `crate::builtins::system::json_decode` (check hook) +//! - `crate::builtins::system::json_validate` (check hook) +//! - `crate::builtins::system::unserialize` (check hook) +//! +//! Key details: +//! - `is_json_string_arg_type` accepts scalars and Mixed (not arrays/objects). +//! - `is_json_associative_arg_type` accepts bool-compatible types and Mixed. +//! - `json_static_int_value` folds literals, known JSON constants, and bitwise ops. + +use crate::parser::ast::{BinOp, Expr, ExprKind}; +use crate::types::json_constants::JSON_INT_CONSTANTS; +use crate::types::PhpType; + +/// Returns `true` if `ty` is a valid type for the JSON string argument in +/// `json_decode` / `json_validate` / `json_encode` (scalar types and `Mixed`). +pub(crate) fn is_json_string_arg_type(ty: &PhpType) -> bool { + match ty { + PhpType::Str + | PhpType::Int + | PhpType::Float + | PhpType::Bool + | PhpType::Void + | PhpType::Mixed => true, + PhpType::Union(types) => types.iter().all(is_json_string_arg_type), + _ => false, + } +} + +/// Returns `true` if `ty` is a valid type for the associative argument in +/// `json_decode` (bool-compatible types plus `Mixed`). +pub(crate) fn is_json_associative_arg_type(ty: &PhpType) -> bool { + match ty { + PhpType::Bool + | PhpType::Int + | PhpType::Float + | PhpType::Str + | PhpType::Void + | PhpType::Mixed => true, + PhpType::Union(types) => types.iter().all(is_json_associative_arg_type), + _ => false, + } +} + +/// Attempts to evaluate an expression as a static integer at compile time. +/// Supports literals, known constants, negation, and bitwise ops. +/// Returns `Some(value)` if the expression is statically computable, `None` otherwise. +pub(crate) fn json_static_int_value(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::ConstRef(name) => JSON_INT_CONSTANTS + .iter() + .find_map(|(constant, value)| (*constant == name.as_str()).then_some(*value)), + ExprKind::Negate(inner) => json_static_int_value(inner).map(|value| -value), + ExprKind::BinaryOp { left, op, right } => { + let left = json_static_int_value(left)?; + let right = json_static_int_value(right)?; + match op { + BinOp::BitAnd => Some(left & right), + BinOp::BitOr => Some(left | right), + BinOp::BitXor => Some(left ^ right), + _ => None, + } + } + _ => None, + } +} diff --git a/src/builtins/system/json_validate.rs b/src/builtins/system/json_validate.rs new file mode 100644 index 0000000000..8ae2825efb --- /dev/null +++ b/src/builtins/system/json_validate.rs @@ -0,0 +1,73 @@ +//! Purpose: +//! Home of the PHP `json_validate` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook validates the json argument type, that depth/flags are integers, +//! and that the flags value (if statically known) is 0 or JSON_INVALID_UTF8_IGNORE. +//! Type errors are reported at the offending argument's span. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::builtins::system::json_support; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "json_validate", + area: System, + params: [ + json: Str, + depth: Int = DefaultSpec::Int(512), + flags: Int = DefaultSpec::Int(0), + ], + returns: Bool, + check: check, + lower: lower, + summary: "Checks if a string contains valid JSON.", +} + +/// Validates the json argument is string-compatible, depth/flags are integers, +/// and the static flags value (if known) is 0 or JSON_INVALID_UTF8_IGNORE. +/// +/// Reports type errors at the span of the offending argument, not the call span. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let json_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !json_support::is_json_string_arg_type(&json_ty) { + return Err(CompileError::new( + cx.args[0].span, + "json_validate() json argument must be string-compatible", + )); + } + for extra in &cx.args[1..] { + let ty = cx.checker.infer_type(extra, cx.env)?; + if ty != PhpType::Int { + return Err(CompileError::new( + extra.span, + "json_validate() depth and flags must be integers", + )); + } + } + if let Some(flags) = cx.args.get(2) { + if let Some(value) = json_support::json_static_int_value(flags) { + const JSON_INVALID_UTF8_IGNORE: i64 = 1_048_576; + if value & !JSON_INVALID_UTF8_IGNORE != 0 { + return Err(CompileError::new( + flags.span, + "json_validate() flags must be 0 or JSON_INVALID_UTF8_IGNORE", + )); + } + } + } + Ok(PhpType::Bool) +} + +/// Lowers a `json_validate` call by dispatching to the shared JSON emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::json::lower_json_validate(ctx, inst) +} diff --git a/src/builtins/system/localtime.rs b/src/builtins/system/localtime.rs new file mode 100644 index 0000000000..96ec5075c2 --- /dev/null +++ b/src/builtins/system/localtime.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `localtime` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `localtime` is a pure-data builtin whose return type +//! (`Mixed`) is fully determined by its declaration. Both parameters are optional: +//! `timestamp` defaults to -1 (current time) and `associative` defaults to `false`. + +use crate::builtins::spec::DefaultSpec; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "localtime", + area: System, + params: [timestamp: Int = DefaultSpec::Int(-1), associative: Bool = DefaultSpec::Bool(false)], + returns: Mixed, + lower: lower, + summary: "Returns the local time.", +} + +/// Lowers a `localtime` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_localtime(ctx, inst) +} diff --git a/src/builtins/system/microtime.rs b/src/builtins/system/microtime.rs new file mode 100644 index 0000000000..aad430ecdd --- /dev/null +++ b/src/builtins/system/microtime.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Home of the PHP `microtime` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` inspects the literal value of the `as_float` argument to refine the return +//! type: `true` → `Float`, `false` → `Str`, non-literal → `Union(Str, Float)`. +//! The registry's common path pre-infers arguments; the hook must not call `infer_type`. +//! - `lower` is a thin wrapper over the shared `system::lower_microtime` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "microtime", + area: System, + params: [as_float: Bool = DefaultSpec::Bool(false)], + arity_error: "microtime() takes 0 or 1 arguments", + returns: Mixed, + check: check, + lower: lower, + summary: "Returns the current Unix timestamp with microseconds.", +} + +/// Refines the return type of `microtime` based on the literal value of `as_float`. +/// +/// Returns `Float` when `as_float` is the literal `true`, `Str` when it is the literal +/// `false` or absent, and `Union(Str, Float)` for any non-literal expression. +/// The registry pre-infers arguments, so this hook must not call `infer_type`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(match cx.args.first() { + Some(arg) => match &arg.kind { + ExprKind::BoolLiteral(true) => PhpType::Float, + ExprKind::BoolLiteral(false) => PhpType::Str, + _ => cx.checker.normalize_union_type(vec![PhpType::Str, PhpType::Float]), + }, + None => PhpType::Str, + }) +} + +/// Lowers a `microtime` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_microtime(ctx, inst) +} diff --git a/src/builtins/system/mktime.rs b/src/builtins/system/mktime.rs new file mode 100644 index 0000000000..600bb54131 --- /dev/null +++ b/src/builtins/system/mktime.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `mktime` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `mktime` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "mktime", + area: System, + params: [hour: Int, minute: Int, second: Int, month: Int, day: Int, year: Int], + returns: Int, + lower: lower, + summary: "Returns the Unix timestamp for a date.", +} + +/// Lowers a `mktime` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_mktime(ctx, inst) +} diff --git a/src/builtins/system/mod.rs b/src/builtins/system/mod.rs new file mode 100644 index 0000000000..27aa1653e1 --- /dev/null +++ b/src/builtins/system/mod.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Groups all `system`-area time/date/sleep/env/process/output/define/json/serialize builtin homes +//! into this module so the registry can collect them in one place. Each submodule +//! declares exactly one builtin via `builtin!` and provides its lowering hook (and +//! optional check hook). +//! +//! Called from: +//! - `crate::builtins` (`mod system;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - Pure-data builtins (no check hook): time, sleep, usleep, checkdate, date, gmdate, +//! mktime, gmmktime, hrtime, getdate, localtime, date_default_timezone_get/set, +//! __elephc_mktime_raw, __elephc_gmmktime_raw, __elephc_strtotime_raw, +//! putenv, http_response_code, header, phpversion, exec, shell_exec, system, passthru, +//! json_last_error, json_last_error_msg, serialize, preg_match_all, preg_replace. +//! - Check-hook builtins: microtime (literal-dependent return type), strtotime +//! (returns Union(Int, Bool)), getenv (returns Union(Str, Bool)), php_uname (validates +//! arg type), define (side-effect: registers constant type), defined (validates literal), +//! class_attribute_names/class_attribute_args/class_get_attributes (compile-time reflection), +//! json_encode, json_decode, json_validate, unserialize, preg_match (by-ref `$matches` +//! variable check), preg_split (element type refined by arg count). +//! - `attr_support` holds shared helpers for the class-attribute builtins. +//! - `json_support` holds shared helpers for the JSON/serialize check hooks. +//! - Add `pub mod ;` here for every new system builtin home. + +pub mod __elephc_gmmktime_raw; +pub mod __elephc_mktime_raw; +pub mod __elephc_strtotime_raw; +pub mod attr_support; +pub mod checkdate; +pub mod class_attribute_args; +pub mod class_attribute_names; +pub mod class_get_attributes; +pub mod date; +pub mod date_default_timezone_get; +pub mod date_default_timezone_set; +pub mod define; +pub mod defined; +pub mod exec; +pub mod getdate; +pub mod getenv; +pub mod gmdate; +pub mod gmmktime; +pub mod header; +pub mod hrtime; +pub mod http_response_code; +pub mod json_decode; +pub mod json_encode; +pub mod json_last_error; +pub mod json_last_error_msg; +pub mod json_support; +pub mod json_validate; +pub mod localtime; +pub mod microtime; +pub mod mktime; +pub mod passthru; +pub mod php_uname; +pub mod phpversion; +pub mod preg_match; +pub mod preg_match_all; +pub mod preg_replace; +pub mod preg_split; +pub mod putenv; +pub mod serialize; +pub mod shell_exec; +pub mod sleep; +pub mod strtotime; +pub mod system; +pub mod time; +pub mod unserialize; +pub mod usleep; diff --git a/src/builtins/system/passthru.rs b/src/builtins/system/passthru.rs new file mode 100644 index 0000000000..21f6b0f51f --- /dev/null +++ b/src/builtins/system/passthru.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `passthru` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Void`) is fully determined by the declaration. +//! - `lower` is a thin wrapper over `system::lower_passthru` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "passthru", + area: System, + params: [command: Str], + returns: Void, + lower: lower, + summary: "Executes an external program and passes its output directly.", +} + +/// Lowers a `passthru` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_passthru(ctx, inst) +} diff --git a/src/builtins/system/php_uname.rs b/src/builtins/system/php_uname.rs new file mode 100644 index 0000000000..120a3a9fb8 --- /dev/null +++ b/src/builtins/system/php_uname.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Home of the PHP `php_uname` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` validates that the optional `mode` argument, when present, is a string type. +//! - `arity_error` overrides the default "takes at most 1 argument" message to match +//! the legacy phrasing "takes 0 or 1 arguments". +//! - `lower` is a thin wrapper over `system::lower_php_uname` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "php_uname", + area: System, + params: [mode: Str = DefaultSpec::Str("a")], + arity_error: "php_uname() takes 0 or 1 arguments", + returns: Str, + check: check, + lower: lower, + summary: "Returns information about the operating system PHP is running on.", +} + +/// Validates that the optional `mode` argument is a string when present. +/// +/// Returns `PhpType::Str` unconditionally; the error path fires when an argument +/// is provided but does not infer as `PhpType::Str`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + if let Some(arg) = cx.args.first() { + let ty = cx.checker.infer_type(arg, cx.env)?; + if ty != PhpType::Str { + return Err(CompileError::new( + cx.span, + "php_uname() argument must be string", + )); + } + } + Ok(PhpType::Str) +} + +/// Lowers a `php_uname` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_php_uname(ctx, inst) +} diff --git a/src/builtins/system/phpversion.rs b/src/builtins/system/phpversion.rs new file mode 100644 index 0000000000..a396ba29fc --- /dev/null +++ b/src/builtins/system/phpversion.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `phpversion` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with zero parameters: return type (`Str`) is fully determined +//! by the declaration. elephc returns the compiler package version string. +//! - `lower` delegates to the module-level `lower_phpversion` in +//! `src/codegen/lower_inst/builtins.rs`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "phpversion", + area: System, + params: [], + returns: Str, + lower: lower, + summary: "Returns the current PHP / elephc compiler version string.", +} + +/// Lowers a `phpversion` call by delegating to the shared module-level emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_phpversion(ctx, inst) +} diff --git a/src/builtins/system/preg_match.rs b/src/builtins/system/preg_match.rs new file mode 100644 index 0000000000..a4be70e634 --- /dev/null +++ b/src/builtins/system/preg_match.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Home of the PHP `preg_match` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The third param `matches` is by-reference (`ref matches: Mixed = DefaultSpec::EmptyArray`), +//! matching the golden signature where `ref_params[2] = true`. +//! - `lazy_check: true` suppresses the registry's default pre-inference loop so the hook +//! can infer args[0] and args[1] (pattern and subject) while deliberately skipping +//! inference of args[2] (`$matches`). `$matches` is a write-only output parameter; +//! it is not declared before the call and inferring it would produce an +//! "Undefined variable" error. +//! - `check` validates that args[2] (when present) is a `Variable` expression; passing +//! a non-variable to the by-ref `$matches` param is a compile error. +//! - `lower` is a thin wrapper over `regex::lower_preg_match` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "preg_match", + area: System, + params: [pattern: Str, subject: Str, ref matches: Mixed = DefaultSpec::EmptyArray], + returns: Int, + check: check, + lazy_check: true, + lower: lower, + summary: "Performs a regular expression match.", +} + +/// Validates that `$matches`, when supplied, is a variable expression. +/// +/// Infers args[0] (pattern) and args[1] (subject) to trigger type-environment side +/// effects, but deliberately skips inference of args[2] (`$matches`) because it is a +/// write-only output parameter that is undefined before the call. Passing a non-variable +/// (such as a literal or function call) to `$matches` is a compile-time error. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + cx.checker.infer_type(&cx.args[1], cx.env)?; + if cx.args.len() == 3 && !matches!(cx.args[2].kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + cx.args[2].span, + "preg_match() parameter $matches must be passed a variable", + )); + } + Ok(PhpType::Int) +} + +/// Lowers a `preg_match` call by dispatching to the shared regex emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::regex::lower_preg_match(ctx, inst) +} diff --git a/src/builtins/system/preg_match_all.rs b/src/builtins/system/preg_match_all.rs new file mode 100644 index 0000000000..095025abc7 --- /dev/null +++ b/src/builtins/system/preg_match_all.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `preg_match_all` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Int`) is fully determined by the declaration. +//! - `lower` is a thin wrapper over `regex::lower_preg_match_all` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "preg_match_all", + area: System, + params: [pattern: Str, subject: Str], + returns: Int, + lower: lower, + summary: "Performs a global regular expression match and returns the number of matches.", +} + +/// Lowers a `preg_match_all` call by dispatching to the shared regex emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::regex::lower_preg_match_all(ctx, inst) +} diff --git a/src/builtins/system/preg_replace.rs b/src/builtins/system/preg_replace.rs new file mode 100644 index 0000000000..a079b12f3e --- /dev/null +++ b/src/builtins/system/preg_replace.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `preg_replace` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Str`) is fully determined by the declaration. +//! - `lower` is a thin wrapper over `regex::lower_preg_replace` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "preg_replace", + area: System, + params: [pattern: Str, replacement: Str, subject: Str], + returns: Str, + lower: lower, + summary: "Performs a regular expression search and replace.", +} + +/// Lowers a `preg_replace` call by dispatching to the shared regex emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::regex::lower_preg_replace(ctx, inst) +} diff --git a/src/builtins/system/preg_split.rs b/src/builtins/system/preg_split.rs new file mode 100644 index 0000000000..0095a11da5 --- /dev/null +++ b/src/builtins/system/preg_split.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Home of the PHP `preg_split` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - Return element type is `Mixed` when `flags` is supplied (4 args), `Str` otherwise. +//! - `arity_error` is overridden to preserve the legacy message "preg_split() takes between +//! 2 and 4 arguments" (the registry default for min=2/max=4 produces "2 to 4 arguments"). +//! - The registry pre-infers arguments before calling the hook; the hook must not +//! call `infer_type` again. +//! - `lower` is a thin wrapper over `regex::lower_preg_split` in the EIR backend. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "preg_split", + area: System, + params: [pattern: Str, subject: Str, limit: Int = DefaultSpec::Int(-1), flags: Int = DefaultSpec::Int(0)], + arity_error: "preg_split() takes between 2 and 4 arguments", + returns: Mixed, + check: check, + lower: lower, + summary: "Splits a string by a regular expression.", +} + +/// Returns the split result array type, refining the element type based on argument count. +/// +/// Returns `Array(Mixed)` when all four arguments are present (the `flags` argument +/// can cause mixed-type entries via `PREG_OFFSET_CAPTURE`), or `Array(Str)` for 2 or +/// 3 arguments. The registry pre-infers arguments; the hook must not call `infer_type`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let elem = if cx.args.len() >= 4 { PhpType::Mixed } else { PhpType::Str }; + Ok(PhpType::Array(Box::new(elem))) +} + +/// Lowers a `preg_split` call by dispatching to the shared regex emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::regex::lower_preg_split(ctx, inst) +} diff --git a/src/builtins/system/putenv.rs b/src/builtins/system/putenv.rs new file mode 100644 index 0000000000..c7ed1eeeb9 --- /dev/null +++ b/src/builtins/system/putenv.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `putenv` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Bool`) is fully determined by the declaration. +//! - `lower` is a thin wrapper over `system::lower_putenv` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "putenv", + area: System, + params: [assignment: Str], + returns: Bool, + lower: lower, + summary: "Sets an environment variable.", +} + +/// Lowers a `putenv` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_putenv(ctx, inst) +} diff --git a/src/builtins/system/serialize.rs b/src/builtins/system/serialize.rs new file mode 100644 index 0000000000..3c26baf4a7 --- /dev/null +++ b/src/builtins/system/serialize.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `serialize` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `serialize` is a pure-data builtin whose return type +//! (`Str`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "serialize", + area: System, + params: [value: Mixed], + returns: Str, + lower: lower, + summary: "Generates a storable representation of a value.", +} + +/// Lowers a `serialize` call by dispatching to the shared serialize emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::serialize::lower_serialize(ctx, inst) +} diff --git a/src/builtins/system/shell_exec.rs b/src/builtins/system/shell_exec.rs new file mode 100644 index 0000000000..f4ec712490 --- /dev/null +++ b/src/builtins/system/shell_exec.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `shell_exec` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Str`) is fully determined by the declaration. +//! - `lower` is a thin wrapper over `system::lower_shell_exec` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "shell_exec", + area: System, + params: [command: Str], + returns: Str, + lower: lower, + summary: "Executes a command via the shell and returns the complete output as a string.", +} + +/// Lowers a `shell_exec` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_shell_exec(ctx, inst) +} diff --git a/src/builtins/system/sleep.rs b/src/builtins/system/sleep.rs new file mode 100644 index 0000000000..26418338d5 --- /dev/null +++ b/src/builtins/system/sleep.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `sleep` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `sleep` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "sleep", + area: System, + params: [seconds: Int], + returns: Int, + lower: lower, + summary: "Delays execution for a number of seconds.", +} + +/// Lowers a `sleep` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_sleep(ctx, inst) +} diff --git a/src/builtins/system/strtotime.rs b/src/builtins/system/strtotime.rs new file mode 100644 index 0000000000..c893c747f2 --- /dev/null +++ b/src/builtins/system/strtotime.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Home of the PHP `strtotime` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - `check` always returns `Union(Int, Bool)` to reflect PHP's behaviour where +//! `strtotime` returns a Unix timestamp on success or `false` on failure. +//! - `lower` is a thin wrapper over the shared `system::lower_strtotime` emitter. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "strtotime", + area: System, + params: [datetime: Str, baseTimestamp: Int = DefaultSpec::Null], + returns: Mixed, + check: check, + lower: lower, + summary: "Parses an English textual datetime description into a Unix timestamp.", +} + +/// Returns `Union(Int, Bool)` to reflect that `strtotime` can return a timestamp or `false`. +/// +/// The registry pre-infers arguments before calling this hook. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::Bool])) +} + +/// Lowers a `strtotime` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_strtotime(ctx, inst) +} diff --git a/src/builtins/system/system.rs b/src/builtins/system/system.rs new file mode 100644 index 0000000000..1c6626e6d1 --- /dev/null +++ b/src/builtins/system/system.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `system` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin: return type (`Str`) is fully determined by the declaration. +//! - `lower` is a thin wrapper over `system::lower_system` in the EIR backend. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "system", + area: System, + params: [command: Str], + returns: Str, + lower: lower, + summary: "Executes an external program and displays the output.", +} + +/// Lowers a `system` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_system(ctx, inst) +} diff --git a/src/builtins/system/time.rs b/src/builtins/system/time.rs new file mode 100644 index 0000000000..88966b0d44 --- /dev/null +++ b/src/builtins/system/time.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `time` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `time` is a pure-data builtin whose return type +//! (`Int`) is fully determined by its declaration. The registry common path +//! infers the argument and enforces arity before falling back to `returns`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "time", + area: System, + params: [], + returns: Int, + lower: lower, + summary: "Returns the current Unix timestamp.", +} + +/// Lowers a `time` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_time(ctx, inst) +} diff --git a/src/builtins/system/unserialize.rs b/src/builtins/system/unserialize.rs new file mode 100644 index 0000000000..d298b96ad4 --- /dev/null +++ b/src/builtins/system/unserialize.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Home of the PHP `unserialize` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The check hook validates the data argument is string-compatible. +//! The optional options argument is accepted without type restriction. +//! Type errors are reported at the offending argument's span. +//! - `options` default is `DefaultSpec::EmptyArray` (matches legacy `ArrayLiteral([])` +//! for parity gate comparison). + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::builtins::system::json_support; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "unserialize", + area: System, + params: [ + data: Str, + options: Mixed = DefaultSpec::EmptyArray, + ], + returns: Mixed, + check: check, + lower: lower, + summary: "Creates a PHP value from a stored representation.", +} + +/// Validates that the data argument is string-compatible. +/// +/// The optional options argument is inferred but not type-restricted. +/// Reports type errors at the span of the offending argument, not the call span. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let data_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !json_support::is_json_string_arg_type(&data_ty) { + return Err(CompileError::new( + cx.args[0].span, + "unserialize() data argument must be string-compatible", + )); + } + if let Some(options) = cx.args.get(1) { + cx.checker.infer_type(options, cx.env)?; + } + Ok(PhpType::Mixed) +} + +/// Lowers an `unserialize` call by dispatching to the shared serialize emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::serialize::lower_unserialize(ctx, inst) +} diff --git a/src/builtins/system/usleep.rs b/src/builtins/system/usleep.rs new file mode 100644 index 0000000000..2363b46496 --- /dev/null +++ b/src/builtins/system/usleep.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `usleep` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), +//! both via `crate::builtins::registry`. +//! +//! Key details: +//! - No `check` hook is needed: `usleep` is a pure-data builtin whose return type +//! (`Void`) is fully determined by its declaration. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "usleep", + area: System, + params: [microseconds: Int], + returns: Void, + lower: lower, + summary: "Delays execution for a number of microseconds.", +} + +/// Lowers a `usleep` call by dispatching to the shared system emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::system::lower_usleep(ctx, inst) +} diff --git a/src/builtins/types/boolval.rs b/src/builtins/types/boolval.rs new file mode 100644 index 0000000000..fba169162f --- /dev/null +++ b/src/builtins/types/boolval.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `boolval` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared boolval emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "boolval", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Returns the boolean value of a variable.", + php_manual: "function.boolval", +} + +/// Lowers a `boolval` call by dispatching to the shared boolval emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_boolval(ctx, inst) +} diff --git a/src/builtins/types/floatval.rs b/src/builtins/types/floatval.rs new file mode 100644 index 0000000000..26848df945 --- /dev/null +++ b/src/builtins/types/floatval.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `floatval` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared floatval emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "floatval", + area: Types, + params: [value: Mixed], + returns: Float, + lower: lower, + summary: "Returns the float value of a variable.", + php_manual: "function.floatval", +} + +/// Lowers a `floatval` call by dispatching to the shared floatval emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_floatval(ctx, inst) +} diff --git a/src/builtins/types/get_resource_id.rs b/src/builtins/types/get_resource_id.rs new file mode 100644 index 0000000000..43f59b9a2a --- /dev/null +++ b/src/builtins/types/get_resource_id.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `get_resource_id` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - The parameter is named `resource` (matching the PHP golden signature). +//! - `lower` is a thin wrapper over the EIR types-module resource-id emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "get_resource_id", + area: Types, + params: [resource: Mixed], + returns: Int, + lower: lower, + summary: "Returns an integer identifier for the given resource.", + php_manual: "function.get-resource-id", +} + +/// Lowers a `get_resource_id` call by dispatching to the EIR types-module emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_get_resource_id(ctx, inst) +} diff --git a/src/builtins/types/get_resource_type.rs b/src/builtins/types/get_resource_type.rs new file mode 100644 index 0000000000..5e12e88b6e --- /dev/null +++ b/src/builtins/types/get_resource_type.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `get_resource_type` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - The parameter is named `resource` (matching the PHP golden signature). +//! - `lower` is a thin wrapper over the EIR types-module resource-type emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "get_resource_type", + area: Types, + params: [resource: Mixed], + returns: Str, + lower: lower, + summary: "Returns the type of a resource.", + php_manual: "function.get-resource-type", +} + +/// Lowers a `get_resource_type` call by dispatching to the EIR types-module emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_get_resource_type(ctx, inst) +} diff --git a/src/builtins/types/gettype.rs b/src/builtins/types/gettype.rs new file mode 100644 index 0000000000..c29598f1a8 --- /dev/null +++ b/src/builtins/types/gettype.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `gettype` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared gettype emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "gettype", + area: Types, + params: [value: Mixed], + returns: Str, + lower: lower, + summary: "Returns the type of a variable as a string.", + php_manual: "function.gettype", +} + +/// Lowers a `gettype` call by dispatching to the shared gettype emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_gettype(ctx, inst) +} diff --git a/src/builtins/types/intval.rs b/src/builtins/types/intval.rs new file mode 100644 index 0000000000..a91e026a1e --- /dev/null +++ b/src/builtins/types/intval.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `intval` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - Declared with exactly one parameter `value` (no `base` param) matching the legacy golden signature. +//! - `lower` is a thin wrapper over the shared intval emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "intval", + area: Types, + params: [value: Mixed], + returns: Int, + lower: lower, + summary: "Returns the integer value of a variable.", + php_manual: "function.intval", +} + +/// Lowers an `intval` call by dispatching to the shared intval emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_intval(ctx, inst) +} diff --git a/src/builtins/types/is_array.rs b/src/builtins/types/is_array.rs new file mode 100644 index 0000000000..e3fb8f3aaf --- /dev/null +++ b/src/builtins/types/is_array.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `is_array` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared array-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_array", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is an array.", + php_manual: "function.is-array", +} + +/// Lowers an `is_array` call by dispatching to the shared array-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_is_array(ctx, inst) +} diff --git a/src/builtins/types/is_bool.rs b/src/builtins/types/is_bool.rs new file mode 100644 index 0000000000..e41732e6b7 --- /dev/null +++ b/src/builtins/types/is_bool.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `is_bool` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` dispatches to the shared static-type-predicate emitter with `PhpType::Bool`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "is_bool", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is a boolean.", + php_manual: "function.is-bool", +} + +/// Lowers an `is_bool` call by dispatching to the shared static-type-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_static_type_predicate( + ctx, + inst, + "is_bool", + PhpType::Bool, + ) +} diff --git a/src/builtins/types/is_callable.rs b/src/builtins/types/is_callable.rs new file mode 100644 index 0000000000..e092446e7b --- /dev/null +++ b/src/builtins/types/is_callable.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `is_callable` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared callable-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_callable", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable can be called as a function.", + php_manual: "function.is-callable", +} + +/// Lowers an `is_callable` call by dispatching to the shared callable-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_is_callable(ctx, inst) +} diff --git a/src/builtins/types/is_finite.rs b/src/builtins/types/is_finite.rs new file mode 100644 index 0000000000..23f9873eb3 --- /dev/null +++ b/src/builtins/types/is_finite.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `is_finite` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - The parameter is named `num` (matching the PHP golden signature), not `value`. +//! - `lower` is a thin wrapper over the EIR math-module finite-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_finite", + area: Types, + params: [num: Float], + returns: Bool, + lower: lower, + summary: "Checks whether a float is finite.", + php_manual: "function.is-finite", +} + +/// Lowers an `is_finite` call by dispatching to the EIR math-module finite-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_is_finite(ctx, inst) +} diff --git a/src/builtins/types/is_float.rs b/src/builtins/types/is_float.rs new file mode 100644 index 0000000000..fe35d5fe0b --- /dev/null +++ b/src/builtins/types/is_float.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `is_float` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` dispatches to the shared static-type-predicate emitter with `PhpType::Float`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "is_float", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is a floating-point number.", + php_manual: "function.is-float", +} + +/// Lowers an `is_float` call by dispatching to the shared static-type-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_static_type_predicate( + ctx, + inst, + "is_float", + PhpType::Float, + ) +} diff --git a/src/builtins/types/is_infinite.rs b/src/builtins/types/is_infinite.rs new file mode 100644 index 0000000000..77f6ef8c2b --- /dev/null +++ b/src/builtins/types/is_infinite.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `is_infinite` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - The parameter is named `num` (matching the PHP golden signature), not `value`. +//! - `lower` is a thin wrapper over the EIR math-module infinite-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_infinite", + area: Types, + params: [num: Float], + returns: Bool, + lower: lower, + summary: "Checks whether a float is infinite.", + php_manual: "function.is-infinite", +} + +/// Lowers an `is_infinite` call by dispatching to the EIR math-module infinite-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_is_infinite(ctx, inst) +} diff --git a/src/builtins/types/is_int.rs b/src/builtins/types/is_int.rs new file mode 100644 index 0000000000..87181a2a7c --- /dev/null +++ b/src/builtins/types/is_int.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `is_int` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` dispatches to the shared static-type-predicate emitter with `PhpType::Int`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "is_int", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is an integer.", + php_manual: "function.is-int", +} + +/// Lowers an `is_int` call by dispatching to the shared static-type-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_static_type_predicate( + ctx, + inst, + "is_int", + PhpType::Int, + ) +} diff --git a/src/builtins/types/is_iterable.rs b/src/builtins/types/is_iterable.rs new file mode 100644 index 0000000000..e57ed34b18 --- /dev/null +++ b/src/builtins/types/is_iterable.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `is_iterable` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared iterable-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_iterable", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is iterable.", + php_manual: "function.is-iterable", +} + +/// Lowers an `is_iterable` call by dispatching to the shared iterable-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_is_iterable(ctx, inst) +} diff --git a/src/builtins/types/is_nan.rs b/src/builtins/types/is_nan.rs new file mode 100644 index 0000000000..7f06b88e42 --- /dev/null +++ b/src/builtins/types/is_nan.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the PHP `is_nan` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - The parameter is named `num` (matching the PHP golden signature), not `value`. +//! - `lower` is a thin wrapper over the EIR math-module NaN-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_nan", + area: Types, + params: [num: Float], + returns: Bool, + lower: lower, + summary: "Checks whether a float is NAN.", + php_manual: "function.is-nan", +} + +/// Lowers an `is_nan` call by dispatching to the EIR math-module NaN-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::math::lower_is_nan(ctx, inst) +} diff --git a/src/builtins/types/is_null.rs b/src/builtins/types/is_null.rs new file mode 100644 index 0000000000..f1364df441 --- /dev/null +++ b/src/builtins/types/is_null.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `is_null` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared null-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_null", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is null.", + php_manual: "function.is-null", +} + +/// Lowers an `is_null` call by dispatching to the shared null-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_is_null_builtin(ctx, inst) +} diff --git a/src/builtins/types/is_numeric.rs b/src/builtins/types/is_numeric.rs new file mode 100644 index 0000000000..507f269732 --- /dev/null +++ b/src/builtins/types/is_numeric.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `is_numeric` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the EIR is_numeric-module emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_numeric", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is a number or a numeric string.", + php_manual: "function.is-numeric", +} + +/// Lowers an `is_numeric` call by dispatching to the EIR is_numeric-module emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::is_numeric::lower_is_numeric(ctx, inst) +} diff --git a/src/builtins/types/is_object.rs b/src/builtins/types/is_object.rs new file mode 100644 index 0000000000..9a716f193b --- /dev/null +++ b/src/builtins/types/is_object.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `is_object` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared object-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_object", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is an object.", + php_manual: "function.is-object", +} + +/// Lowers an `is_object` call by dispatching to the shared object-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_is_object(ctx, inst) +} diff --git a/src/builtins/types/is_resource.rs b/src/builtins/types/is_resource.rs new file mode 100644 index 0000000000..ede43d6dc7 --- /dev/null +++ b/src/builtins/types/is_resource.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `is_resource` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the EIR types-module resource-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_resource", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is a resource.", + php_manual: "function.is-resource", +} + +/// Lowers an `is_resource` call by dispatching to the EIR types-module resource-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_is_resource(ctx, inst) +} diff --git a/src/builtins/types/is_scalar.rs b/src/builtins/types/is_scalar.rs new file mode 100644 index 0000000000..5fd78d3caa --- /dev/null +++ b/src/builtins/types/is_scalar.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `is_scalar` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` is a thin wrapper over the shared scalar-predicate emitter. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; + +builtin! { + name: "is_scalar", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is a scalar.", + php_manual: "function.is-scalar", +} + +/// Lowers an `is_scalar` call by dispatching to the shared scalar-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_is_scalar(ctx, inst) +} diff --git a/src/builtins/types/is_string.rs b/src/builtins/types/is_string.rs new file mode 100644 index 0000000000..d2996ce906 --- /dev/null +++ b/src/builtins/types/is_string.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `is_string` builtin: its declaration and lowering. +//! +//! Called from: +//! - The builtin registry (declaration) and the EIR backend (lower hook), via `crate::builtins::registry`. +//! +//! Key details: +//! - Pure-data builtin with no check hook; arity and arg inference are handled by the registry common path. +//! - `lower` dispatches to the shared static-type-predicate emitter with `PhpType::Str`. + +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "is_string", + area: Types, + params: [value: Mixed], + returns: Bool, + lower: lower, + summary: "Checks whether a variable is a string.", + php_manual: "function.is-string", +} + +/// Lowers an `is_string` call by dispatching to the shared static-type-predicate emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::lower_static_type_predicate( + ctx, + inst, + "is_string", + PhpType::Str, + ) +} diff --git a/src/builtins/types/mod.rs b/src/builtins/types/mod.rs new file mode 100644 index 0000000000..638d9a66b2 --- /dev/null +++ b/src/builtins/types/mod.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Groups all `types`-area builtin homes into this module so the registry can +//! collect them in one place. Each submodule declares exactly one builtin via +//! `builtin!` and provides its lowering hook. +//! +//! Called from: +//! - `crate::builtins` (`mod types;` in `src/builtins/mod.rs`). +//! +//! Key details: +//! - Add `pub mod ;` here for every new types builtin home. +//! - Pure-data builtins (no `check` hook) rely on the registry common path to +//! infer each argument and enforce arity before falling back to the declared +//! `returns` type. +//! - Builtins with validation logic (`settype`) use `lazy_check: true` so the +//! check hook controls argument inference order. + +pub mod boolval; +pub mod floatval; +pub mod get_resource_id; +pub mod get_resource_type; +pub mod gettype; +pub mod intval; +pub mod is_array; +pub mod is_bool; +pub mod is_callable; +pub mod is_finite; +pub mod is_float; +pub mod is_infinite; +pub mod is_int; +pub mod is_iterable; +pub mod is_nan; +pub mod is_null; +pub mod is_numeric; +pub mod is_object; +pub mod is_resource; +pub mod is_scalar; +pub mod is_string; +pub mod settype; diff --git a/src/builtins/types/settype.rs b/src/builtins/types/settype.rs new file mode 100644 index 0000000000..a682040303 --- /dev/null +++ b/src/builtins/types/settype.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Home of the PHP `settype` builtin: its declaration, type-check hook, and lowering. +//! +//! Called from: +//! - The builtin registry (declaration), the type checker (check hook), and the EIR +//! backend (lower hook), all via `crate::builtins::registry`. +//! +//! Key details: +//! - The first parameter `var` is passed by reference (mutating builtin); `ref_params[0]` +//! is set by the `ref` marker in the `builtin!` declaration. +//! - `lazy_check: true` so the check hook controls argument inference order: it infers +//! `var` then `type` in source order (once each), matching legacy exactly-once inference. +//! - `check` validates that the second argument is a string and returns `Bool`. +//! - `lower` is a thin wrapper over the EIR types-module settype emitter. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::codegen::context::FunctionContext; +use crate::codegen::CodegenIrError; +use crate::errors::CompileError; +use crate::ir::Instruction; +use crate::types::PhpType; + +builtin! { + name: "settype", + area: Types, + params: [ref var: Mixed, type: Str], + returns: Bool, + check: check, + lazy_check: true, + lower: lower, + summary: "Sets the type of a variable.", + php_manual: "function.settype", +} + +/// Validates the `settype` arguments: infers both in source order and rejects a non-string type. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let ty = cx.checker.infer_type(&cx.args[1], cx.env)?; + if ty != PhpType::Str { + return Err(CompileError::new( + cx.span, + "settype() second argument must be a string", + )); + } + Ok(PhpType::Bool) +} + +/// Lowers a `settype` call by dispatching to the EIR types-module emitter. +fn lower(ctx: &mut FunctionContext, inst: &Instruction) -> Result<(), CodegenIrError> { + crate::codegen::lower_inst::builtins::types::lower_settype(ctx, inst) +} diff --git a/src/cli.rs b/src/cli.rs index c7bb14d401..6b6519e232 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -15,14 +15,7 @@ pub(crate) use crate::codegen::Emit; use crate::codegen::platform::Target; /// Usage string printed to stderr when command-line arguments are invalid or missing. -pub(crate) const USAGE: &str = "Usage: elephc [--target TARGET] [--heap-size=BYTES] [--gc-stats] [--heap-debug] [--emit-ir] [--ir-backend] [--ast-backend] [--emit-asm] [--emit KIND] [--check] [--null-repr=sentinel|tagged] [--regalloc=linear|stack] [--ir-opt=on|off] [--timings] [--source-map] [--define SYMBOL] [--link LIB|-lLIB] [--link-path DIR|-LDIR] [--framework NAME] [--web] "; - -/// Backend selected for assembly generation after frontend and optimization passes. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum CodegenBackend { - Eir, - Ast, -} +pub(crate) const USAGE: &str = "Usage: elephc [--target TARGET] [--heap-size=BYTES] [--gc-stats] [--heap-debug] [--emit-ir] [--emit-asm] [--emit KIND] [--check] [--null-repr=sentinel|tagged] [--regalloc=linear|stack] [--ir-opt=on|off] [--timings] [--source-map] [--define SYMBOL] [--link LIB|-lLIB] [--link-path DIR|-LDIR] [--framework NAME] [--web] [--with-CRATE] "; /// Configuration derived from command-line arguments, passed to the compile pipeline. /// Controls heap allocation size, debug output, code generation options, and linking behavior. @@ -32,7 +25,6 @@ pub(crate) struct CliConfig { pub(crate) gc_stats: bool, pub(crate) heap_debug: bool, pub(crate) emit_ir: bool, - pub(crate) backend: CodegenBackend, pub(crate) null_repr: crate::codegen::NullRepr, pub(crate) emit_asm: bool, pub(crate) emit: Emit, @@ -47,6 +39,12 @@ pub(crate) struct CliConfig { pub(crate) extra_frameworks: Vec, pub(crate) defines: HashSet, pub(crate) web: bool, + /// Bridge crates the user force-enabled with `--with-` (short flag + /// names such as `"pdo"`). Each one force-links the matching staticlib and, + /// for crates with a PHP-surface prelude, forces that prelude's injection so + /// the API is available even when feature auto-detection would not trigger. + /// `--with-web` is folded into `web` instead, since it aliases `--web`. + pub(crate) with_crates: HashSet, } /// Parse command-line arguments into a CliConfig struct. @@ -60,9 +58,6 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { let mut gc_stats = false; let mut heap_debug = false; let mut emit_ir = false; - let mut backend = CodegenBackend::Eir; - let mut explicit_ir_backend = false; - let mut explicit_ast_backend = false; let mut emit_asm = false; let mut emit = Emit::Executable; let mut check_only = false; @@ -75,6 +70,7 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { let mut extra_frameworks: Vec = Vec::new(); let mut defines: HashSet = HashSet::new(); let mut web = false; + let mut with_crates: HashSet = HashSet::new(); let mut null_repr = match std::env::var("ELEPHC_NULL_REPR").as_deref() { Ok("tagged") => crate::codegen::NullRepr::Tagged, Ok("sentinel") => crate::codegen::NullRepr::Sentinel, @@ -111,12 +107,6 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { heap_debug = true; } else if arg == "--emit-ir" { emit_ir = true; - } else if arg == "--ir-backend" { - explicit_ir_backend = true; - backend = CodegenBackend::Eir; - } else if arg == "--ast-backend" { - explicit_ast_backend = true; - backend = CodegenBackend::Ast; } else if arg == "--emit-asm" { emit_asm = true; } else if arg == "--emit" { @@ -173,6 +163,22 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { )); } else if arg == "--web" { web = true; + } else if let Some(name) = arg.strip_prefix("--with-") { + // `--with-web` aliases the full `--web` mode (it owns the program + // entry point); every other known crate is recorded for force-link + // and prelude forcing. An unknown crate name is a hard error so a + // typo never silently no-ops. + if name == "web" { + web = true; + } else if crate::linker::bridge_lib_for_flag(name).is_some() { + with_crates.insert(name.to_string()); + } else { + fail(&format!( + "Unknown crate for --with-{}: expected one of: {}", + name, + crate::linker::crate_flag_names().join(", ") + )); + } } else if arg.starts_with("--") { fail(&format!("Unknown flag: {}", arg)); } else { @@ -192,14 +198,6 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { if output_modes > 1 { fail("--emit-ir, --emit-asm, and --check are mutually exclusive"); } - if explicit_ir_backend && explicit_ast_backend { - fail("cannot use --ir-backend and --ast-backend together"); - } - if explicit_ast_backend { - eprintln!( - "warning: --ast-backend is deprecated and will be removed in v0.26.0. The EIR backend is now the default. See docs/internals/the-ir.md for details." - ); - } if web && check_only { fail("--web cannot be combined with --check"); } @@ -219,7 +217,6 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { gc_stats, heap_debug, emit_ir, - backend, null_repr, emit_asm, emit, @@ -234,6 +231,7 @@ pub(crate) fn parse_args(args: &[String]) -> CliConfig { extra_frameworks, defines, web, + with_crates, } } @@ -401,4 +399,46 @@ mod tests { let config = parse_args(&args); assert!(!config.web); } + + /// Verifies `--with-pdo` records the crate for force-link/prelude forcing + /// without touching the web mode. + #[test] + fn with_pdo_records_forced_crate() { + let args = vec!["elephc".into(), "--with-pdo".into(), "app.php".into()]; + let config = parse_args(&args); + assert!(config.with_crates.contains("pdo")); + assert!(!config.web); + } + + /// Verifies multiple `--with-` flags accumulate into the forced set. + #[test] + fn multiple_with_crates_accumulate() { + let args = vec![ + "elephc".into(), + "--with-pdo".into(), + "--with-tls".into(), + "app.php".into(), + ]; + let config = parse_args(&args); + assert!(config.with_crates.contains("pdo")); + assert!(config.with_crates.contains("tls")); + } + + /// Verifies `--with-web` aliases `--web` (full web mode) instead of being + /// recorded as a plain force-link crate, since elephc_web owns the entry point. + #[test] + fn with_web_aliases_web_mode() { + let args = vec!["elephc".into(), "--with-web".into(), "app.php".into()]; + let config = parse_args(&args); + assert!(config.web); + assert!(config.with_crates.is_empty()); + } + + /// Verifies the default has no forced crates so non-`--with` builds are unaffected. + #[test] + fn no_with_flag_defaults_empty() { + let args = vec!["elephc".into(), "app.php".into()]; + let config = parse_args(&args); + assert!(config.with_crates.is_empty()); + } } diff --git a/src/codegen/abi/bootstrap.rs b/src/codegen/abi/bootstrap.rs deleted file mode 100644 index ccf67fbd17..0000000000 --- a/src/codegen/abi/bootstrap.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Purpose: -//! Emits process bootstrap snippets that move OS-provided values into compiler-managed locations. -//! Provides small target-aware helpers for heap debug setup, frame copying, and process exit. -//! -//! Called from: -//! - `crate::codegen::main_emission` and top-level program prologue emission -//! -//! Key details: -//! - Register choices must match the platform entry convention before normal PHP frame setup begins. - -use crate::codegen::{emit::Emitter, platform::Arch}; - -use super::{ - emit_load_int_immediate, emit_store_reg_to_symbol, - process_argc_reg, - process_argv_reg, - temp_int_reg, -}; - -/// Store OS-provided argc and argv into global symbols. -pub fn emit_store_process_args_to_globals(emitter: &mut Emitter) { - emit_store_reg_to_symbol(emitter, process_argc_reg(emitter.target), "_global_argc", 0); - emit_store_reg_to_symbol(emitter, process_argv_reg(emitter.target), "_global_argv", 0); -} - -/// Set the heap debug flag to 1 in global symbol storage. -pub fn emit_enable_heap_debug_flag(emitter: &mut Emitter) { - let scratch = temp_int_reg(emitter.target); - emit_load_int_immediate(emitter, scratch, 1); - emit_store_reg_to_symbol(emitter, scratch, "_heap_debug_enabled", 0); -} - -/// Copy the current frame pointer into the destination scratch register. -pub fn emit_copy_frame_pointer(emitter: &mut Emitter, dest: &str) { - emitter.instruction(&format!("mov {}, {}", dest, super::registers::frame_pointer_reg(emitter))); // copy the current frame pointer into the requested scratch register -} - -/// Emit a process-exit sequence for the current target, then return control to the OS. -/// -/// # Arguments -/// - `code`: the exit code visible to the OS; must fit in the target's exit register. -/// -/// # Platform behavior -/// - **macOS ARM64 / Linux ARM64**: loads `code` into `x0` and invokes syscall 1 (`sys_exit`). -/// - **Linux x86_64**: loads `code` into `edi` (SysV first-argument register) and invokes syscall 60 (`exit`). -/// - **macOS x86_64**: panics — not yet implemented. -/// -/// This routine never returns to the calling code. The syscall consumes the current execution context. -pub fn emit_exit(emitter: &mut Emitter, code: u32) { - match (emitter.target.platform, emitter.target.arch) { - (super::super::platform::Platform::MacOS, Arch::AArch64) - | (super::super::platform::Platform::Linux, Arch::AArch64) => { - emitter.instruction(&format!("mov x0, #{}", code)); // load the requested process exit code into the ABI return register - emitter.syscall(1); - } - (super::super::platform::Platform::Linux, Arch::X86_64) => { - emitter.instruction(&format!("mov edi, {}", code)); // load the requested process exit code into the SysV first-argument register - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate the process through the Linux x86_64 syscall ABI - } - (super::super::platform::Platform::MacOS, Arch::X86_64) => { - panic!("process exit emission is not implemented yet for target macos-x86_64"); - } - } -} - -/// Emit a process-exit sequence that uses the integer result register as the exit code. -/// -/// Unlike `emit_exit`, which takes a constant, this routine exits with whatever -/// value a preceding call left in the target's integer result register (`x0` / -/// `rax`). Used by the `--web` process-entry stub to surface `elephc_web_run`'s -/// return value as the process exit code. -/// -/// # Platform behavior -/// - **macOS ARM64 / Linux ARM64**: the return value already sits in `x0`, which -/// is `sys_exit`'s argument register, so it invokes syscall 1 directly. -/// - **Linux x86_64**: moves `eax` (the C return value) into `edi` (the SysV exit -/// argument) and invokes syscall 60 (`exit`). -/// - **macOS x86_64**: panics — not in the supported target matrix. -/// -/// This routine never returns to the calling code. -pub fn emit_exit_with_result_reg(emitter: &mut Emitter) { - match (emitter.target.platform, emitter.target.arch) { - (super::super::platform::Platform::MacOS, Arch::AArch64) - | (super::super::platform::Platform::Linux, Arch::AArch64) => { - emitter.syscall(1); - } - (super::super::platform::Platform::Linux, Arch::X86_64) => { - emitter.instruction("mov edi, eax"); // move the C return value into the SysV exit argument register - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate the process with the bridge return code - } - (super::super::platform::Platform::MacOS, Arch::X86_64) => { - panic!("process exit emission is not implemented yet for target macos-x86_64"); - } - } -} diff --git a/src/codegen/abi/calls/incoming.rs b/src/codegen/abi/calls/incoming.rs deleted file mode 100644 index f420a77e61..0000000000 --- a/src/codegen/abi/calls/incoming.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! Purpose: -//! Stores function entry parameters from ABI registers or caller stack into compiler local slots. -//! Handles scalar, float, string-pair, and aggregate parameter shapes for each target. -//! -//! Called from: -//! - `crate::codegen::functions` during function and wrapper prologue emission -//! -//! Key details: -//! - Incoming cursor state must match outgoing assignment rules or calls will corrupt frame slots. - -use crate::codegen::{ - emit::Emitter, - platform::Arch, -}; -use crate::types::PhpType; - -use super::super::frame::{load_from_caller_stack, store_at_offset}; -use super::super::registers::{ - IncomingArgCursor, float_arg_reg_limit, float_arg_reg_name, int_arg_reg_limit, - int_arg_reg_name, secondary_scratch_reg, tertiary_scratch_reg, -}; - -/// Stores a function parameter from the next available ABI register or caller stack slot -/// into the local frame slot at `offset`. -/// -/// Uses `cursor` to track the current position in the integer/float register file and the -/// caller stack. Advances the cursor past the consumed register(s) or stack slot. For string -/// parameters, consumes two consecutive integer registers (pointer + length). The `ty` is -/// the codegen representation which determines routing to register or stack. Returns no -/// value but mutates `cursor` to reflect the advance. -pub fn emit_store_incoming_param( - emitter: &mut Emitter, - name: &str, - ty: &PhpType, - offset: usize, - is_ref: bool, - cursor: &mut IncomingArgCursor, -) { - let ty = ty.codegen_repr(); - let float_spill_reg = match emitter.target.arch { - Arch::AArch64 => "d15", - Arch::X86_64 => "xmm15", - }; - let int_spill_reg = secondary_scratch_reg(emitter); - let int_hi_spill_reg = tertiary_scratch_reg(emitter); - let int_reg_limit = int_arg_reg_limit(emitter.target); - let float_reg_limit = float_arg_reg_limit(emitter.target); - - if is_ref { - if !cursor.int_stack_only && cursor.int_reg_idx < int_reg_limit { - let reg = int_arg_reg_name(emitter.target, cursor.int_reg_idx); - emitter.comment(&format!("param &${} from {} (ref)", name, reg)); - store_at_offset(emitter, reg, offset); // save the by-reference address from the incoming integer argument register - cursor.int_reg_idx += 1; - } else { - emitter.comment(&format!( - "param &${} from caller stack +{}", - name, - cursor.caller_stack_offset - )); - load_from_caller_stack(emitter, int_spill_reg, cursor.caller_stack_offset); - store_at_offset(emitter, int_spill_reg, offset); // save the spilled by-reference address into the local param slot - cursor.caller_stack_offset += 16; - cursor.int_stack_only = true; - } - return; - } - - match ty { - PhpType::Bool | PhpType::Int | PhpType::Resource(_) => { - if !cursor.int_stack_only && cursor.int_reg_idx < int_reg_limit { - let reg = int_arg_reg_name(emitter.target, cursor.int_reg_idx); - emitter.comment(&format!("param ${} from {}", name, reg)); - store_at_offset(emitter, reg, offset); // save the scalar parameter from the incoming integer argument register - cursor.int_reg_idx += 1; - } else { - emitter.comment(&format!( - "param ${} from caller stack +{}", - name, - cursor.caller_stack_offset - )); - load_from_caller_stack(emitter, int_spill_reg, cursor.caller_stack_offset); - store_at_offset(emitter, int_spill_reg, offset); // save the spilled scalar parameter into the local param slot - cursor.caller_stack_offset += 16; - cursor.int_stack_only = true; - } - } - PhpType::Float => { - if !cursor.float_stack_only && cursor.float_reg_idx < float_reg_limit { - let reg = float_arg_reg_name(emitter.target, cursor.float_reg_idx); - emitter.comment(&format!("param ${} from {}", name, reg)); - store_at_offset(emitter, reg, offset); // save the float parameter from the incoming floating-point argument register - cursor.float_reg_idx += 1; - } else { - emitter.comment(&format!( - "param ${} from caller stack +{}", - name, - cursor.caller_stack_offset - )); - load_from_caller_stack(emitter, float_spill_reg, cursor.caller_stack_offset); - store_at_offset(emitter, float_spill_reg, offset); // save the spilled float parameter into the local param slot - cursor.caller_stack_offset += 16; - cursor.float_stack_only = true; - } - } - PhpType::Str => { - if !cursor.int_stack_only && cursor.int_reg_idx + 1 < int_reg_limit { - let ptr_reg = int_arg_reg_name(emitter.target, cursor.int_reg_idx); - let len_reg = int_arg_reg_name(emitter.target, cursor.int_reg_idx + 1); - emitter.comment(&format!( - "param ${} from {},{}", - name, ptr_reg, len_reg - )); - store_at_offset(emitter, ptr_reg, offset); // save the string pointer from the incoming integer-register pair - store_at_offset(emitter, len_reg, offset - 8); // save the string length from the incoming integer-register pair - cursor.int_reg_idx += 2; - } else { - emitter.comment(&format!( - "param ${} from caller stack +{}", - name, - cursor.caller_stack_offset - )); - load_from_caller_stack(emitter, int_spill_reg, cursor.caller_stack_offset); - load_from_caller_stack(emitter, int_hi_spill_reg, cursor.caller_stack_offset + 8); - store_at_offset(emitter, int_spill_reg, offset); // save the spilled string pointer into the local param slot - store_at_offset(emitter, int_hi_spill_reg, offset - 8); // save the spilled string length into the local param slot - cursor.caller_stack_offset += 16; - cursor.int_stack_only = true; - } - } - PhpType::TaggedScalar => { - if !cursor.int_stack_only && cursor.int_reg_idx + 1 < int_reg_limit { - let payload_reg = int_arg_reg_name(emitter.target, cursor.int_reg_idx); - let tag_reg = int_arg_reg_name(emitter.target, cursor.int_reg_idx + 1); - emitter.comment(&format!( - "param ${} from {},{}", - name, payload_reg, tag_reg - )); - store_at_offset(emitter, payload_reg, offset); // save the tagged scalar payload from the incoming integer-register pair - store_at_offset(emitter, tag_reg, offset - 8); // save the tagged scalar tag from the incoming integer-register pair - cursor.int_reg_idx += 2; - } else { - emitter.comment(&format!( - "param ${} from caller stack +{}", - name, - cursor.caller_stack_offset - )); - load_from_caller_stack(emitter, int_spill_reg, cursor.caller_stack_offset); - load_from_caller_stack(emitter, int_hi_spill_reg, cursor.caller_stack_offset + 8); - store_at_offset(emitter, int_spill_reg, offset); // save the spilled tagged scalar payload into the local param slot - store_at_offset(emitter, int_hi_spill_reg, offset - 8); // save the spilled tagged scalar tag into the local param slot - cursor.caller_stack_offset += 16; - cursor.int_stack_only = true; - } - } - PhpType::Void | PhpType::Never => {} - PhpType::Iterable - | PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - if !cursor.int_stack_only && cursor.int_reg_idx < int_reg_limit { - let reg = int_arg_reg_name(emitter.target, cursor.int_reg_idx); - emitter.comment(&format!("param ${} from {}", name, reg)); - store_at_offset(emitter, reg, offset); // save the pointer-like parameter from the incoming integer argument register - cursor.int_reg_idx += 1; - } else { - emitter.comment(&format!( - "param ${} from caller stack +{}", - name, - cursor.caller_stack_offset - )); - load_from_caller_stack(emitter, int_spill_reg, cursor.caller_stack_offset); - store_at_offset(emitter, int_spill_reg, offset); // save the spilled pointer-like parameter into the local param slot - cursor.caller_stack_offset += 16; - cursor.int_stack_only = true; - } - } - } -} diff --git a/src/codegen/abi/calls/invoke.rs b/src/codegen/abi/calls/invoke.rs deleted file mode 100644 index be655c1aa4..0000000000 --- a/src/codegen/abi/calls/invoke.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Purpose: -//! Emits direct and indirect call instructions for the active target architecture. -//! Keeps call-site syntax separate from argument materialization and result handling. -//! -//! Called from: -//! - `crate::codegen::expr::calls`, wrappers, and runtime-facing helper emitters -//! -//! Key details: -//! - The caller is responsible for ABI setup; these helpers only transfer control to a label or register. - -use crate::codegen::{emit::Emitter, platform::Arch}; - -/// Emits a direct call to a compile-time-known label. -/// The caller has already set up the ABI (stack frame, arguments, etc.). -/// This only transfers control to the callee. -pub fn emit_call_label(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("bl {}", label)); // branch-and-link to the named direct-call target - } - Arch::X86_64 => { - emitter.instruction(&format!("call {}", label)); // call the named direct-call target through the native x86_64 instruction - } - } -} - -/// Emits an indirect call through a register holding the callee address. -/// The callee is not known at compile time; the caller has already loaded -/// the target address into `reg` and set up the ABI. This only transfers control. -pub fn emit_call_reg(emitter: &mut Emitter, reg: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("blr {}", reg)); // branch to the indirect-call target held in the requested register - } - Arch::X86_64 => { - emitter.instruction(&format!("call {}", reg)); // call the indirect target held in the requested register - } - } -} diff --git a/src/codegen/abi/calls/mod.rs b/src/codegen/abi/calls/mod.rs deleted file mode 100644 index 31de1eb877..0000000000 --- a/src/codegen/abi/calls/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Purpose: -//! Groups ABI call helpers for incoming parameters, outgoing materialization, invocation, and stack scratch space. -//! Offers the call-lowering surface used by PHP, method, wrapper, and extern emitters. -//! -//! Called from: -//! - `crate::codegen::abi` and call expression emitters -//! -//! Key details: -//! - Source-order evaluation is handled above this layer; this module materializes already-planned ABI order. - -mod incoming; -mod invoke; -mod outgoing; -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 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, - emit_release_temporary_stack, emit_reserve_temporary_stack, emit_store_to_sp, - emit_temporary_stack_address, -}; diff --git a/src/codegen/abi/frame.rs b/src/codegen/abi/frame.rs deleted file mode 100644 index 58def70ac5..0000000000 --- a/src/codegen/abi/frame.rs +++ /dev/null @@ -1,483 +0,0 @@ -//! Purpose: -//! Owns stack-frame setup, teardown, frame-slot addressing, and generic memory loads/stores. -//! Provides target-specific helpers for local slots, caller stack access, and cleanup callbacks. -//! -//! Called from: -//! - `crate::codegen::functions`, `crate::codegen::main_emission`, and ABI call helpers -//! -//! Key details: -//! - Frame offsets and stack alignment are shared contracts with local collection and call materialization. - -use crate::codegen::{emit::Emitter, platform::Arch}; -use crate::types::PhpType; - -use super::registers::{ - float_result_reg, frame_pointer_reg, int_result_reg, is_float_register, string_result_regs, -}; - -/// Sets up the stack frame for a function body. -/// On AArch64: allocates `frame_size` bytes, saves x29/x30 in the footer, and establishes x29 as the frame pointer. -/// On x86_64: pushes rbp, establishes rsp as the frame base, and reserves `frame_size - 16` bytes for locals. -pub fn emit_frame_prologue(emitter: &mut Emitter, frame_size: usize) { - debug_assert!( - frame_size >= 16, - "frame_size must reserve the 16-byte frame footer (x29/x30), got {frame_size}" - ); - emitter.comment("prologue"); - match emitter.target.arch { - Arch::AArch64 => { - emit_adjust_sp(emitter, frame_size, true); - let footer_offset = frame_size - 16; - if footer_offset <= 504 { - emitter.instruction(&format!("stp x29, x30, [sp, #{}]", footer_offset)); // save frame pointer and return address in the fixed frame footer - } else { - emit_sp_address(emitter, "x9", footer_offset); - emitter.instruction("stp x29, x30, [x9]"); // save frame pointer and return address through the computed footer pointer - } - if footer_offset == 0 { - emitter.instruction("mov x29, sp"); // use the current stack pointer directly when the frame footer starts at sp - } else if footer_offset <= 4095 { - emitter.instruction(&format!("add x29, sp, #{}", footer_offset)); // point the frame pointer at the nearby fixed frame footer - } else { - emit_sp_address(emitter, "x29", footer_offset); - } - } - Arch::X86_64 => { - let local_bytes = frame_size.saturating_sub(16); - emitter.instruction("push rbp"); // save the caller frame pointer on the stack - emitter.instruction("mov rbp, rsp"); // establish the current stack pointer as the new frame base - if local_bytes > 0 { - emitter.instruction(&format!("sub rsp, {}", local_bytes)); // reserve aligned stack space for local slots below rbp - } - } - } -} - -/// Tears down the stack frame and restores the caller's frame state. -/// On AArch64: restores x29/x30 from the footer and releases `frame_size` bytes. -/// On x86_64: releases local bytes and pops rbp. -pub fn emit_frame_restore(emitter: &mut Emitter, frame_size: usize) { - debug_assert!( - frame_size >= 16, - "frame_size must reserve the 16-byte frame footer (x29/x30), got {frame_size}" - ); - match emitter.target.arch { - Arch::AArch64 => { - let footer_offset = frame_size - 16; - if footer_offset <= 504 { - emitter.instruction(&format!("ldp x29, x30, [sp, #{}]", footer_offset)); // restore frame pointer and return address from the fixed frame footer - } else { - emit_sp_address(emitter, "x9", footer_offset); - emitter.instruction("ldp x29, x30, [x9]"); // restore frame pointer and return address through the computed footer pointer - } - emit_adjust_sp(emitter, frame_size, false); - } - Arch::X86_64 => { - let local_bytes = frame_size.saturating_sub(16); - if local_bytes > 0 { - emitter.instruction(&format!("add rsp, {}", local_bytes)); // release the aligned local-slot area below rbp - } - emitter.instruction("pop rbp"); // restore the caller frame pointer from the stack - } - } -} - -/// Emits the function return sequence using the platform `ret` instruction. -pub fn emit_return(emitter: &mut Emitter) { - emitter.instruction("ret"); // return to the caller using the platform return instruction -} - -/// Sets up the stack frame for a cleanup callback (e.g., from destructor unwinding). -/// On AArch64: allocates 16 bytes of spill space and saves x29/x30. -/// On x86_64: pushes rbp and establishes `frame_base_reg` as the temporary frame pointer. -pub fn emit_cleanup_callback_prologue(emitter: &mut Emitter, frame_base_reg: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("sub sp, sp, #16"); // reserve spill space for the callback's saved frame state - emitter.instruction("stp x29, x30, [sp, #0]"); // save the callback caller's frame pointer and return address - emitter.instruction(&format!("mov x29, {}", frame_base_reg)); // treat the unwound frame base as the temporary frame pointer during cleanup - } - Arch::X86_64 => { - emitter.instruction("push rbp"); // preserve the callback caller frame pointer before rebasing cleanup - emitter.instruction(&format!("mov rbp, {}", frame_base_reg)); // treat the unwound frame base as the temporary cleanup frame pointer - } - } -} - -/// Tears down the cleanup callback frame and returns. -/// On AArch64: restores x29/x30 from the 16-byte spill area and releases it. -/// On x86_64: pops rbp. Both targets then emit the platform `ret` instruction. -pub fn emit_cleanup_callback_epilogue(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldp x29, x30, [sp, #0]"); // restore the callback caller's frame pointer and return address - emitter.instruction("add sp, sp, #16"); // release the callback spill space - } - Arch::X86_64 => { - emitter.instruction("pop rbp"); // restore the callback caller frame pointer after cleanup work - } - } - emit_return(emitter); -} - -/// Emits code that computes the address of a local frame slot and stores it in `dest`. -/// Uses the frame pointer (x29/rbp) as the base. Large offsets on AArch64 are walked down in -/// 4095-byte chunks to stay within immediate-add instructions. -pub fn emit_frame_slot_address(emitter: &mut Emitter, dest: &str, offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - if offset == 0 { - emitter.instruction(&format!("mov {}, x29", dest)); // copy the frame pointer when the requested slot is the frame base itself - } else if offset <= 4095 { - emitter.instruction(&format!("sub {}, x29, #{}", dest, offset)); // compute the local-slot address directly from the frame pointer - } else { - emitter.instruction(&format!("mov {}, x29", dest)); // seed the destination register from the frame pointer for a far local-slot address - let mut remaining = offset; - while remaining > 0 { - let chunk = remaining.min(4095); - emitter.instruction(&format!("sub {}, {}, #{}", dest, dest, chunk)); // walk the destination register down toward the distant local-slot address - remaining -= chunk; - } - } - } - Arch::X86_64 => { - if offset == 0 { - emitter.instruction(&format!("mov {}, {}", dest, frame_pointer_reg(emitter))); // copy rbp when the requested slot is the frame base itself - } else { - emitter.instruction(&format!("lea {}, [{} - {}]", dest, frame_pointer_reg(emitter), offset)); // materialize the local-slot address relative to rbp - } - } - } -} - -/// Stores `reg` into the local frame slot at `offset` from the frame pointer, using x9 as scratch. -/// On AArch64: uses `stur` for offsets ≤ 255, otherwise computes the address first. -/// On x86_64: stores via `[rbp - offset]` with a mov instruction; float registers use movsd. -pub fn store_at_offset(emitter: &mut Emitter, reg: &str, offset: usize) { - store_at_offset_scratch(emitter, reg, offset, "x9"); -} - -/// Stores `reg` into the local frame slot at `offset` from the frame pointer, using `scratch` as scratch. -/// This variant accepts a caller-specified scratch register to avoid conflicts in multi-register sequences. -pub fn store_at_offset_scratch(emitter: &mut Emitter, reg: &str, offset: usize, scratch: &str) { - match emitter.target.arch { - Arch::AArch64 => { - if offset <= 255 { - emitter.instruction(&format!("stur {}, [x29, #-{}]", reg, offset)); // store via unscaled immediate offset - } else { - emit_frame_slot_address(emitter, scratch, offset); - emitter.instruction(&format!("str {}, [{}]", reg, scratch)); // store via computed address - } - } - Arch::X86_64 => { - let slot = if offset == 0 { - format!("[{}]", frame_pointer_reg(emitter)) - } else { - format!("[{} - {}]", frame_pointer_reg(emitter), offset) - }; - if is_float_register(reg) { - emitter.instruction(&format!("movsd QWORD PTR {}, {}", slot, reg)); // store the floating-point payload into the local frame slot - } else { - emitter.instruction(&format!("mov QWORD PTR {}, {}", slot, reg)); // store the integer or pointer payload into the local frame slot - } - } - } -} - -/// Loads the local frame slot at `offset` from the frame pointer into `reg`, using x9 as scratch. -/// On AArch64: uses `ldur` for offsets ≤ 255, otherwise computes the address first. -/// On x86_64: loads via `[rbp - offset]` with a mov instruction; float registers use movsd. -pub fn load_at_offset(emitter: &mut Emitter, reg: &str, offset: usize) { - load_at_offset_scratch(emitter, reg, offset, "x9"); -} - -/// Loads the local frame slot at `offset` from the frame pointer into `reg`, using `scratch` as scratch. -/// This variant accepts a caller-specified scratch register to avoid conflicts in multi-register sequences. -pub fn load_at_offset_scratch(emitter: &mut Emitter, reg: &str, offset: usize, scratch: &str) { - match emitter.target.arch { - Arch::AArch64 => { - if offset <= 255 { - emitter.instruction(&format!("ldur {}, [x29, #-{}]", reg, offset)); // load via unscaled immediate offset - } else { - emit_frame_slot_address(emitter, scratch, offset); - emitter.instruction(&format!("ldr {}, [{}]", reg, scratch)); // load via computed address - } - } - Arch::X86_64 => { - let slot = if offset == 0 { - format!("[{}]", frame_pointer_reg(emitter)) - } else { - format!("[{} - {}]", frame_pointer_reg(emitter), offset) - }; - if is_float_register(reg) { - emitter.instruction(&format!("movsd {}, QWORD PTR {}", reg, slot)); // load the floating-point payload from the local frame slot - } else { - emitter.instruction(&format!("mov {}, QWORD PTR {}", reg, slot)); // load the integer or pointer payload from the local frame slot - } - } - } -} - -/// Emits a register-to-register move, choosing the move form by the register -/// classes of both operands. Handles same-class moves and moves between a -/// general-purpose and a floating-point register (a raw 64-bit bit copy, used -/// when a float value's payload must reach an integer register). Emits nothing -/// when source and destination are identical. -pub fn emit_reg_move(emitter: &mut Emitter, dst: &str, src: &str) { - if dst == src { - return; - } - let dst_float = is_float_register(dst); - let src_float = is_float_register(src); - match emitter.target.arch { - Arch::AArch64 => { - if dst_float || src_float { - emitter.instruction(&format!("fmov {}, {}", dst, src)); // move between FP and/or GP registers - } else { - emitter.instruction(&format!("mov {}, {}", dst, src)); // move integer/pointer register - } - } - Arch::X86_64 => { - if dst_float && src_float { - emitter.instruction(&format!("movsd {}, {}", dst, src)); // move scalar double between XMM registers - } else if dst_float || src_float { - emitter.instruction(&format!("movq {}, {}", dst, src)); // move 64-bit payload between GP and XMM - } else { - emitter.instruction(&format!("mov {}, {}", dst, src)); // move integer/pointer register - } - } - } -} - -/// Loads a value from an arbitrary address in memory into `reg`. -/// `addr_reg` holds the base address; `byte_offset` is added (AArch64 scaled immediate, x86_64 additive). -/// On x86_64, float registers use movsd; integers use mov. -pub fn emit_load_from_address(emitter: &mut Emitter, reg: &str, addr_reg: &str, byte_offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - if byte_offset == 0 { - emitter.instruction(&format!("ldr {}, [{}]", reg, addr_reg)); // load the requested value directly from the computed address register - } else { - emitter.instruction(&format!("ldr {}, [{}, #{}]", reg, addr_reg, byte_offset)); // load the requested value from the computed address register plus byte offset - } - } - Arch::X86_64 => { - let slot = if byte_offset == 0 { - format!("[{}]", addr_reg) - } else { - format!("[{} + {}]", addr_reg, byte_offset) - }; - if is_float_register(reg) { - emitter.instruction(&format!("movsd {}, QWORD PTR {}", reg, slot)); // load the floating-point payload through the computed address register - } else { - emitter.instruction(&format!("mov {}, QWORD PTR {}", reg, slot)); // load the integer or pointer payload through the computed address register - } - } - } -} - -/// Stores `reg` to an arbitrary address in memory. -/// `addr_reg` holds the base address; `byte_offset` is added (AArch64 scaled immediate, x86_64 additive). -/// On x86_64, float registers use movsd; integers use mov. -pub fn emit_store_to_address( - emitter: &mut Emitter, - reg: &str, - addr_reg: &str, - byte_offset: usize, -) { - match emitter.target.arch { - Arch::AArch64 => { - if byte_offset == 0 { - emitter.instruction(&format!("str {}, [{}]", reg, addr_reg)); // store the requested value directly through the computed address register - } else { - emitter.instruction(&format!("str {}, [{}, #{}]", reg, addr_reg, byte_offset)); // store the requested value through the computed address register plus byte offset - } - } - Arch::X86_64 => { - let slot = if byte_offset == 0 { - format!("[{}]", addr_reg) - } else { - format!("[{} + {}]", addr_reg, byte_offset) - }; - if is_float_register(reg) { - emitter.instruction(&format!("movsd QWORD PTR {}, {}", slot, reg)); // store the floating-point payload through the computed address register - } else { - emitter.instruction(&format!("mov QWORD PTR {}, {}", slot, reg)); // store the integer or pointer payload through the computed address register - } - } - } -} - -/// Stores zero to an arbitrary address in memory using the architectural zero register. -/// On AArch64 uses xzr; on x86_64 stores an explicit 0. `byte_offset` is added to `addr_reg`. -pub fn emit_store_zero_to_address(emitter: &mut Emitter, addr_reg: &str, byte_offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - if byte_offset == 0 { - emitter.instruction(&format!("str xzr, [{}]", addr_reg)); // store architectural zero directly through the computed address register - } else { - emitter.instruction(&format!("str xzr, [{}, #{}]", addr_reg, byte_offset)); // store architectural zero through the computed address register plus byte offset - } - } - Arch::X86_64 => { - let slot = if byte_offset == 0 { - format!("[{}]", addr_reg) - } else { - format!("[{} + {}]", addr_reg, byte_offset) - }; - emitter.instruction(&format!("mov QWORD PTR {}, 0", slot)); // store an integer zero through the computed address register - } - } -} - -/// Loads a spilled incoming call argument from the caller stack into `reg`. -/// On AArch64 uses the frame pointer (x29) as base with positive offset; large offsets are walked -/// through a scratch register in 4080-byte chunks. On x86_64 uses rbp with positive offset. -pub fn load_from_caller_stack(emitter: &mut Emitter, reg: &str, offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - if offset <= 4095 { - emitter.instruction(&format!("ldr {}, [x29, #{}]", reg, offset)); // load a spilled incoming argument from the caller stack - } else { - emitter.instruction("mov x9, x29"); // seed a scratch pointer from the current frame base - let mut remaining = offset; - while remaining > 0 { - let chunk = remaining.min(4080); - emitter.instruction(&format!("add x9, x9, #{}", chunk)); // advance the scratch pointer toward the distant caller-stack slot - remaining -= chunk; - } - emitter.instruction(&format!("ldr {}, [x9]", reg)); // load the spilled incoming argument through the computed caller-stack pointer - } - } - Arch::X86_64 => { - let slot = if offset == 0 { - format!("[{}]", frame_pointer_reg(emitter)) - } else { - format!("[{} + {}]", frame_pointer_reg(emitter), offset) - }; - if is_float_register(reg) { - emitter.instruction(&format!("movsd {}, QWORD PTR {}", reg, slot)); // load a spilled floating-point argument from the caller stack area - } else { - emitter.instruction(&format!("mov {}, QWORD PTR {}", reg, slot)); // load a spilled integer or pointer argument from the caller stack area - } - } - } -} - -/// Zero-initializes the local frame slot at `offset` from the frame pointer. -/// On AArch64 uses the xzr register via `store_at_offset`; on x86_64 emits a mov with immediate 0. -pub fn emit_store_zero_to_local_slot(emitter: &mut Emitter, offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - store_at_offset(emitter, "xzr", offset); // zero-initialize the local slot with the architectural zero register - } - Arch::X86_64 => { - if offset == 0 { - emitter.instruction(&format!("mov QWORD PTR [{}], 0", frame_pointer_reg(emitter))); // zero-initialize the frame-base slot directly through rbp - } else { - emitter.instruction(&format!("mov QWORD PTR [{} - {}], 0", frame_pointer_reg(emitter), offset)); // zero-initialize the requested local slot relative to rbp - } - } - } -} - -/// Saves the return value into a hidden frame slot so it survives a tail-call or callback frame switch. -/// Float values use the float result register; strings use string_result_regs (pointer + length); -/// scalars use the integer result register. `return_offset` is the slot for the primary value; string -/// length is stored 8 bytes before it. -pub fn emit_preserve_return_value( - emitter: &mut Emitter, - return_ty: &PhpType, - return_offset: usize, -) { - match return_ty.codegen_repr() { - PhpType::Float => { - store_at_offset(emitter, float_result_reg(emitter), return_offset); // preserve the float return value in the hidden frame slot - } - PhpType::Str => { - let (ptr_reg, len_reg) = string_result_regs(emitter); - store_at_offset(emitter, ptr_reg, return_offset); // preserve the string return pointer in the hidden frame slot - store_at_offset(emitter, len_reg, return_offset - 8); // preserve the string return length in the hidden frame slot - } - _ => { - store_at_offset(emitter, int_result_reg(emitter), return_offset); // preserve the scalar or pointer-like return value in the hidden frame slot - } - } -} - -/// Restores the return value from a hidden frame slot after a tail-call or callback frame switch. -/// Reverse of `emit_preserve_return_value`: loads based on `return_ty` codegen repr into the -/// appropriate result registers. -pub fn emit_restore_return_value( - emitter: &mut Emitter, - return_ty: &PhpType, - return_offset: usize, -) { - match return_ty.codegen_repr() { - PhpType::Float => { - load_at_offset(emitter, float_result_reg(emitter), return_offset); // restore the preserved float return value from the hidden frame slot - } - PhpType::Str => { - let (ptr_reg, len_reg) = string_result_regs(emitter); - load_at_offset(emitter, ptr_reg, return_offset); // restore the preserved string return pointer from the hidden frame slot - load_at_offset(emitter, len_reg, return_offset - 8); // restore the preserved string return length from the hidden frame slot - } - _ => { - load_at_offset(emitter, int_result_reg(emitter), return_offset); // restore the preserved scalar or pointer-like return value from the hidden frame slot - } - } -} - -/// Allocates or releases `amount` bytes from the stack pointer. -/// On AArch64 emits at most 4080-byte chunks to stay within sub/add immediate limits. -/// On x86_64 emits a single sub or add. `subtract=true` reserves space; `subtract=false` releases. -pub(crate) fn emit_adjust_sp(emitter: &mut Emitter, amount: usize, subtract: bool) { - match emitter.target.arch { - Arch::AArch64 => { - let mut remaining = amount; - while remaining > 0 { - let chunk = remaining.min(4080); - if subtract { - emitter.instruction(&format!("sub sp, sp, #{}", chunk)); // reserve stack space for spilled outgoing call arguments - } else { - emitter.instruction(&format!("add sp, sp, #{}", chunk)); // release temporary outgoing call-argument stack space - } - remaining -= chunk; - } - } - Arch::X86_64 => { - if amount == 0 { - return; - } - if subtract { - emitter.instruction(&format!("sub rsp, {}", amount)); // reserve stack space for spilled outgoing call arguments - } else { - emitter.instruction(&format!("add rsp, {}", amount)); // release temporary outgoing call-argument stack space - } - } - } -} - -/// Computes the address of a temporary stack slot relative to the current stack pointer and -/// stores it in `scratch`. Used for stack positions that are not part of the fixed frame layout. -/// On AArch64 walks up from sp in 4080-byte chunks; on x86_64 uses lea with rsp base. -pub(crate) fn emit_sp_address(emitter: &mut Emitter, scratch: &str, offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, sp", scratch)); // seed a scratch pointer from the current stack pointer - let mut remaining = offset; - while remaining > 0 { - let chunk = remaining.min(4080); - emitter.instruction(&format!("add {}, {}, #{}", scratch, scratch, chunk)); // advance the scratch pointer toward the desired stack slot - remaining -= chunk; - } - } - Arch::X86_64 => { - if offset == 0 { - emitter.instruction(&format!("mov {}, rsp", scratch)); // copy the current stack pointer when the requested stack slot is at rsp - } else { - emitter.instruction(&format!("lea {}, [rsp + {}]", scratch, offset)); // materialize the temporary stack-slot address relative to rsp - } - } - } -} diff --git a/src/codegen/abi/mod.rs b/src/codegen/abi/mod.rs deleted file mode 100644 index 6380cab11d..0000000000 --- a/src/codegen/abi/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Purpose: -//! Collects target ABI helpers for frames, registers, calls, symbols, bootstrap, and values. -//! Provides the stable API used by higher-level codegen without exposing architecture details. -//! -//! Called from: -//! - `crate::codegen::*` expression, statement, function, and runtime-facing emitters -//! -//! Key details: -//! - Shared lowering should go through this module instead of hardcoding platform registers or stack rules. - -mod bootstrap; -mod calls; -mod frame; -mod registers; -mod symbols; -#[cfg(test)] -mod tests; -mod values; - -pub use bootstrap::{ - emit_copy_frame_pointer, emit_enable_heap_debug_flag, emit_exit, emit_exit_with_result_reg, - emit_store_process_args_to_globals, -}; -pub use calls::{ - build_outgoing_arg_assignments_for_target, emit_call_label, emit_call_reg, emit_pop_reg, - emit_pop_float_reg, emit_pop_reg_pair, 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, emit_load_temporary_stack_slot, materialize_outgoing_args, -}; -pub use frame::{ - emit_cleanup_callback_epilogue, emit_cleanup_callback_prologue, emit_frame_prologue, - emit_frame_restore, emit_frame_slot_address, emit_load_from_address, - emit_preserve_return_value, emit_reg_move, emit_restore_return_value, emit_return, - emit_store_to_address, emit_store_zero_to_address, emit_store_zero_to_local_slot, load_at_offset, - load_at_offset_scratch, load_from_caller_stack, store_at_offset, store_at_offset_scratch, -}; -pub use registers::{ - nested_call_reg, process_argc_reg, process_argv_reg, temp_int_reg, IncomingArgCursor, - OutgoingArgAssignment, -}; -pub use symbols::{ - emit_cmp_reg_to_symbol, emit_dec_symbol, emit_extern_symbol_address, - emit_load_extern_symbol_to_reg, emit_load_symbol_to_local_slot, emit_load_symbol_to_reg, - emit_load_symbol_to_reg_via_page, emit_load_symbol_to_result, emit_store_imm_to_symbol, - emit_store_local_slot_to_symbol, - emit_store_reg_to_extern_symbol, emit_store_reg_to_symbol, emit_store_result_to_symbol, - emit_store_zero_to_symbol, emit_symbol_address, -}; -pub use values::{ - emit_branch_if_int_result_nonzero, emit_branch_if_int_result_zero, emit_load_int_immediate, - emit_decref_if_refcounted, emit_float_result_to_int_result, emit_incref_if_refcounted, - emit_int_result_to_float_result, emit_jump, emit_load, emit_release_local_ref_cell, - emit_store, emit_write_stdout, -}; -pub(crate) use registers::{ - float_arg_reg_name, float_result_reg, int_arg_reg_name, int_result_reg, - secondary_scratch_reg, string_result_regs, symbol_scratch_reg, tertiary_scratch_reg, -}; diff --git a/src/codegen/abi/registers.rs b/src/codegen/abi/registers.rs deleted file mode 100644 index 46d0b8d0f9..0000000000 --- a/src/codegen/abi/registers.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Purpose: -//! Defines target register names, argument cursors, and outgoing argument assignment planning. -//! Abstracts integer, float, string-pair, scratch, and result-register conventions. -//! -//! Called from: -//! - `crate::codegen::abi` helpers and all higher-level emitters using ABI registers -//! -//! Key details: -//! - Register allocation here models platform ABI limits and must stay in sync with call stack spill logic. - -use crate::codegen::{ - emit::Emitter, - platform::{Arch, Platform, Target}, -}; -use crate::types::PhpType; - -const MAX_INT_ARG_REGS: usize = 8; -const MAX_FLOAT_ARG_REGS: usize = 8; -const CALLER_STACK_START_OFFSET: usize = 32; -/// Sentinel value indicating an outgoing argument is passed on the caller stack rather than in a register. -pub(crate) const STACK_ARG_SENTINEL: usize = usize::MAX; - -/// Returns the maximum number of integer arguments that can be passed in registers for the target ABI. -/// AArch64: 8 (x0–x7). x86_64: 6 (rdi, rsi, rdx, rcx, r8, r9). -pub(crate) fn int_arg_reg_limit(target: Target) -> usize { - match target.arch { - Arch::AArch64 => MAX_INT_ARG_REGS, - Arch::X86_64 => 6, - } -} - -/// Returns the maximum number of float arguments that can be passed in registers for the target ABI. -/// Both AArch64 and x86_64 support 8 float registers (d0–d7 and xmm0–xmm7 respectively). -pub(crate) fn float_arg_reg_limit(target: Target) -> usize { - match target.arch { - Arch::AArch64 => MAX_FLOAT_ARG_REGS, - Arch::X86_64 => MAX_FLOAT_ARG_REGS, - } -} - -/// Returns the frame-pointer offset where the caller's outgoing stack arguments begin. -/// AArch64 call sites keep a 16-byte nested-call save slot above outgoing stack args; -/// after the callee saves x29/x30, the first stack arg is therefore at x29+32. -/// x86_64 reaches the first stack arg at rbp+16 after call pushes the return address. -pub(crate) fn caller_stack_start_offset(target: Target) -> usize { - match target.arch { - Arch::AArch64 => CALLER_STACK_START_OFFSET, - Arch::X86_64 => 16, - } -} - -/// Returns the register name for the `idx`-th integer argument register. -/// Panics if `idx >= int_arg_reg_limit(target)`. -pub(crate) fn int_arg_reg_name(target: Target, idx: usize) -> &'static str { - match target.arch { - Arch::AArch64 => ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"][idx], - Arch::X86_64 => ["rdi", "rsi", "rdx", "rcx", "r8", "r9"][idx], - } -} - -/// Returns the register name for the `idx`-th float argument register. -/// Panics if `idx >= float_arg_reg_limit(target)`. -pub(crate) fn float_arg_reg_name(target: Target, idx: usize) -> &'static str { - match target.arch { - Arch::AArch64 => ["d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7"][idx], - Arch::X86_64 => ["xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"][idx], - } -} - -/// Returns the register used to hold integer/pointer function results. -/// AArch64: x0. x86_64: rax. -pub(crate) fn int_result_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "x0", - Arch::X86_64 => "rax", - } -} - -/// Returns the register used to hold float/double function results. -/// AArch64: d0. x86_64: xmm0. -pub(crate) fn float_result_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "d0", - Arch::X86_64 => "xmm0", - } -} - -/// Returns the pair of registers used to hold a string result (pointer, length). -/// AArch64: (x1, x2). x86_64: (rax, rdx). -pub(crate) fn string_result_regs(emitter: &Emitter) -> (&'static str, &'static str) { - match emitter.target.arch { - Arch::AArch64 => ("x1", "x2"), - Arch::X86_64 => ("rax", "rdx"), - } -} - -/// Returns the frame pointer register for the target. -/// AArch64: x29 (used as platform register). x86_64: rbp. -pub(crate) fn frame_pointer_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "x29", - Arch::X86_64 => "rbp", - } -} - -/// Returns a callee-saved scratch register safe for symbol address materialization. -/// AArch64: x9. x86_64: r11 ( caller-saved on x86_64, but used as scratch here). -pub(crate) fn symbol_scratch_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "x9", - Arch::X86_64 => "r11", - } -} - -/// Returns a secondary scratch register for temporary use during code generation. -/// AArch64: x10. x86_64: r10. -pub(crate) fn secondary_scratch_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "x10", - Arch::X86_64 => "r10", - } -} - -/// Returns a tertiary scratch register for temporary use during code generation. -/// AArch64: x11. x86_64: rcx. -pub(crate) fn tertiary_scratch_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "x11", - Arch::X86_64 => "rcx", - } -} - -/// Returns a temporary integer register for general intermediate computations. -/// AArch64: x10. x86_64: r10. -pub fn temp_int_reg(target: Target) -> &'static str { - match target.arch { - Arch::AArch64 => "x10", - Arch::X86_64 => "r10", - } -} - -/// Returns the register holding argc (argument count) on entry to `main`. -/// AArch64: x0. x86_64: rdi. -pub fn process_argc_reg(target: Target) -> &'static str { - match target.arch { - Arch::AArch64 => "x0", - Arch::X86_64 => "rdi", - } -} - -/// Returns the register holding argv (argument vector pointer) on entry to `main`. -/// AArch64: x1. x86_64: rsi. -pub fn process_argv_reg(target: Target) -> &'static str { - match target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "rsi", - } -} - -/// Returns a callee-saved register used to preserve the frame pointer across nested calls. -/// AArch64: x19. x86_64: r12. -pub fn nested_call_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "x19", - Arch::X86_64 => "r12", - } -} - -/// Returns true if `reg` is a floating-point register name (d0–d7 or xmm0–xmm7). -pub(crate) fn is_float_register(reg: &str) -> bool { - reg.starts_with('d') || reg.starts_with("xmm") -} - -/// Tracks the next available integer/float registers and stack offset when receiving arguments in a function prologue. -/// `int_stack_only` is set to true when the callee receives all arguments from the caller stack (e.g., variadic functions). -/// `float_stack_only` is reserved for calls that pass floats only on the stack. -#[derive(Debug, Clone, Copy)] -pub struct IncomingArgCursor { - pub(crate) int_reg_idx: usize, - pub(crate) float_reg_idx: usize, - pub(crate) caller_stack_offset: usize, - pub(crate) int_stack_only: bool, - pub(crate) float_stack_only: bool, -} - -impl IncomingArgCursor { - /// Creates a cursor for a callee receiving `initial_int_reg_idx` integer register arguments. - pub fn new(initial_int_reg_idx: usize) -> Self { - Self::for_target(Target::new(Platform::MacOS, Arch::AArch64), initial_int_reg_idx) - } - - /// Creates a cursor for the given target and initial integer register index. - /// Sets `int_stack_only` when `initial_int_reg_idx` exceeds the target's integer arg register limit. - pub fn for_target(target: Target, initial_int_reg_idx: usize) -> Self { - Self { - int_reg_idx: initial_int_reg_idx, - float_reg_idx: 0, - caller_stack_offset: caller_stack_start_offset(target), - int_stack_only: initial_int_reg_idx >= int_arg_reg_limit(target), - float_stack_only: false, - } - } -} - -impl Default for IncomingArgCursor { - /// Default cursor starts at integer register 0 with default AArch64 target. - fn default() -> Self { - Self::new(0) - } -} - -/// Describes where a single outgoing argument is placed: register or stack. -#[derive(Debug, Clone, PartialEq)] -pub struct OutgoingArgAssignment { - pub ty: PhpType, - pub start_reg: usize, - pub is_float: bool, -} - -impl OutgoingArgAssignment { - /// Returns true if the argument is passed in a register (not on the caller stack). - pub(crate) fn in_register(&self) -> bool { - self.start_reg != STACK_ARG_SENTINEL - } -} diff --git a/src/codegen/abi/symbols.rs b/src/codegen/abi/symbols.rs deleted file mode 100644 index 75bfbcfa52..0000000000 --- a/src/codegen/abi/symbols.rs +++ /dev/null @@ -1,561 +0,0 @@ -//! Purpose: -//! Emits target-aware loads, stores, and addresses for assembly symbols and external globals. -//! Bridges local slot values, result registers, and static storage symbols. -//! -//! Called from: -//! - `crate::codegen::stmt::storage`, `crate::codegen::functions`, and global emitters -//! -//! Key details: -//! - Symbol relocations differ by platform and refcounted stores must preserve ownership cleanup. - -use crate::codegen::{emit::Emitter, platform::Arch}; -use crate::codegen::NULL_SENTINEL; -use crate::types::PhpType; - -use super::calls::emit_call_label; -use super::frame::{ - emit_load_from_address, emit_store_to_address, load_at_offset_scratch, store_at_offset_scratch, -}; -use super::registers::{ - float_result_reg, int_result_reg, is_float_register, secondary_scratch_reg, - string_result_regs, symbol_scratch_reg, tertiary_scratch_reg, -}; -use super::values::{emit_decref_if_refcounted, emit_load_int_immediate}; - - -/// Stores a local variable from its frame slot into a static/global symbol. -/// Loads the value from `offset` relative to the frame pointer, then writes it -/// to `symbol` at `byte_offset`. Handles Float (single register), Str (pointer -/// + length pair), Void (null sentinel), and scalar/pointer types differently. -pub fn emit_store_local_slot_to_symbol( - emitter: &mut Emitter, - symbol: &str, - ty: &PhpType, - offset: usize, -) { - let symbol_reg = symbol_scratch_reg(emitter); - let local_reg = secondary_scratch_reg(emitter); - let local_hi_reg = tertiary_scratch_reg(emitter); - match ty.codegen_repr() { - PhpType::Float => { - load_at_offset_scratch(emitter, float_result_reg(emitter), offset, local_reg); // load the local float value from its frame slot - emit_store_reg_to_symbol(emitter, float_result_reg(emitter), symbol, 0); // store the local float value into symbol storage - } - PhpType::Str => { - load_at_offset_scratch(emitter, local_reg, offset, symbol_reg); // load the local string pointer from its frame slot - load_at_offset_scratch(emitter, local_hi_reg, offset - 8, symbol_reg); // load the local string length from its paired frame slot - emit_store_reg_to_symbol(emitter, local_reg, symbol, 0); // store the local string pointer into symbol storage - emit_store_reg_to_symbol(emitter, local_hi_reg, symbol, 8); // store the local string length into symbol storage - } - PhpType::Void => { - load_at_offset_scratch(emitter, local_reg, offset, symbol_reg); // load the local null sentinel from its frame slot - emit_store_reg_to_symbol(emitter, local_reg, symbol, 0); // store the local null sentinel into symbol storage - } - _ => { - load_at_offset_scratch(emitter, local_reg, offset, symbol_reg); // load the local scalar or pointer-like value from its frame slot - emit_store_reg_to_symbol(emitter, local_reg, symbol, 0); // store the local scalar or pointer-like value into symbol storage - } - } -} - -/// Loads a value from a static/global symbol into a local frame slot. -/// Reads from `symbol` at `byte_offset` and writes it to `offset` relative to -/// the frame pointer. Dispatches on `ty` to handle Float, Str (pointer + -/// length), Void (null sentinel), and scalar/pointer types. -pub fn emit_load_symbol_to_local_slot( - emitter: &mut Emitter, - symbol: &str, - ty: &PhpType, - offset: usize, -) { - let local_reg = secondary_scratch_reg(emitter); - let local_hi_reg = tertiary_scratch_reg(emitter); - match ty.codegen_repr() { - PhpType::Float => { - emit_load_symbol_to_reg(emitter, float_result_reg(emitter), symbol, 0); // load the float value from symbol storage - store_at_offset_scratch(emitter, float_result_reg(emitter), offset, local_reg); // write the loaded float value into the local frame slot - } - PhpType::Str => { - let (ptr_reg, len_reg) = string_result_regs(emitter); - emit_load_symbol_to_reg(emitter, ptr_reg, symbol, 0); // load the string pointer from symbol storage - emit_load_symbol_to_reg(emitter, len_reg, symbol, 8); // load the string length from symbol storage - store_at_offset_scratch(emitter, ptr_reg, offset, local_reg); // write the loaded string pointer into the local frame slot - store_at_offset_scratch(emitter, len_reg, offset - 8, local_hi_reg); // write the loaded string length into the paired local frame slot - } - PhpType::Void => { - emit_load_symbol_to_reg(emitter, int_result_reg(emitter), symbol, 0); // load the null sentinel from symbol storage - store_at_offset_scratch(emitter, int_result_reg(emitter), offset, local_reg); // write the loaded null sentinel into the local frame slot - } - _ => { - emit_load_symbol_to_reg(emitter, int_result_reg(emitter), symbol, 0); // load the scalar or pointer-like value from symbol storage - store_at_offset_scratch(emitter, int_result_reg(emitter), offset, local_reg); // write the loaded scalar or pointer-like value into the local frame slot - } - } -} - -/// Materializes the address of a local/internal symbol into `dest`. -/// Uses ADRP+ADD on AArch64 (page-relative) and LEA with RIP-relative -/// addressing on x86_64. The symbol must be defined in the current module's -/// data section. -pub fn emit_symbol_address(emitter: &mut Emitter, dest: &str, symbol: &str) { - if emitter.pic_data_refs { - emit_extern_symbol_address(emitter, dest, symbol); - return; - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.adrp(dest, &format!("{}", symbol)); // load the page of the requested symbol storage - emitter.add_lo12(dest, dest, &format!("{}", symbol)); // resolve the exact address of the requested symbol storage - } - Arch::X86_64 => { - emitter.instruction(&format!("lea {}, [rip + {}]", dest, symbol)); // materialize the symbol address through a RIP-relative LEA - } - } -} - -/// Materializes the address of an external/global symbol into `dest`. -/// Resolves the symbol through the GOT on both targets: ADRP+GOT on AArch64 -/// and MOVQ with GOTPCREL on x86_64. Used for symbols defined outside the -/// current translation unit. -pub fn emit_extern_symbol_address(emitter: &mut Emitter, dest: &str, symbol: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.adrp_got(dest, symbol); // load the GOT page that points at the requested extern symbol - emitter.ldr_got_lo12(dest, dest, symbol); // resolve the GOT entry into the actual extern symbol address - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, QWORD PTR {}@GOTPCREL[rip]", dest, symbol)); // materialize the extern symbol address through the ELF GOTPCREL slot - } - } -} - -/// Loads a value from an external symbol into `reg`. -/// First resolves the extern symbol address via the GOT, then performs a -/// load from `byte_offset` into `reg`. Used for reading global variables -/// defined in external libraries or other compilation units. -pub fn emit_load_extern_symbol_to_reg( - emitter: &mut Emitter, - reg: &str, - symbol: &str, - byte_offset: usize, -) { - let scratch = symbol_scratch_reg(emitter); - emit_extern_symbol_address(emitter, scratch, symbol); - emit_load_from_address(emitter, reg, scratch, byte_offset); -} - -/// Stores the contents of `reg` into an external symbol at a byte offset. -/// First resolves the extern symbol address via the GOT, then performs a -/// store from `reg` to `byte_offset`. Used for writing to global variables -/// defined in external libraries or other compilation units. -pub fn emit_store_reg_to_extern_symbol( - emitter: &mut Emitter, - reg: &str, - symbol: &str, - byte_offset: usize, -) { - let scratch = symbol_scratch_reg(emitter); - emit_extern_symbol_address(emitter, scratch, symbol); - emit_store_to_address(emitter, reg, scratch, byte_offset); -} - -/// Loads a value from a local/internal symbol into `reg`. -/// Uses a temporary scratch register (x9 on AArch64) to compute the symbol -/// address, then loads from `byte_offset`. On x86_64 uses RIP-relative -/// addressing directly when offset is zero. Dispatches on register type -/// for float vs. integer moves on x86_64. -/// -/// In PIC mode the x86_64 path keeps the register footprint identical to the -/// non-PIC emission: integer destinations resolve the GOT entry through the -/// destination register itself, and float destinations borrow r11 behind a -/// push/pop so call sites never see an extra clobbered register. -pub fn emit_load_symbol_to_reg( - emitter: &mut Emitter, - reg: &str, - symbol: &str, - byte_offset: usize, -) { - if emitter.pic_data_refs { - match emitter.target.arch { - Arch::AArch64 => { - emit_load_extern_symbol_to_reg(emitter, reg, symbol, byte_offset); - } - Arch::X86_64 => { - if is_float_register(reg) { - emitter.instruction("push r11"); // preserve the borrowed GOT scratch register around the float load - emit_extern_symbol_address(emitter, "r11", symbol); - emit_load_from_address(emitter, reg, "r11", byte_offset); - emitter.instruction("pop r11"); // restore the borrowed GOT scratch register after the float load - } else { - emit_extern_symbol_address(emitter, reg, symbol); - emit_load_from_address(emitter, reg, reg, byte_offset); - } - } - } - return; - } - match emitter.target.arch { - Arch::AArch64 => { - emit_symbol_address(emitter, "x9", symbol); - if byte_offset == 0 { - emitter.instruction(&format!("ldr {}, [x9]", reg)); // load the symbol payload directly from its base address - } else { - emitter.instruction(&format!("ldr {}, [x9, #{}]", reg, byte_offset)); // load the symbol payload from the requested byte offset - } - } - Arch::X86_64 => { - let scratch = symbol_scratch_reg(emitter); - if byte_offset == 0 { - if is_float_register(reg) { - emitter.instruction(&format!("movsd {}, QWORD PTR [rip + {}]", reg, symbol)); // load the floating-point symbol payload through RIP-relative addressing - } else { - emitter.instruction(&format!("mov {}, QWORD PTR [rip + {}]", reg, symbol)); // load the integer or pointer symbol payload through RIP-relative addressing - } - } else { - emit_symbol_address(emitter, scratch, symbol); - if is_float_register(reg) { - emitter.instruction(&format!("movsd {}, QWORD PTR [{} + {}]", reg, scratch, byte_offset)); // load the floating-point symbol payload from a non-zero byte offset - } else { - emitter.instruction(&format!("mov {}, QWORD PTR [{} + {}]", reg, scratch, byte_offset)); // load the integer or pointer symbol payload from a non-zero byte offset - } - } - } - } -} - -/// Stores the contents of `reg` into a local/internal symbol at a byte offset. -/// Uses a temporary scratch register (x9 on AArch64) to compute the symbol -/// address, then stores at `byte_offset`. On x86_64 uses RIP-relative -/// addressing directly when offset is zero. Dispatches on register type -/// for float vs. integer moves on x86_64. -/// -/// In PIC mode the x86_64 path needs a general-purpose register to hold the -/// GOT-resolved address; it borrows r11 (or r10 when r11 is the stored value) -/// behind a push/pop so call sites never see an extra clobbered register. -pub fn emit_store_reg_to_symbol( - emitter: &mut Emitter, - reg: &str, - symbol: &str, - byte_offset: usize, -) { - if emitter.pic_data_refs { - match emitter.target.arch { - Arch::AArch64 => { - emit_store_reg_to_extern_symbol(emitter, reg, symbol, byte_offset); - } - Arch::X86_64 => { - let scratch = if reg == "r11" { "r10" } else { "r11" }; - emitter.instruction(&format!("push {}", scratch)); // preserve the borrowed GOT scratch register around the store - emit_extern_symbol_address(emitter, scratch, symbol); - emit_store_to_address(emitter, reg, scratch, byte_offset); - emitter.instruction(&format!("pop {}", scratch)); // restore the borrowed GOT scratch register after the store - } - } - return; - } - match emitter.target.arch { - Arch::AArch64 => { - emit_symbol_address(emitter, "x9", symbol); - if byte_offset == 0 { - emitter.instruction(&format!("str {}, [x9]", reg)); // store the register payload directly into the symbol base slot - } else { - emitter.instruction(&format!("str {}, [x9, #{}]", reg, byte_offset)); // store the register payload into the requested symbol byte offset - } - } - Arch::X86_64 => { - let scratch = symbol_scratch_reg(emitter); - if byte_offset == 0 { - if is_float_register(reg) { - emitter.instruction(&format!("movsd QWORD PTR [rip + {}], {}", symbol, reg)); // store the floating-point payload directly into RIP-relative symbol storage - } else { - emitter.instruction(&format!("mov QWORD PTR [rip + {}], {}", symbol, reg)); // store the integer or pointer payload directly into RIP-relative symbol storage - } - } else { - emit_symbol_address(emitter, scratch, symbol); - if is_float_register(reg) { - emitter.instruction(&format!("movsd QWORD PTR [{} + {}], {}", scratch, byte_offset, reg)); // store the floating-point payload into a non-zero symbol byte offset - } else { - emitter.instruction(&format!("mov QWORD PTR [{} + {}], {}", scratch, byte_offset, reg)); // store the integer or pointer payload into a non-zero symbol byte offset - } - } - } - } -} - -/// Stores the architectural zero register (xzr / zero) into a symbol slot. -/// On AArch64 this is a single STR using xzr; on x86_64 it emits a MOV -/// immediate zero. Used to initialize symbol storage to null/zero without -/// a separate load-from-register step. -pub fn emit_store_zero_to_symbol(emitter: &mut Emitter, symbol: &str, byte_offset: usize) { - if emitter.pic_data_refs { - match emitter.target.arch { - Arch::AArch64 => { - emit_store_reg_to_extern_symbol(emitter, "xzr", symbol, byte_offset); - } - Arch::X86_64 => { - emitter.instruction("push r11"); // preserve the borrowed GOT scratch register around the zero store - emit_extern_symbol_address(emitter, "r11", symbol); - if byte_offset == 0 { - emitter.instruction("mov QWORD PTR [r11], 0"); // zero the symbol base slot through the GOT-resolved address - } else { - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", byte_offset)); // zero the requested symbol byte offset through the GOT-resolved address - } - emitter.instruction("pop r11"); // restore the borrowed GOT scratch register after the zero store - } - } - return; - } - match emitter.target.arch { - Arch::AArch64 => { - emit_store_reg_to_symbol(emitter, "xzr", symbol, byte_offset); // store architectural zero directly into symbol-backed storage - } - Arch::X86_64 => { - let scratch = symbol_scratch_reg(emitter); - if byte_offset == 0 { - emitter.instruction(&format!("mov QWORD PTR [rip + {}], 0", symbol)); // zero the symbol base slot through RIP-relative addressing - } else { - emit_symbol_address(emitter, scratch, symbol); - emitter.instruction(&format!("mov QWORD PTR [{} + {}], 0", scratch, byte_offset)); // zero the requested symbol byte offset through the computed address - } - } - } -} - -/// Loads a 64-bit value from a symbol using the AArch64 paged addressing idiom: -/// `adrp addr_reg, sym` followed by `ldr dest, [addr_reg, :lo12:sym]` (two -/// instructions, matching the historical hand-written emission). In PIC mode -/// the GOT entry is resolved into `addr_reg` first and the payload loaded -/// through it. `addr_reg` is clobbered in both modes; `dest` may be an integer -/// or floating-point register. AArch64-only: x86_64 callers use -/// `emit_load_symbol_to_reg`, which already covers both modes there. -pub fn emit_load_symbol_to_reg_via_page( - emitter: &mut Emitter, - dest: &str, - addr_reg: &str, - symbol: &str, -) { - emitter - .target - .ensure_aarch64_backend("paged symbol load emission"); - if emitter.pic_data_refs { - emitter.adrp_got(addr_reg, symbol); // load the GOT page that points at the requested symbol - emitter.ldr_got_lo12(addr_reg, addr_reg, symbol); // resolve the GOT entry into the symbol address - emitter.instruction(&format!("ldr {}, [{}]", dest, addr_reg)); // load the symbol payload through the GOT-resolved address - } else { - emitter.adrp(addr_reg, symbol); // load the page address that contains the symbol storage - emitter.ldr_lo12(dest, addr_reg, symbol); // load the symbol payload from its page offset - } -} - -/// Stores a small integer immediate into a local/internal symbol slot. -/// On x86_64 non-PIC this is a single RIP-relative MOV with an immediate -/// operand; in PIC mode the GOT-resolved address is borrowed into r11 behind -/// a push/pop. On AArch64 the immediate is materialized in x10 and stored -/// through x9 (both clobbered). -pub fn emit_store_imm_to_symbol( - emitter: &mut Emitter, - symbol: &str, - byte_offset: usize, - imm: i64, -) { - match emitter.target.arch { - Arch::AArch64 => { - emit_symbol_address(emitter, "x9", symbol); // resolve the symbol address into the x9 scratch register - emit_load_int_immediate(emitter, "x10", imm); - if byte_offset == 0 { - emitter.instruction("str x10, [x9]"); // store the immediate payload into the symbol base slot - } else { - emitter.instruction(&format!("str x10, [x9, #{}]", byte_offset)); // store the immediate payload into the requested symbol byte offset - } - } - Arch::X86_64 => { - let slot_suffix = if byte_offset == 0 { - String::new() - } else { - format!(" + {}", byte_offset) - }; - if emitter.pic_data_refs { - emitter.instruction("push r11"); // preserve the borrowed GOT scratch register around the immediate store - emit_extern_symbol_address(emitter, "r11", symbol); - emitter.instruction(&format!("mov QWORD PTR [r11{}], {}", slot_suffix, imm)); // store the immediate payload through the GOT-resolved address - emitter.instruction("pop r11"); // restore the borrowed GOT scratch register after the immediate store - } else { - let inst = format!("mov QWORD PTR [rip + {}{}], {}", symbol, slot_suffix, imm); - emitter.instruction(&inst); // store the immediate payload through RIP-relative addressing - } - } - } -} - -/// Compares `reg` against the 64-bit value stored at a local/internal symbol, -/// setting the condition flags for a following conditional branch. -/// On x86_64 non-PIC this is a single memory-operand CMP; in PIC mode the -/// GOT-resolved address is held in a borrowed scratch register (r11, or r10 -/// when `reg` is r11) protected by push/pop, which do not alter flags. -/// On AArch64 the symbol payload is loaded through x9 (clobbered) and compared -/// register-to-register. -pub fn emit_cmp_reg_to_symbol(emitter: &mut Emitter, reg: &str, symbol: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emit_load_symbol_to_reg(emitter, "x9", symbol, 0); // load the symbol payload into the x9 scratch register - emitter.instruction(&format!("cmp {}, x9", reg)); // compare the register payload against the symbol payload - } - Arch::X86_64 => { - if emitter.pic_data_refs { - let scratch = if reg == "r11" { "r10" } else { "r11" }; - emitter.instruction(&format!("push {}", scratch)); // preserve the borrowed GOT scratch register (push leaves flags intact) - emit_extern_symbol_address(emitter, scratch, symbol); - emitter.instruction(&format!("cmp {}, QWORD PTR [{}]", reg, scratch)); // compare the register payload against the GOT-resolved symbol payload - emitter.instruction(&format!("pop {}", scratch)); // restore the borrowed GOT scratch register (pop leaves flags intact) - } else { - emitter.instruction(&format!("cmp {}, QWORD PTR [rip + {}]", reg, symbol)); // compare the register payload against the RIP-relative symbol payload - } - } - } -} - -/// Decrements the 64-bit counter stored at a local/internal symbol by one. -/// On x86_64 non-PIC this is a single memory-operand DEC; in PIC mode the -/// GOT-resolved address is held in r11 behind a push/pop. On AArch64 the -/// counter is loaded through x9/x10 (both clobbered), decremented, and stored -/// back. -pub fn emit_dec_symbol(emitter: &mut Emitter, symbol: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emit_symbol_address(emitter, "x9", symbol); // resolve the symbol address into the x9 scratch register - emitter.instruction("ldr x10, [x9]"); // load the current counter value - emitter.instruction("sub x10, x10, #1"); // decrement the counter by one - emitter.instruction("str x10, [x9]"); // store the decremented counter back into symbol storage - } - Arch::X86_64 => { - if emitter.pic_data_refs { - emitter.instruction("push r11"); // preserve the borrowed GOT scratch register around the decrement - emit_extern_symbol_address(emitter, "r11", symbol); - emitter.instruction("dec QWORD PTR [r11]"); // decrement the counter through the GOT-resolved address - emitter.instruction("pop r11"); // restore the borrowed GOT scratch register after the decrement - } else { - emitter.instruction(&format!("dec QWORD PTR [rip + {}]", symbol)); // decrement the counter through RIP-relative addressing - } - } - } -} - -/// Loads a symbol's value and places it into the appropriate result registers. -/// Reads from `symbol` at offset 0 (and 8 for strings) and places the value -/// into the target's canonical result registers: float_result_reg for Float, -/// string_result_regs for Str, tagged-scalar result registers for TaggedScalar, -/// int_result_reg otherwise. Used to return a symbol's value as a call result. -pub fn emit_load_symbol_to_result(emitter: &mut Emitter, symbol: &str, ty: &PhpType) { - match ty.codegen_repr() { - PhpType::Float => { - emit_load_symbol_to_reg(emitter, float_result_reg(emitter), symbol, 0); // load the float payload from symbol storage into the result register - } - PhpType::Str => { - let (ptr_reg, len_reg) = string_result_regs(emitter); - emit_load_symbol_to_reg(emitter, ptr_reg, symbol, 0); // load the string pointer from symbol storage into the result register pair - emit_load_symbol_to_reg(emitter, len_reg, symbol, 8); // load the string length from symbol storage into the result register pair - } - PhpType::Void => { - emit_load_symbol_to_reg(emitter, int_result_reg(emitter), symbol, 0); // load the null sentinel from symbol storage into the result register - } - PhpType::TaggedScalar => { - emit_load_symbol_to_reg(emitter, int_result_reg(emitter), symbol, 0); // load the tagged scalar payload from symbol storage - emit_load_symbol_to_reg( - emitter, - crate::codegen::sentinels::tagged_scalar_tag_reg(emitter), - symbol, - 8, - ); // load the tagged scalar tag from symbol storage - } - _ => { - emit_load_symbol_to_reg(emitter, int_result_reg(emitter), symbol, 0); // load the scalar or pointer-like payload from symbol storage into the result register - } - } -} - -/// Stores the current result registers into a static/global symbol. -/// If `release_previous` is true, first loads the old symbol value and -/// releases it: strings call `__rt_heap_free_safe`, refcounted types call -/// `emit_decref_if_refcounted`. Incoming results are preserved on the stack -/// during the release call. Handles Float, Str (pointer + length pair), -/// TaggedScalar (payload + tag pair), Void (null sentinel), and scalar/pointer types. -pub fn emit_store_result_to_symbol( - emitter: &mut Emitter, - symbol: &str, - ty: &PhpType, - release_previous: bool, -) { - let ty = ty.codegen_repr(); - if release_previous { - if matches!(ty, PhpType::Str) { - let (ptr_reg, len_reg) = string_result_regs(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the incoming string result while releasing the previous symbol payload - } - Arch::X86_64 => { - emitter.instruction(&format!("push {}", ptr_reg)); // preserve the incoming string pointer result while releasing the previous symbol payload - emitter.instruction(&format!("push {}", len_reg)); // preserve the incoming string length result while releasing the previous symbol payload - } - } - emit_load_symbol_to_reg(emitter, int_result_reg(emitter), symbol, 0); - emit_call_label(emitter, "__rt_heap_free_safe"); // release the previous string allocation before overwriting the symbol slot - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the incoming string result after the release helper call - } - Arch::X86_64 => { - emitter.instruction(&format!("pop {}", len_reg)); // restore the incoming string length result after the release helper call - emitter.instruction(&format!("pop {}", ptr_reg)); // restore the incoming string pointer result after the release helper call - } - } - } else if ty.is_refcounted() { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the incoming heap pointer while decreffing the previous symbol payload - } - Arch::X86_64 => { - emitter.instruction(&format!("push {}", int_result_reg(emitter))); // preserve the incoming heap pointer while decreffing the previous symbol payload - } - } - emit_load_symbol_to_reg(emitter, int_result_reg(emitter), symbol, 0); - emit_decref_if_refcounted(emitter, &ty); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp], #16"); // restore the incoming heap pointer after decreffing the previous payload - } - Arch::X86_64 => { - emitter.instruction(&format!("pop {}", int_result_reg(emitter))); // restore the incoming heap pointer after decreffing the previous payload - } - } - } - } - - match ty { - PhpType::Float => { - emit_store_reg_to_symbol(emitter, float_result_reg(emitter), symbol, 0); // store the float result into symbol storage - } - PhpType::Str => { - let (ptr_reg, len_reg) = string_result_regs(emitter); - emit_store_reg_to_symbol(emitter, ptr_reg, symbol, 0); // store the string pointer result into symbol storage - emit_store_reg_to_symbol(emitter, len_reg, symbol, 8); // store the string length result into symbol storage - } - PhpType::Void => { - let null_reg = secondary_scratch_reg(emitter); - emit_load_int_immediate(emitter, null_reg, NULL_SENTINEL); - emit_store_reg_to_symbol(emitter, null_reg, symbol, 0); // store the null sentinel result into symbol storage - } - PhpType::TaggedScalar => { - emit_store_reg_to_symbol(emitter, int_result_reg(emitter), symbol, 0); // store the tagged scalar payload into symbol storage - emit_store_reg_to_symbol( - emitter, - crate::codegen::sentinels::tagged_scalar_tag_reg(emitter), - symbol, - 8, - ); // store the tagged scalar tag into symbol storage - } - _ => { - emit_store_reg_to_symbol(emitter, int_result_reg(emitter), symbol, 0); // store the scalar or pointer-like result into symbol storage - } - } -} diff --git a/src/codegen/abi/tests/linux_x86_64.rs b/src/codegen/abi/tests/linux_x86_64.rs deleted file mode 100644 index 2f451344e4..0000000000 --- a/src/codegen/abi/tests/linux_x86_64.rs +++ /dev/null @@ -1,598 +0,0 @@ -//! Purpose: -//! Contains ABI regression tests for linux x86 64 helper behavior. -//! Checks emitted assembly fragments rather than running linked programs. -//! -//! Called from: -//! - `crate::codegen::abi::tests` through Rust test harness -//! -//! Key details: -//! - Assertions pin register, stack, relocation, and platform-specific instruction choices. - -use super::*; - -/// Verifies that build_outgoing_arg_assignments_for_target applies SysV AMD64 ABI -/// rules: integer arguments use registers rdi, rsi, rdx, rcx, r8, r9 (indices 0-5), -/// floating-point arguments use xmm0-xmm7 (indices 0-7), and arguments beyond those -/// limits are assigned the STACK_ARG_SENTINEL sentinel indicating stack placement. -#[test] -fn test_build_outgoing_arg_assignments_for_linux_x86_64_respects_sysv_limits() { - let assignments = build_outgoing_arg_assignments_for_target( - Target::new(Platform::Linux, Arch::X86_64), - &[ - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Str, - PhpType::Float, - PhpType::Float, - PhpType::Float, - PhpType::Float, - PhpType::Float, - PhpType::Float, - PhpType::Float, - PhpType::Float, - PhpType::Float, - ], - 0, - ); - - assert_eq!(assignments[0].start_reg, 0); - assert_eq!(assignments[5].start_reg, 5); - assert_eq!(assignments[6].start_reg, crate::codegen::abi::registers::STACK_ARG_SENTINEL); - assert_eq!(assignments[7].start_reg, crate::codegen::abi::registers::STACK_ARG_SENTINEL); - assert!(assignments[8].is_float); - assert_eq!(assignments[15].start_reg, 7); - assert_eq!(assignments[16].start_reg, crate::codegen::abi::registers::STACK_ARG_SENTINEL); -} - -/// Verifies that IncomingArgCursor::for_target initializes with correct SysV AMD64 -/// defaults: caller_stack_offset is 16 (space for return address + saved rbp), -/// int_stack_only is false for the first 6 integer register slots, and the cursor -/// transitions to int_stack_only mode when argument index exceeds 5 (the last -/// integer register slot). Caller stack offset is 16. -#[test] -fn test_incoming_arg_cursor_for_linux_x86_64_uses_sysv_defaults() { - let cursor = IncomingArgCursor::for_target(Target::new(Platform::Linux, Arch::X86_64), 0); - assert_eq!(cursor.caller_stack_offset, 16); - assert!(!cursor.int_stack_only); - - let stack_only_cursor = - IncomingArgCursor::for_target(Target::new(Platform::Linux, Arch::X86_64), 6); - assert!(stack_only_cursor.int_stack_only); -} - -/// Verifies that Linux x86_64 outgoing integer args use SysV registers and staged stack overflow. -#[test] -fn test_materialize_outgoing_args_for_linux_x86_64_uses_sysv_registers() { - let mut emitter = test_emitter_x86(); - let assignments = build_outgoing_arg_assignments_for_target( - Target::new(Platform::Linux, Arch::X86_64), - &[ - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Int, - PhpType::Int, - ], - 0, - ); - - let overflow_bytes = materialize_outgoing_args(&mut emitter, &assignments); - let out = emitter.output(); - - assert_eq!(overflow_bytes, 16); - assert!(out.contains(" sub rsp, 32\n")); - assert!(out.contains(" mov rdi, QWORD PTR [rsp + 128]\n")); - assert!(out.contains(" mov r9, QWORD PTR [rsp + 48]\n")); - assert!(out.contains(" mov r10, QWORD PTR [rsp + 32]\n")); - assert!(out.contains(" mov QWORD PTR [rsp], r10\n")); - assert!(out.contains(" mov r10, QWORD PTR [rsp]\n")); - assert!(out.contains(" mov QWORD PTR [rsp + 128], r10\n")); - assert!(out.contains(" add rsp, 128\n")); -} - -/// Verifies that Linux x86_64 outgoing string args preserve register temps while staging overflow. -#[test] -fn test_materialize_outgoing_string_args_for_linux_x86_64_preserves_live_rcx() { - let mut emitter = test_emitter_x86(); - let assignments = build_outgoing_arg_assignments_for_target( - Target::new(Platform::Linux, Arch::X86_64), - &[PhpType::Str, PhpType::Str, PhpType::Str], - 1, - ); - - let overflow_bytes = materialize_outgoing_args(&mut emitter, &assignments); - let out = emitter.output(); - - assert_eq!(overflow_bytes, 16); - assert!(out.contains(" mov rsi, QWORD PTR [rsp + 64]\n")); - assert!(out.contains(" mov rdx, QWORD PTR [rsp + 72]\n")); - assert!(out.contains(" mov rcx, QWORD PTR [rsp + 48]\n")); - assert!(out.contains(" mov r8, QWORD PTR [rsp + 56]\n")); - assert!(out.contains(" mov r10, QWORD PTR [rsp + 32]\n")); - assert!(out.contains(" mov r11, QWORD PTR [rsp + 40]\n")); - assert!(out.contains(" mov QWORD PTR [rsp], r10\n")); - assert!(out.contains(" mov QWORD PTR [rsp + 8], r11\n")); - assert!(out.contains(" mov QWORD PTR [rsp + 64], r10\n")); - assert!(out.contains(" mov QWORD PTR [rsp + 72], r11\n")); - assert!(!out.contains(" mov rcx, QWORD PTR [rsp + 40]\n")); -} - -/// Verifies that emit_frame_prologue emits push rbp / mov rbp, rsp / sub rsp, N -/// for the x86_64 frame setup; emit_frame_restore emits add rsp, N / pop rbp; -/// and emit_return emits the standard epilogue with ret. The test confirms the -/// 16-byte stack alignment requirement is respected. -#[test] -fn test_emit_frame_helpers_linux_x86_64() { - let mut emitter = test_emitter_x86(); - emit_frame_prologue(&mut emitter, 48); - emit_frame_restore(&mut emitter, 48); - emit_return(&mut emitter); - - assert_eq!( - emitter.output(), - concat!( - " # prologue\n", - " push rbp\n", - " mov rbp, rsp\n", - " sub rsp, 32\n", - " add rsp, 32\n", - " pop rbp\n", - " ret\n", - ) - ); -} - -/// Verifies that emit_frame_slot_address emits "lea reg, [rbp - offset]" to -/// compute the address of a local variable slot relative to the frame pointer. -/// On x86_64, negative offsets from rbp access locals; the lea instruction -/// computes the address without touching memory. -#[test] -fn test_emit_frame_slot_address_linux_x86_64() { - let mut emitter = test_emitter_x86(); - emit_frame_slot_address(&mut emitter, "r10", 40); - - assert_eq!(emitter.output(), " lea r10, [rbp - 40]\n"); -} - -/// Verifies that emit_load_from_address emits mov for integer and movsd for -/// floating-point loads from a base+offset address; emit_store_to_address -/// emits the corresponding store instructions; and emit_store_zero_to_address -/// emits a mov with immediate zero. Tests both integer (QWORD PTR) and -/// floating-point (movsd) variants at different offsets. -#[test] -fn test_emit_load_and_store_to_address_linux_x86_64() { - let mut emitter = test_emitter_x86(); - emit_load_from_address(&mut emitter, "rax", "r11", 0); - emit_load_from_address(&mut emitter, "xmm0", "r11", 8); - emit_store_to_address(&mut emitter, "r10", "r11", 0); - emit_store_to_address(&mut emitter, "xmm1", "r11", 8); - emit_store_zero_to_address(&mut emitter, "r11", 16); - - assert_eq!( - emitter.output(), - concat!( - " mov rax, QWORD PTR [r11]\n", - " movsd xmm0, QWORD PTR [r11 + 8]\n", - " mov QWORD PTR [r11], r10\n", - " movsd QWORD PTR [r11 + 8], xmm1\n", - " mov QWORD PTR [r11 + 16], 0\n", - ) - ); -} - -/// Verifies that emit_symbol_address uses RIP-relative LEA on x86_64 (the -/// standard position-independent code pattern: lea reg, [rip + symbol]). -/// RIP-relative addressing is the standard ABI way to access global symbols -/// in position-independent code without needing a GOT entry. -#[test] -fn test_emit_symbol_address_uses_rip_relative_on_linux_x86_64() { - let mut emitter = test_emitter_x86(); - emit_symbol_address(&mut emitter, "r11", "_demo_symbol"); - - assert_eq!(emitter.output(), " lea r11, [rip + _demo_symbol]\n"); -} - -/// Verifies that extern symbol addresses are accessed via GOTPCREL on x86_64. -/// The emitted instruction is "mov reg, QWORD PTR symbol@GOTPCREL[rip]" which -/// loads the symbol's address from the Global Offset Table using a PC-relative -/// relocation. This is the standard PIC mechanism for accessing symbols -/// imported from shared libraries. -#[test] -fn test_emit_extern_symbol_address_uses_gotpcrel_on_linux_x86_64() { - let mut emitter = test_emitter_x86(); - crate::codegen::abi::symbols::emit_extern_symbol_address(&mut emitter, "r11", "demo_extern"); - - assert_eq!( - emitter.output(), - " mov r11, QWORD PTR demo_extern@GOTPCREL[rip]\n" - ); -} - -/// Verifies that load_extern_symbol_to_reg and store_reg_to_extern_symbol -/// emit the standard x86_64 sequence for accessing extern data: load the -/// symbol address via GOTPCREL into a scratch register, then dereference with -/// offset. The helpers load/store both integer (QWORD PTR) and floating-point -/// (movsd) values at arbitrary offsets. -#[test] -fn test_emit_load_and_store_extern_symbol_linux_x86_64_use_shared_helpers() { - let mut emitter = test_emitter_x86(); - emit_load_extern_symbol_to_reg(&mut emitter, "rax", "demo_extern", 0); - emit_load_extern_symbol_to_reg(&mut emitter, "xmm0", "demo_extern", 8); - emit_store_reg_to_extern_symbol(&mut emitter, "r10", "demo_extern", 0); - emit_store_reg_to_extern_symbol(&mut emitter, "xmm1", "demo_extern", 8); - - assert_eq!( - emitter.output(), - concat!( - " mov r11, QWORD PTR demo_extern@GOTPCREL[rip]\n", - " mov rax, QWORD PTR [r11]\n", - " mov r11, QWORD PTR demo_extern@GOTPCREL[rip]\n", - " movsd xmm0, QWORD PTR [r11 + 8]\n", - " mov r11, QWORD PTR demo_extern@GOTPCREL[rip]\n", - " mov QWORD PTR [r11], r10\n", - " mov r11, QWORD PTR demo_extern@GOTPCREL[rip]\n", - " movsd QWORD PTR [r11 + 8], xmm1\n", - ) - ); -} - -/// Verifies that emit_store_zero_to_symbol uses RIP-relative LEA to get the -/// symbol address, then emits a "mov QWORD PTR [reg + offset], 0" to store -/// zero at that location. This is the standard x86_64 sequence for storing -/// an immediate zero to a global variable. -#[test] -fn test_emit_store_zero_to_symbol_uses_native_zero_store_on_linux_x86_64() { - let mut emitter = test_emitter_x86(); - emit_store_zero_to_symbol(&mut emitter, "_demo_symbol", 8); - - assert_eq!( - emitter.output(), - concat!( - " lea r11, [rip + _demo_symbol]\n", - " mov QWORD PTR [r11 + 8], 0\n", - ) - ); -} - -/// Verifies that emit_branch_if_int_result_zero emits "test rax, rax / je label" -/// to branch when the integer in rax is zero; emit_branch_if_int_result_nonzero -/// emits "test rax, rax / jne label". The test rax, rax idiom sets the ZF flag -/// based on the register value without modifying it, which is the standard -/// zero/nonzero check pattern on x86_64. -#[test] -fn test_emit_branch_helpers_use_native_zero_checks_on_linux_x86_64() { - let mut emitter = test_emitter_x86(); - emit_branch_if_int_result_zero(&mut emitter, "zero_label"); - emit_branch_if_int_result_nonzero(&mut emitter, "nonzero_label"); - - assert_eq!( - emitter.output(), - concat!( - " test rax, rax\n", - " je zero_label\n", - " test rax, rax\n", - " jne nonzero_label\n", - ) - ); -} - -/// Verifies that emit_store_result_to_symbol stores a string result (pointer -/// in rax, length in rdx) via RIP-relative mov, and emit_load_symbol_to_result -/// loads it back. For strings, the result is stored as two adjacent QWORDs -/// at the symbol: [symbol] = pointer, [symbol + 8] = length. -#[test] -fn test_emit_store_and_load_result_to_symbol_for_string_linux_x86_64() { - let mut emitter = test_emitter_x86(); - emit_store_result_to_symbol(&mut emitter, "_demo_symbol", &PhpType::Str, false); - emit_load_symbol_to_result(&mut emitter, "_demo_symbol", &PhpType::Str); - let out = emitter.output(); - - assert!(out.contains(" mov QWORD PTR [rip + _demo_symbol], rax\n")); - assert!(out.contains(" mov QWORD PTR [r11 + 8], rdx\n")); - assert!(out.contains(" mov rax, QWORD PTR [rip + _demo_symbol]\n")); - assert!(out.contains(" mov rdx, QWORD PTR [r11 + 8]\n")); -} - -/// Verifies that process-entry helpers emit correct x86_64 instructions: -/// emit_store_process_args_to_globals stores argc (rdi) and argv (rsi) to -/// global symbols; emit_enable_heap_debug_flag sets the heap debug flag; -/// emit_copy_frame_pointer copies rbp to a destination register; and -/// emit_exit emits the exit syscall (syscall with eax=60, edi=exit_code). -#[test] -fn test_process_entry_helpers_linux_x86_64() { - let mut emitter = test_emitter_x86(); - - emit_store_process_args_to_globals(&mut emitter); - emit_enable_heap_debug_flag(&mut emitter); - emit_copy_frame_pointer(&mut emitter, "r10"); - emit_exit(&mut emitter, 7); - - let out = emitter.output(); - - assert!(out.contains(" mov QWORD PTR [rip + _global_argc], rdi\n")); - assert!(out.contains(" mov QWORD PTR [rip + _global_argv], rsi\n")); - assert!(out.contains(" mov r10, 1\n")); - assert!(out.contains(" mov QWORD PTR [rip + _heap_debug_enabled], r10\n")); - assert!(out.contains(" mov r10, rbp\n")); - assert!(out.contains(" mov edi, 7\n")); - assert!(out.contains(" mov eax, 60\n")); - assert!(out.contains(" syscall\n")); -} - -/// Verifies that emit_store_incoming_param emits correct instructions for -/// each SysV AMD64 argument type and position: integer arguments come from -/// rdi/rsi/rdx/rcx/r8/r9, floating-point arguments from xmm0-xmm7, and -/// arguments beyond the register arguments come from the caller stack at -/// [rbp + 16]. The cursor tracks register vs. stack parameters and advances -/// after each call. -#[test] -fn test_emit_store_incoming_param_linux_x86_64_uses_sysv_registers_and_stack() { - let mut emitter = test_emitter_x86(); - let mut cursor = - IncomingArgCursor::for_target(Target::new(Platform::Linux, Arch::X86_64), 0); - - emit_store_incoming_param(&mut emitter, "a", &PhpType::Int, 8, false, &mut cursor); - emit_store_incoming_param(&mut emitter, "b", &PhpType::Float, 16, false, &mut cursor); - emit_store_incoming_param(&mut emitter, "c", &PhpType::Str, 32, false, &mut cursor); - - let mut stack_cursor = - IncomingArgCursor::for_target(Target::new(Platform::Linux, Arch::X86_64), 6); - emit_store_incoming_param(&mut emitter, "d", &PhpType::Int, 40, false, &mut stack_cursor); - - let out = emitter.output(); - - assert!(out.contains(" # param $a from rdi\n")); - assert!(out.contains(" mov QWORD PTR [rbp - 8], rdi\n")); - assert!(out.contains(" # param $b from xmm0\n")); - assert!(out.contains(" movsd QWORD PTR [rbp - 16], xmm0\n")); - assert!(out.contains(" # param $c from rsi,rdx\n")); - assert!(out.contains(" mov QWORD PTR [rbp - 32], rsi\n")); - assert!(out.contains(" mov QWORD PTR [rbp - 24], rdx\n")); - assert!(out.contains(" # param $d from caller stack +16\n")); - assert!(out.contains(" mov r10, QWORD PTR [rbp + 16]\n")); - assert!(out.contains(" mov QWORD PTR [rbp - 40], r10\n")); -} - -/// Verifies that call and temporary-stack helpers emit correct x86_64 code: -/// emit_push_reg / emit_pop_reg handle general-purpose register save/restore; -/// emit_push_float_reg / emit_pop_float_reg handle XMM register save/restore; -/// emit_push_reg_pair / emit_pop_reg_pair handle register pair (two QWORDs); -/// emit_reserve_temporary_stack / emit_release_temporary_stack manage a -/// temporary region of the stack with sub rsp / add rsp; emit_temporary_stack_address -/// computes the address of a slot within that region; emit_load_temporary_stack_slot -/// loads from it; emit_call_label emits a direct call; emit_call_reg emits an -/// indirect call via register; and emit_store_zero_to_local_slot stores zero -/// to a frame-local slot. -#[test] -fn test_emit_call_and_temporary_stack_helpers_linux_x86_64() { - let mut emitter = test_emitter_x86(); - - emit_push_reg(&mut emitter, "r12"); - super::calls::emit_pop_reg(&mut emitter, "r12"); - super::calls::emit_push_float_reg(&mut emitter, "xmm3"); - emit_pop_float_reg(&mut emitter, "xmm3"); - super::calls::emit_push_reg_pair(&mut emitter, "rax", "rdx"); - emit_pop_reg_pair(&mut emitter, "rax", "rdx"); - emit_reserve_temporary_stack(&mut emitter, 32); - emit_temporary_stack_address(&mut emitter, "r10", 16); - emit_load_temporary_stack_slot(&mut emitter, "r11", 24); - emit_call_label(&mut emitter, "_fn_demo"); - emit_call_reg(&mut emitter, "r12"); - emit_release_temporary_stack(&mut emitter, 32); - emit_store_zero_to_local_slot(&mut emitter, 24); - - assert_eq!( - emitter.output(), - concat!( - " sub rsp, 16\n", - " mov QWORD PTR [rsp], r12\n", - " mov r12, QWORD PTR [rsp]\n", - " add rsp, 16\n", - " sub rsp, 16\n", - " movsd QWORD PTR [rsp], xmm3\n", - " movsd xmm3, QWORD PTR [rsp]\n", - " add rsp, 16\n", - " sub rsp, 16\n", - " mov QWORD PTR [rsp], rax\n", - " mov QWORD PTR [rsp + 8], rdx\n", - " mov rax, QWORD PTR [rsp]\n", - " mov rdx, QWORD PTR [rsp + 8]\n", - " add rsp, 16\n", - " sub rsp, 32\n", - " lea r10, [rsp + 16]\n", - " mov r11, QWORD PTR [rsp + 24]\n", - " call _fn_demo\n", - " call r12\n", - " add rsp, 32\n", - " mov QWORD PTR [rbp - 24], 0\n", - ) - ); -} - -/// Verifies that emit_push_result_value emits the correct x86_64 instructions -/// to push a return value onto the stack for a callee-saved register fixup: -/// PhpType::Int uses rax (mov QWORD PTR [rsp], rax); PhpType::Float uses -/// xmm0 (movsd QWORD PTR [rsp], xmm0); PhpType::Str uses rax+rdx for -/// pointer and length (two QWORDs on the stack). -#[test] -fn test_emit_push_result_value_linux_x86_64_uses_native_result_registers() { - let mut emitter = test_emitter_x86(); - - emit_push_result_value(&mut emitter, &PhpType::Int); - emit_push_result_value(&mut emitter, &PhpType::Float); - emit_push_result_value(&mut emitter, &PhpType::Str); - - assert_eq!( - emitter.output(), - concat!( - " sub rsp, 16\n", - " mov QWORD PTR [rsp], rax\n", - " sub rsp, 16\n", - " movsd QWORD PTR [rsp], xmm0\n", - " sub rsp, 16\n", - " mov QWORD PTR [rsp], rax\n", - " mov QWORD PTR [rsp + 8], rdx\n", - ) - ); -} - -/// Verifies that emit_write_stdout routes the terminal x86_64 write through the -/// `__rt_stdout_write` runtime indirection: the integer argument is converted via -/// `__rt_itoa` (leaving the pointer in rax and the length in rdx), then the string -/// result registers are moved into the `__rt_stdout_write` calling convention -/// (rdi=byte pointer, rsi=length) and the helper is called. The actual `write(1, …)` -/// syscall now lives inside `__rt_stdout_write`, not at this call site. -#[test] -fn test_emit_write_stdout_linux_x86_64_routes_through_stdout_write() { - let mut emitter = test_emitter_x86(); - - emit_write_stdout(&mut emitter, &PhpType::Int); - - assert_eq!( - emitter.output(), - concat!( - " call __rt_itoa\n", - " mov rsi, rdx\n", - " mov rdi, rax\n", - " call __rt_stdout_write\n", - ) - ); -} - -/// Constructs a PIC-mode x86_64/Linux emitter for verifying the GOT-routed -/// data-reference sequences used by `--emit cdylib` artifacts. -fn test_emitter_x86_pic() -> Emitter { - Emitter::new_pic(Target::new(Platform::Linux, Arch::X86_64)) -} - -/// Verifies that a PIC-mode integer symbol load resolves the GOT entry through -/// the destination register itself, so the sequence clobbers no register the -/// non-PIC emission would have left intact. -#[test] -fn test_pic_load_symbol_to_int_reg_self_scratches_on_linux_x86_64() { - let mut emitter = test_emitter_x86_pic(); - crate::codegen::abi::emit_load_symbol_to_reg(&mut emitter, "r10", "_concat_off", 0); - - assert_eq!( - emitter.output(), - concat!( - " mov r10, QWORD PTR _concat_off@GOTPCREL[rip]\n", - " mov r10, QWORD PTR [r10]\n", - ) - ); -} - -/// Verifies that a PIC-mode float symbol load borrows r11 for the GOT address -/// behind a push/pop pair, preserving the caller-visible register state. -#[test] -fn test_pic_load_symbol_to_float_reg_protects_r11_on_linux_x86_64() { - let mut emitter = test_emitter_x86_pic(); - crate::codegen::abi::emit_load_symbol_to_reg(&mut emitter, "xmm0", "_float_slot", 8); - - assert_eq!( - emitter.output(), - concat!( - " push r11\n", - " mov r11, QWORD PTR _float_slot@GOTPCREL[rip]\n", - " movsd xmm0, QWORD PTR [r11 + 8]\n", - " pop r11\n", - ) - ); -} - -/// Verifies that a PIC-mode symbol store borrows r11 behind a push/pop, and -/// falls back to r10 when the stored value itself lives in r11. -#[test] -fn test_pic_store_reg_to_symbol_protects_scratch_on_linux_x86_64() { - let mut emitter = test_emitter_x86_pic(); - crate::codegen::abi::emit_store_reg_to_symbol(&mut emitter, "rax", "_concat_off", 0); - crate::codegen::abi::emit_store_reg_to_symbol(&mut emitter, "r11", "_concat_off", 0); - - assert_eq!( - emitter.output(), - concat!( - " push r11\n", - " mov r11, QWORD PTR _concat_off@GOTPCREL[rip]\n", - " mov QWORD PTR [r11], rax\n", - " pop r11\n", - " push r10\n", - " mov r10, QWORD PTR _concat_off@GOTPCREL[rip]\n", - " mov QWORD PTR [r10], r11\n", - " pop r10\n", - ) - ); -} - -/// Verifies that a PIC-mode zero store writes an immediate zero through the -/// GOT-resolved address instead of routing a zeroed register through the GOT -/// scratch (the previous sequence clobbered the zero with the address). -#[test] -fn test_pic_store_zero_to_symbol_writes_immediate_on_linux_x86_64() { - let mut emitter = test_emitter_x86_pic(); - crate::codegen::abi::emit_store_zero_to_symbol(&mut emitter, "_eof_flags", 0); - - assert_eq!( - emitter.output(), - concat!( - " push r11\n", - " mov r11, QWORD PTR _eof_flags@GOTPCREL[rip]\n", - " mov QWORD PTR [r11], 0\n", - " pop r11\n", - ) - ); -} - -/// Verifies that the symbol-compare helper emits a single memory-operand CMP -/// in non-PIC mode and a flag-safe push/GOT/cmp/pop sequence in PIC mode. -#[test] -fn test_cmp_reg_to_symbol_non_pic_and_pic_on_linux_x86_64() { - let mut emitter = test_emitter_x86(); - crate::codegen::abi::emit_cmp_reg_to_symbol(&mut emitter, "rax", "_class_destruct_count"); - assert_eq!( - emitter.output(), - " cmp rax, QWORD PTR [rip + _class_destruct_count]\n" - ); - - let mut emitter = test_emitter_x86_pic(); - crate::codegen::abi::emit_cmp_reg_to_symbol(&mut emitter, "rax", "_class_destruct_count"); - assert_eq!( - emitter.output(), - concat!( - " push r11\n", - " mov r11, QWORD PTR _class_destruct_count@GOTPCREL[rip]\n", - " cmp rax, QWORD PTR [r11]\n", - " pop r11\n", - ) - ); -} - -/// Verifies that the symbol-decrement helper emits a single memory-operand DEC -/// in non-PIC mode and a push/GOT/dec/pop sequence in PIC mode. -#[test] -fn test_dec_symbol_non_pic_and_pic_on_linux_x86_64() { - let mut emitter = test_emitter_x86(); - crate::codegen::abi::emit_dec_symbol(&mut emitter, "_http_active_max_redirects"); - assert_eq!( - emitter.output(), - " dec QWORD PTR [rip + _http_active_max_redirects]\n" - ); - - let mut emitter = test_emitter_x86_pic(); - crate::codegen::abi::emit_dec_symbol(&mut emitter, "_http_active_max_redirects"); - assert_eq!( - emitter.output(), - concat!( - " push r11\n", - " mov r11, QWORD PTR _http_active_max_redirects@GOTPCREL[rip]\n", - " dec QWORD PTR [r11]\n", - " pop r11\n", - ) - ); -} diff --git a/src/codegen/abi/tests/symbols.rs b/src/codegen/abi/tests/symbols.rs deleted file mode 100644 index 30aa557b25..0000000000 --- a/src/codegen/abi/tests/symbols.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Purpose: -//! Contains ABI regression tests for symbols helper behavior. -//! Checks emitted assembly fragments rather than running linked programs. -//! -//! Called from: -//! - `crate::codegen::abi::tests` through Rust test harness -//! -//! Key details: -//! - Assertions pin register, stack, relocation, and platform-specific instruction choices. - -use super::*; - -/// Verifies `emit_symbol_address` uses platform-appropriate relocations (ADRP + ADD -/// with @PAGE/@PAGEOFF) rather than raw immediates on ARM64. -#[test] -fn test_emit_symbol_address_uses_platform_relocations() { - let mut emitter = test_emitter(); - emit_symbol_address(&mut emitter, "x9", "_demo_symbol"); - - assert_eq!( - emitter.output(), - concat!( - " adrp x9, _demo_symbol@PAGE\n", - " add x9, x9, _demo_symbol@PAGEOFF\n", - ) - ); -} - -/// Checks that `emit_store_result_to_symbol` stores both registers of a string (ptr in x1, -/// len in x2) at the symbol address, and that `emit_load_symbol_to_result` reverses the -/// operation correctly. Verifies str/ldr pair for x1 and x2. -#[test] -fn test_emit_store_and_load_result_to_symbol_for_string() { - let mut emitter = test_emitter(); - emit_store_result_to_symbol(&mut emitter, "_demo_symbol", &PhpType::Str, false); - emit_load_symbol_to_result(&mut emitter, "_demo_symbol", &PhpType::Str); - let out = emitter.output(); - - assert!(out.contains(" str x1, [x9]\n")); - assert!(out.contains(" str x2, [x9, #8]\n")); - assert!(out.contains(" ldr x1, [x9]\n")); - assert!(out.contains(" ldr x2, [x9, #8]\n")); -} - -/// Verifies `emit_extern_symbol_address` on ARM64 emits GOT-relative relocations -/// (ADRP + ldr via @GOTPAGE/@GOTPAGEOFF) rather than direct symbol addressing. -#[test] -fn test_emit_extern_symbol_address_uses_got_relocations_on_aarch64() { - let mut emitter = test_emitter(); - crate::codegen::abi::symbols::emit_extern_symbol_address(&mut emitter, "x9", "_demo_extern"); - - assert_eq!( - emitter.output(), - concat!( - " adrp x9, _demo_extern@GOTPAGE\n", - " ldr x9, [x9, _demo_extern@GOTPAGEOFF]\n", - ) - ); -} diff --git a/src/codegen/abi/values.rs b/src/codegen/abi/values.rs deleted file mode 100644 index 5705b42354..0000000000 --- a/src/codegen/abi/values.rs +++ /dev/null @@ -1,392 +0,0 @@ -//! Purpose: -//! Provides type-directed load, store, branch, jump, conversion, and refcount helpers for result values. -//! Normalizes scalar, string, array, object, and Mixed value movement across emitters. -//! -//! Called from: -//! - `crate::codegen::expr`, `crate::codegen::stmt`, and function cleanup emitters -//! -//! Key details: -//! - Refcounted values require balanced retain/release behavior around borrowed and owned temporaries. - -use crate::codegen::callable_descriptor; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::types::PhpType; - -use super::calls::{emit_call_label, emit_pop_reg, emit_push_reg}; -use super::frame::{emit_load_from_address, load_at_offset, store_at_offset}; -use super::registers::{float_result_reg, int_result_reg, string_result_regs}; -use crate::codegen::sentinels::tagged_scalar_tag_reg; - -/// Stores the current result value (in result registers) of the given type at a stack frame offset. -/// -/// For `PhpType::Str`, calls `__rt_str_persist` to copy the string into owned heap storage, -/// then stores the pointer and length as separate values. For `PhpType::Void`/`Never`, stores -/// a null sentinel. Refcounted types (array, object, etc.) are stored directly without -/// incrementing the refcount—the caller owns the result register value. -pub fn emit_store(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - match ty { - PhpType::Bool | PhpType::Int | PhpType::Resource(_) => { - store_at_offset(emitter, int_result_reg(emitter), offset); // store scalar integer-like value to stack - } - PhpType::Float => { - store_at_offset(emitter, float_result_reg(emitter), offset); // store float to stack - } - PhpType::Str => { - emit_call_label(emitter, "__rt_str_persist"); // copy the current string payload into owned heap storage when needed - let (ptr_reg, len_reg) = string_result_regs(emitter); - store_at_offset(emitter, ptr_reg, offset); // store string pointer - store_at_offset(emitter, len_reg, offset - 8); // store string length - } - PhpType::Void | PhpType::Never => { - store_at_offset(emitter, int_result_reg(emitter), offset); // store null sentinel - } - PhpType::TaggedScalar => { - store_at_offset(emitter, int_result_reg(emitter), offset); // store tagged scalar payload word - store_at_offset(emitter, tagged_scalar_tag_reg(emitter), offset - 8); // store tagged scalar tag word - } - PhpType::Iterable - | PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - store_at_offset(emitter, int_result_reg(emitter), offset); // store array/callable/object/pointer value - } - } -} - -/// Retains a refcounted result value before it is reused or overwritten. -/// -/// If `ty` is a refcounted heap value or callable descriptor, emits a retain call after -/// preserving the heap pointer in a temporary slot. The target architecture dictates -/// register preservation conventions: AArch64 uses a pre-decrement stack store, while -/// x86_64 uses a 16-byte aligned push/pop pair to maintain SysV ABI alignment. -pub fn emit_incref_if_refcounted(emitter: &mut Emitter, ty: &PhpType) { - if matches!(ty, PhpType::Callable) { - callable_descriptor::emit_retain_current_descriptor(emitter); - return; - } - if ty.is_refcounted() { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve heap pointer across incref helper call - emitter.instruction("bl __rt_incref"); // retain shared heap value before creating a new owner - emitter.instruction("ldr x0, [sp], #16"); // restore original heap pointer after incref - } - Arch::X86_64 => { - emit_push_reg(emitter, "rax"); // preserve the heap pointer in a 16-byte temporary slot to keep the SysV stack aligned across the helper call - emitter.instruction("call __rt_incref"); // retain shared heap value before creating a new owner - emit_pop_reg(emitter, "rax"); // restore the original heap pointer after the aligned incref helper call - } - } - } -} - -/// Releases a refcounted result value held in `x0`. -/// -/// Dispatches to the appropriate runtime helper based on the PHP type: -/// - `Mixed`/`Union` → `__rt_decref_mixed` -/// - `Array` → `__rt_decref_array` -/// - `AssocArray` → `__rt_decref_hash` -/// - `Object` → `__rt_decref_object` -/// - `Iterable` → `__rt_decref_any` (inspects heap kind) -/// - `Callable` → `__rt_callable_descriptor_release` -/// - Non-refcounted types → no-op -pub fn emit_decref_if_refcounted(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Mixed | PhpType::Union(_) => { - emit_call_label(emitter, "__rt_decref_mixed"); // release mixed cell reference - } - PhpType::Array(_) => { - emit_call_label(emitter, "__rt_decref_array"); // release indexed array reference - } - PhpType::AssocArray { .. } => { - emit_call_label(emitter, "__rt_decref_hash"); // release associative array reference - } - PhpType::Object(_) => { - emit_call_label(emitter, "__rt_decref_object"); // release object reference - } - PhpType::Iterable => { - emit_call_label(emitter, "__rt_decref_any"); // release the erased iterable payload by inspecting its heap kind - } - PhpType::Callable => { - callable_descriptor::emit_release_current_descriptor(emitter); - } - _ => {} - } -} - -/// Releases the payload of a local reference-counted cell and the cell itself. -/// -/// Pushes `cell_reg` as a temporary, then: -/// - For `PhpType::Str`: loads the string payload and calls `__rt_heap_free_safe`. -/// - For other refcounted types: loads the heap pointer and calls `emit_decref_if_refcounted`. -/// Pops the preserved cell pointer and calls `__rt_heap_free` to release the cell. -/// Used during function epilogue for local variables that held borrowed or owned refs. -pub fn emit_release_local_ref_cell(emitter: &mut Emitter, cell_reg: &str, value_ty: &PhpType) { - emit_push_reg(emitter, cell_reg); // preserve the owned reference cell pointer while releasing its payload - match value_ty.codegen_repr() { - PhpType::Str => { - emit_load_from_address(emitter, int_result_reg(emitter), cell_reg, 0); - emit_call_label(emitter, "__rt_heap_free_safe"); // release the owned string payload stored inside the local reference cell - } - ty if ty.is_refcounted() => { - emit_load_from_address(emitter, int_result_reg(emitter), cell_reg, 0); - emit_decref_if_refcounted(emitter, &ty); - } - PhpType::Callable => { - emit_load_from_address(emitter, int_result_reg(emitter), cell_reg, 0); - callable_descriptor::emit_release_current_descriptor(emitter); - } - _ => {} - } - emit_pop_reg(emitter, int_result_reg(emitter)); // restore the owned reference cell pointer for heap release - emit_call_label(emitter, "__rt_heap_free"); // release the local reference cell itself -} - -/// Loads a value of the given type from a stack frame offset into result registers. -/// -/// For `PhpType::Str`, loads both the string pointer and length. For scalar types (bool, int, -/// float, resource), loads into the appropriate result register. For void/never, loads a null -/// sentinel. For compound types (array, object, callable, pointer, etc.), loads the heap pointer. -pub fn emit_load(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - match ty { - PhpType::Bool | PhpType::Int | PhpType::Resource(_) => { - load_at_offset(emitter, int_result_reg(emitter), offset); // load scalar integer-like value from stack - } - PhpType::Float => { - load_at_offset(emitter, float_result_reg(emitter), offset); // load float from stack - } - PhpType::Str => { - let (ptr_reg, len_reg) = string_result_regs(emitter); - load_at_offset(emitter, ptr_reg, offset); // load string pointer - load_at_offset(emitter, len_reg, offset - 8); // load string length - } - PhpType::Void | PhpType::Never => { - load_at_offset(emitter, int_result_reg(emitter), offset); // load null sentinel - } - PhpType::TaggedScalar => { - load_at_offset(emitter, int_result_reg(emitter), offset); // load tagged scalar payload word - load_at_offset(emitter, tagged_scalar_tag_reg(emitter), offset - 8); // load tagged scalar tag word - } - PhpType::Iterable - | PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - load_at_offset(emitter, int_result_reg(emitter), offset); // load array/callable/object/pointer value - } - } -} - -/// Branches to `label` when the integer result register is zero (coerced truthiness). -/// -/// AArch64: `cbz` (compare and branch if zero). x86_64: `test` + `je` (set cc + conditional jump). -/// The integer result represents a coerced PHP truthiness value used in conditional contexts. -pub fn emit_branch_if_int_result_zero(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("cbz {}, {}", int_result_reg(emitter), label)); // branch when the coerced integer truthiness result is zero - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("test {}, {}", int_result_reg(emitter), int_result_reg(emitter))); // test whether the coerced integer truthiness result is zero - emitter.instruction(&format!("je {}", label)); // branch when the coerced integer truthiness result is zero - } - } -} - -/// Branches to `label` when the integer result register is non-zero (coerced truthiness). -/// -/// AArch64: `cbnz` (compare and branch if non-zero). x86_64: `test` + `jne` (set cc + conditional jump). -/// The integer result represents a coerced PHP truthiness value used in conditional contexts. -pub fn emit_branch_if_int_result_nonzero(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("cbnz {}, {}", int_result_reg(emitter), label)); // branch when the coerced integer truthiness result is non-zero - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("test {}, {}", int_result_reg(emitter), int_result_reg(emitter))); // test whether the coerced integer truthiness result is non-zero - emitter.instruction(&format!("jne {}", label)); // branch when the coerced integer truthiness result is non-zero - } - } -} - -/// Unconditionally jumps to `label` for control flow transfer. -/// -/// AArch64 uses `b label`. x86_64 uses `jmp label`. -pub fn emit_jump(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("b {}", label)); // jump unconditionally to the target label - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("jmp {}", label)); // jump unconditionally to the target label - } - } -} - -/// Promotes the integer result register value to the floating-point result register. -/// -/// AArch64: `scvtf` (signed convert to floating). x86_64: `cvtsi2sd` (signed integer to scalar double). -/// Used when a PHP int must be coerced to a float in mixed arithmetic contexts. -pub fn emit_int_result_to_float_result(emitter: &mut Emitter) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - let inst = format!("scvtf {}, {}", float_result_reg(emitter), int_result_reg(emitter)); - emitter.instruction(&inst); // promote the integer result into the floating-point result register - } - crate::codegen::platform::Arch::X86_64 => { - let inst = format!("cvtsi2sd {}, {}", float_result_reg(emitter), int_result_reg(emitter)); - emitter.instruction(&inst); // promote the integer result into the floating-point result register - } - } -} - -/// Truncates the floating-point result register value to the integer result register. -/// -/// AArch64: `fcvtzs` (floating-point convert to signed fixed-point). x86_64: `cvttsd2si` (convert with truncation). -/// Used when a PHP float must be coerced to int in mixed arithmetic contexts. -pub fn emit_float_result_to_int_result(emitter: &mut Emitter) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - let inst = format!("fcvtzs {}, {}", int_result_reg(emitter), float_result_reg(emitter)); - emitter.instruction(&inst); // truncate the floating-point result into the integer result register - } - crate::codegen::platform::Arch::X86_64 => { - let inst = format!("cvttsd2si {}, {}", int_result_reg(emitter), float_result_reg(emitter)); - emitter.instruction(&inst); // truncate the floating-point result into the integer result register - } - } -} - -/// Loads a 64-bit immediate integer `value` into `reg`. -/// -/// AArch64: uses `mov` for values in [−65536, 65535]; otherwise constructs the value using -/// `movz`/`movk` pairs for each 16-bit chunk (low, bits 16–31, 32–47, 48–63). -/// x86_64: a single `mov` instruction handles any immediate since x86_64 immediates are 32-bit -/// sign-extended to 64-bit. -pub fn emit_load_int_immediate(emitter: &mut Emitter, reg: &str, value: i64) { - match emitter.target.arch { - Arch::AArch64 => { - if (0..=65535).contains(&value) { - emitter.instruction(&format!("mov {}, #{}", reg, value)); // load a small non-negative immediate directly into the target register - } else if (-65536..0).contains(&value) { - emitter.instruction(&format!("mov {}, #{}", reg, value)); // load a small negative immediate directly into the target register - } else { - let uval = value as u64; - emitter.instruction(&format!("movz {}, #0x{:x}", reg, uval & 0xFFFF)); // seed the low 16 bits of the wider immediate value - if (uval >> 16) & 0xFFFF != 0 { - emitter.instruction(&format!( // patch bits 16-31 of the wider immediate value - "movk {}, #0x{:x}, lsl #16", - reg, - (uval >> 16) & 0xFFFF - )); - } - if (uval >> 32) & 0xFFFF != 0 { - emitter.instruction(&format!( // patch bits 32-47 of the wider immediate value - "movk {}, #0x{:x}, lsl #32", - reg, - (uval >> 32) & 0xFFFF - )); - } - if (uval >> 48) & 0xFFFF != 0 { - emitter.instruction(&format!( // patch bits 48-63 of the wider immediate value - "movk {}, #0x{:x}, lsl #48", - reg, - (uval >> 48) & 0xFFFF - )); - } - } - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", reg, value)); // load the immediate directly into the native x86_64 register - } - } -} - -/// Writes a result value of the given PHP type to stdout. -/// -/// Dispatches to the appropriate runtime helper: -/// - `Str` → `emit_write_current_string_stdout` directly -/// - `Bool`/`Int` → `__rt_itoa` then `emit_write_current_string_stdout` -/// - `Float` → `__rt_ftoa` then `emit_write_current_string_stdout` -/// - `Pointer`/`Buffer`/`Packed` → `__rt_ptoa` then `emit_write_current_string_stdout` -/// - `Resource` → `__rt_resource_write_stdout` -/// - `Mixed` → `__rt_mixed_write_stdout` -/// - `Iterable` → `__rt_iterable_write_stdout` -/// - Other types (void, array, callable, object) → no-op -pub fn emit_write_stdout(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Str => { - emit_write_current_string_stdout(emitter); - } - PhpType::Bool | PhpType::Int => { - emit_call_label(emitter, "__rt_itoa"); - emit_write_current_string_stdout(emitter); - } - PhpType::TaggedScalar => { - emit_call_label(emitter, "__rt_itoa"); // convert the tagged scalar payload; callers suppress the null case first - emit_write_current_string_stdout(emitter); - } - PhpType::Resource(_) => { - emit_call_label(emitter, "__rt_resource_write_stdout"); - } - PhpType::Float => { - emit_call_label(emitter, "__rt_ftoa"); - emit_write_current_string_stdout(emitter); - } - PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) => { - emit_call_label(emitter, "__rt_ptoa"); - emit_write_current_string_stdout(emitter); - } - PhpType::Mixed | PhpType::Union(_) => { - emit_call_label(emitter, "__rt_mixed_write_stdout"); - } - PhpType::Iterable => { - emit_call_label(emitter, "__rt_iterable_write_stdout"); // dispatch echo iterable through the heap-kind-aware writer instead of the mixed-cell writer - } - PhpType::Void - | PhpType::Never - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Callable - | PhpType::Object(_) => {} - } -} - -/// Writes the current string result registers to stdout through `__rt_stdout_write`. -/// -/// Moves the string pointer/length out of the platform string result registers and -/// into the `__rt_stdout_write` calling convention (byte pointer in `x0`/`rdi`, -/// length in `x1`/`rsi`), then calls the runtime indirection. That routine performs -/// the actual `write(1, ptr, len)` syscall (or, in `--web` builds with capture -/// enabled, hands the bytes to `elephc_web_write`). -/// -/// AArch64: string result regs are `(x1, x2)` → set `x0=x1`, `x1=x2`. -/// x86_64: string result regs are `(rax, rdx)` → set `rdi=rax`, `rsi=rdx`. -fn emit_write_current_string_stdout(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - let (ptr_reg, len_reg) = string_result_regs(emitter); - emitter.instruction(&format!("mov x0, {}", ptr_reg)); // stdout_write ptr arg = current string pointer (copy before x1 is overwritten with the length, since ptr lives in x1) - emitter.instruction(&format!("mov x1, {}", len_reg)); // stdout_write len arg = current string length - emit_call_label(emitter, "__rt_stdout_write"); // route the terminal write through the stdout-write indirection - } - Arch::X86_64 => { - let (ptr_reg, len_reg) = string_result_regs(emitter); - emitter.instruction(&format!("mov rsi, {}", len_reg)); // stdout_write len arg = current string length - emitter.instruction(&format!("mov rdi, {}", ptr_reg)); // stdout_write ptr arg = current string pointer - emit_call_label(emitter, "__rt_stdout_write"); // route the terminal write through the stdout-write indirection - } - } -} diff --git a/src/codegen_ir/block_emit.rs b/src/codegen/block_emit.rs similarity index 85% rename from src/codegen_ir/block_emit.rs rename to src/codegen/block_emit.rs index cf5cd7ad73..480b8c3c95 100644 --- a/src/codegen_ir/block_emit.rs +++ b/src/codegen/block_emit.rs @@ -3,7 +3,7 @@ //! Owns function setup for the initial Phase 04 backend path. //! //! Called from: -//! - `crate::codegen_ir::generate_user_asm_from_ir()`. +//! - `crate::codegen::generate_user_asm_from_ir()`. //! //! Key details: //! - This first backend increment supports straight-line main blocks and reports @@ -12,13 +12,13 @@ //! user blocks run. use crate::codegen::abi; -use crate::codegen::context::DeferredFiberWrapper; use crate::codegen::data_section::DataSection; -use crate::codegen::emit_fiber_wrapper; use crate::codegen::emit::Emitter; +use crate::codegen::emit_fiber_wrapper; use crate::codegen::platform::Arch; use crate::codegen::Emit; use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; +use crate::codegen_support::DeferredFiberWrapper; use crate::ir::{BasicBlock, Function, InstId, Module}; use crate::names::{ enum_case_symbol, function_epilogue_symbol, function_symbol, method_symbol, php_symbol_key, @@ -63,7 +63,11 @@ pub(super) fn emit_module( web: bool, ) -> Result<()> { function_variants::emit_dispatchers(module, emitter, data); - for function in module.functions.iter().filter(|function| !is_main(function)) { + for function in module + .functions + .iter() + .filter(|function| !is_main(function)) + { emit_user_function(module, function, emitter, data, regalloc_linear)?; } for method in &module.class_methods { @@ -189,6 +193,34 @@ fn emit_user_function( Ok(()) } +/// Emits a synthetic EIR function body at an explicit already-published entry label. +pub(super) fn emit_synthetic_function_with_label( + module: &Module, + function: &Function, + entry_label: &str, + emitter: &mut Emitter, + data: &mut DataSection, + regalloc_linear: bool, +) -> Result<()> { + let layout = frame::layout_for_function(function, emitter.target, regalloc_linear); + let epilogue_label = format!("{}_epilogue", entry_label); + let mut ctx = FunctionContext::new( + module, + function, + emitter, + data, + layout, + false, + false, + false, + Some(epilogue_label), + ); + frame::emit_function_prologue_with_label(&mut ctx, entry_label)?; + emit_blocks(&mut ctx)?; + frame::emit_function_epilogue(&mut ctx); + Ok(()) +} + /// Returns the assembly entry label for a user or synthetic EIR function. fn user_function_entry_symbol(function: &Function) -> String { if is_property_init_thunk(function) { @@ -286,7 +318,14 @@ fn emit_generator_function( ))); } emit_generator_constructor(emitter, entry_label, &callback_label, ¶m_types); - emit_generator_body(module, function, &body_label, emitter, data, regalloc_linear)?; + emit_generator_body( + module, + function, + &body_label, + emitter, + data, + regalloc_linear, + )?; emit_generator_callback(emitter, &callback_label, &body_label, ¶m_types); Ok(()) } @@ -390,24 +429,31 @@ fn emit_generator_constructor( // valid across it. let mut cursor = abi::IncomingArgCursor::for_target(target, 0); for (idx, ty) in param_types.iter().enumerate() { - abi::emit_store_incoming_param(emitter, &format!("arg{idx}"), ty, gen_param_slot(idx), false, &mut cursor); + abi::emit_store_incoming_param( + emitter, + &format!("arg{idx}"), + ty, + gen_param_slot(idx), + false, + &mut cursor, + ); } // -- allocate the Generator coroutine object -- match target.arch { Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // no callable descriptor — the wrapper runs the compiled body + emitter.instruction("mov x0, #0"); // no callable descriptor — the wrapper runs the compiled body abi::emit_load_symbol_to_reg(emitter, "x1", "_generator_class_id", 0); // x1 = runtime class id of Generator - abi::emit_symbol_address(emitter, "x2", callback_label); // x2 = generator coroutine entry wrapper - emitter.instruction("bl __rt_fiber_construct"); // x0 = freshly allocated Generator coroutine object - emitter.instruction("mov x19, x0"); // cache the generator object across the per-parameter boxing calls + abi::emit_symbol_address(emitter, "x2", callback_label); // x2 = generator coroutine entry wrapper + emitter.instruction("bl __rt_fiber_construct"); // x0 = freshly allocated Generator coroutine object + emitter.instruction("mov x19, x0"); // cache the generator object across the per-parameter boxing calls } Arch::X86_64 => { - emitter.instruction("xor edi, edi"); // no callable descriptor — the wrapper runs the compiled body + emitter.instruction("xor edi, edi"); // no callable descriptor — the wrapper runs the compiled body abi::emit_load_symbol_to_reg(emitter, "rsi", "_generator_class_id", 0); // rsi = runtime class id of Generator - abi::emit_symbol_address(emitter, "rdx", callback_label); // rdx = generator coroutine entry wrapper - emitter.instruction("call __rt_fiber_construct"); // rax = freshly allocated Generator coroutine object - emitter.instruction("mov r12, rax"); // cache the generator object across the per-parameter boxing calls + abi::emit_symbol_address(emitter, "rdx", callback_label); // rdx = generator coroutine entry wrapper + emitter.instruction("call __rt_fiber_construct"); // rax = freshly allocated Generator coroutine object + emitter.instruction("mov r12, rax"); // cache the generator object across the per-parameter boxing calls } } @@ -419,8 +465,8 @@ fn emit_generator_constructor( GenParamKind::Mixed => { abi::load_at_offset(emitter, gen_reg, slot); match target.arch { - Arch::AArch64 => emitter.instruction("bl __rt_incref"), // own a reference to the forwarded Mixed cell - Arch::X86_64 => emitter.instruction("call __rt_incref"), // own a reference to the forwarded Mixed cell + Arch::AArch64 => emitter.instruction("bl __rt_incref"), // own a reference to the forwarded Mixed cell + Arch::X86_64 => emitter.instruction("call __rt_incref"), // own a reference to the forwarded Mixed cell } } GenParamKind::Float => { @@ -449,28 +495,28 @@ fn emit_generator_constructor( } } match target.arch { - Arch::AArch64 => emitter - .instruction(&format!("str {}, [x19, #{}]", gen_reg, store_off)), // store the owned Mixed cell into the generator start_args slot - Arch::X86_64 => emitter.instruction(&format!( - "mov QWORD PTR [r12 + {}], {}", - store_off, gen_reg - )), // store the owned Mixed cell into the generator start_args slot + Arch::AArch64 => { + emitter.instruction(&format!("str {}, [x19, #{}]", gen_reg, store_off)) + } // store the owned Mixed cell into the generator start_args slot + Arch::X86_64 => { + emitter.instruction(&format!("mov QWORD PTR [r12 + {}], {}", store_off, gen_reg)) + } // store the owned Mixed cell into the generator start_args slot } } // -- publish the forwarded argument count and return the Generator object -- match target.arch { Arch::AArch64 => { - emitter.instruction(&format!("mov x9, #{}", n)); // number of boxed start arguments forwarded to the body + emitter.instruction(&format!("mov x9, #{}", n)); // number of boxed start arguments forwarded to the body emitter.instruction(&format!("str x9, [x19, #{}]", FIBER_START_ARG_COUNT_OFFSET)); // publish the start argument count - emitter.instruction("mov x0, x19"); // return the Generator object to the caller + emitter.instruction("mov x0, x19"); // return the Generator object to the caller } Arch::X86_64 => { emitter.instruction(&format!( "mov QWORD PTR [r12 + {}], {}", FIBER_START_ARG_COUNT_OFFSET, n )); // publish the start argument count - emitter.instruction("mov rax, r12"); // return the Generator object to the caller + emitter.instruction("mov rax, r12"); // return the Generator object to the caller } } abi::load_at_offset(emitter, gen_cache, GEN_SAVE_OFF); // restore the caller's callee-saved generator-object cache register @@ -537,13 +583,16 @@ fn emit_generator_callback( let assignments = abi::build_outgoing_arg_assignments_for_target(target, param_types, 0); emitter.blank(); - emitter.comment(&format!("--- generator entry wrapper {} ---", callback_label)); + emitter.comment(&format!( + "--- generator entry wrapper {} ---", + callback_label + )); emitter.label_global(callback_label); abi::emit_frame_prologue(emitter, frame_size); abi::store_at_offset(emitter, gen_cache, GEN_SAVE_OFF); // preserve the caller's callee-saved generator-object cache register match target.arch { - Arch::AArch64 => emitter.instruction("mov x19, x0"), // x19 = Generator object passed by __rt_fiber_entry - Arch::X86_64 => emitter.instruction("mov r12, rdi"), // r12 = Generator object passed by __rt_fiber_entry + Arch::AArch64 => emitter.instruction("mov x19, x0"), // x19 = Generator object passed by __rt_fiber_entry + Arch::X86_64 => emitter.instruction("mov r12, rdi"), // r12 = Generator object passed by __rt_fiber_entry }; // -- unbox each start_args cell into its scratch frame slot -- @@ -552,10 +601,11 @@ fn emit_generator_callback( let load_off = FIBER_START_ARGS_OFFSET as usize + idx * 8; match target.arch { Arch::AArch64 => { - emitter.instruction(&format!("ldr x0, [x19, #{}]", load_off)); // load the boxed Mixed start argument + emitter.instruction(&format!("ldr x0, [x19, #{}]", load_off)); // load the boxed Mixed start argument } Arch::X86_64 => { - emitter.instruction(&format!("mov rax, QWORD PTR [r12 + {}]", load_off)); // load the boxed Mixed start argument + emitter.instruction(&format!("mov rax, QWORD PTR [r12 + {}]", load_off)); + // load the boxed Mixed start argument } } if gen_param_kind(ty) == GenParamKind::Mixed { @@ -563,16 +613,16 @@ fn emit_generator_callback( continue; } match target.arch { - Arch::AArch64 => emitter.instruction("bl __rt_mixed_unbox"), // x1 = primary payload, x2 = string length - Arch::X86_64 => emitter.instruction("call __rt_mixed_unbox"), // rdi = primary payload, rdx = string length + Arch::AArch64 => emitter.instruction("bl __rt_mixed_unbox"), // x1 = primary payload, x2 = string length + Arch::X86_64 => emitter.instruction("call __rt_mixed_unbox"), // rdi = primary payload, rdx = string length } match (gen_param_kind(ty), target.arch) { (GenParamKind::Float, Arch::AArch64) => { - emitter.instruction("fmov d0, x1"); // reinterpret the unboxed float payload bits + emitter.instruction("fmov d0, x1"); // reinterpret the unboxed float payload bits abi::store_at_offset(emitter, "d0", slot); } (GenParamKind::Float, Arch::X86_64) => { - emitter.instruction("movq xmm0, rdi"); // reinterpret the unboxed float payload bits + emitter.instruction("movq xmm0, rdi"); // reinterpret the unboxed float payload bits abi::store_at_offset(emitter, "xmm0", slot); } (GenParamKind::Str, Arch::AArch64) => { @@ -603,7 +653,7 @@ fn emit_generator_callback( Arch::X86_64 => "xmm0", }; abi::load_at_offset(emitter, freg, slot); - abi::emit_push_float_reg(emitter, freg); // stage the float parameter on the temporary call stack + abi::emit_push_float_reg(emitter, freg); // stage the float parameter on the temporary call stack } GenParamKind::Str => { let (lo, hi) = match target.arch { @@ -612,7 +662,7 @@ fn emit_generator_callback( }; abi::load_at_offset(emitter, lo, slot); abi::load_at_offset(emitter, hi, slot - 8); - abi::emit_push_reg_pair(emitter, lo, hi); // stage the string pointer/length pair on the temporary call stack + abi::emit_push_reg_pair(emitter, lo, hi); // stage the string pointer/length pair on the temporary call stack } GenParamKind::IntLike | GenParamKind::Mixed => { let reg = match target.arch { @@ -620,7 +670,7 @@ fn emit_generator_callback( Arch::X86_64 => "r10", }; abi::load_at_offset(emitter, reg, slot); - abi::emit_push_reg(emitter, reg); // stage the scalar/pointer parameter on the temporary call stack + abi::emit_push_reg(emitter, reg); // stage the scalar/pointer parameter on the temporary call stack } } } @@ -637,10 +687,10 @@ fn emit_generator_callback( // -- run the body, park its return value, and yield a null fiber transfer value -- match target.arch { Arch::AArch64 => { - emitter.instruction(&format!("bl {}", body_label)); // run the generator body to completion; x0 = boxed return value + emitter.instruction(&format!("bl {}", body_label)); // run the generator body to completion; x0 = boxed return value } Arch::X86_64 => { - emitter.instruction(&format!("call {}", body_label)); // run the generator body to completion; rax = boxed return value + emitter.instruction(&format!("call {}", body_label)); // run the generator body to completion; rax = boxed return value } } abi::emit_release_temporary_stack(emitter, call_pad); // drop the ARM64 nested-call alignment pad @@ -648,11 +698,14 @@ fn emit_generator_callback( match target.arch { Arch::AArch64 => { emitter.instruction(&format!("str x0, [x19, #{}]", GEN_RETURN_VALUE_OFFSET)); // park the body return value for getReturn() - emitter.instruction("mov x0, #0"); // hand the fiber transfer value a null so it does not alias the return + emitter.instruction("mov x0, #0"); // hand the fiber transfer value a null so it does not alias the return } Arch::X86_64 => { - emitter.instruction(&format!("mov QWORD PTR [r12 + {}], rax", GEN_RETURN_VALUE_OFFSET)); // park the body return value for getReturn() - emitter.instruction("xor eax, eax"); // hand the fiber transfer value a null so it does not alias the return + emitter.instruction(&format!( + "mov QWORD PTR [r12 + {}], rax", + GEN_RETURN_VALUE_OFFSET + )); // park the body return value for getReturn() + emitter.instruction("xor eax, eax"); // hand the fiber transfer value a null so it does not alias the return } } abi::load_at_offset(emitter, gen_cache, GEN_SAVE_OFF); // restore the caller's callee-saved generator-object cache register @@ -660,7 +713,6 @@ fn emit_generator_callback( abi::emit_return(emitter); } - /// Returns the runtime metadata entry label for an EIR class-method function. fn class_method_entry_symbol(function: &Function) -> Result { let Some((class_name, method_name)) = function.name.rsplit_once("::") else { @@ -698,15 +750,7 @@ fn emit_main_function( ) -> Result<()> { let layout = frame::layout_for_function(function, emitter.target, regalloc_linear); let mut ctx = FunctionContext::new( - module, - function, - emitter, - data, - layout, - true, - gc_stats, - heap_debug, - None, + module, function, emitter, data, layout, true, gc_stats, heap_debug, None, ); if web { ctx.web = true; @@ -715,7 +759,7 @@ fn emit_main_function( frame::emit_main_prologue(&mut ctx); } if requires_elephc_tls { - crate::codegen::builtins::publish_tls_function_pointers(ctx.emitter); + crate::codegen::tls::publish_tls_function_pointers(ctx.emitter); } emit_enum_singleton_initializers(&mut ctx); emit_static_property_initializers(&mut ctx)?; @@ -779,7 +823,10 @@ fn emit_enum_singleton_initializer( name_offset: usize, case: &EnumCaseInfo, ) { - ctx.emitter.comment(&format!("initialize enum singleton {}::{}", enum_name, case.name)); + ctx.emitter.comment(&format!( + "initialize enum singleton {}::{}", + enum_name, case.name + )); emit_enum_object_allocation(ctx, class_id, property_count); if let Some(case_value) = &case.value { emit_enum_backing_value(ctx, case_value); @@ -804,24 +851,33 @@ fn emit_enum_name_property(ctx: &mut FunctionContext<'_>, case_name: &str, offse } /// Allocates an object-shaped enum singleton and zeroes its property storage. -fn emit_enum_object_allocation(ctx: &mut FunctionContext<'_>, class_id: u64, property_count: usize) { +fn emit_enum_object_allocation( + ctx: &mut FunctionContext<'_>, + class_id: u64, + property_count: usize, +) { let payload_size = 8 + property_count * 16; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov x0, #{}", payload_size)); // request enum singleton object payload storage + ctx.emitter + .instruction(&format!("mov x0, #{}", payload_size)); // request enum singleton object payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks enum singletons as object instances - ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the enum singleton payload - ctx.emitter.instruction(&format!("mov x10, #{}", class_id)); // materialize the enum class id - ctx.emitter.instruction("str x10, [x0]"); // store the enum class id at payload offset zero + ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks enum singletons as object instances + ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the enum singleton payload + ctx.emitter.instruction(&format!("mov x10, #{}", class_id)); // materialize the enum class id + ctx.emitter.instruction("str x10, [x0]"); // store the enum class id at payload offset zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov rax, {}", payload_size)); // request enum singleton object payload storage + ctx.emitter + .instruction(&format!("mov rax, {}", payload_size)); // request enum singleton 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("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the enum singleton payload - ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the enum class id - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the enum class id at 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 enum singleton payload + ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the enum class id + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the enum class id at payload offset zero } } let object_reg = abi::int_result_reg(ctx.emitter); @@ -874,7 +930,10 @@ fn emit_static_property_initializers(ctx: &mut FunctionContext<'_>) -> Result<() if declaring_class != class_name { continue; } - let default = class_info.static_defaults.get(index).and_then(Option::as_ref); + let default = class_info + .static_defaults + .get(index) + .and_then(Option::as_ref); if let Some(default_expr) = default { default_initializers.push(( class_name.clone(), @@ -897,11 +956,7 @@ fn emit_static_property_initializers(ctx: &mut FunctionContext<'_>) -> Result<() } /// Marks one typed static property without a default as uninitialized. -fn emit_static_property_sentinel( - ctx: &mut FunctionContext<'_>, - class_name: &str, - property: &str, -) { +fn emit_static_property_sentinel(ctx: &mut FunctionContext<'_>, class_name: &str, property: &str) { ctx.emitter.comment(&format!( "mark static property {}::${} uninitialized", class_name, property @@ -1034,7 +1089,10 @@ fn emit_static_property_default_value( } let symbol = static_property_symbol(class_name, property); abi::emit_store_result_to_symbol(ctx.emitter, &symbol, php_type, false); - if !matches!(php_type.codegen_repr(), PhpType::Str | PhpType::TaggedScalar) { + if !matches!( + php_type.codegen_repr(), + PhpType::Str | PhpType::TaggedScalar + ) { abi::emit_store_zero_to_symbol(ctx.emitter, &symbol, 8); } Ok(()) @@ -1051,22 +1109,25 @@ fn emit_blocks(ctx: &mut FunctionContext<'_>) -> Result<()> { /// Emits one EIR basic block. fn emit_block(ctx: &mut FunctionContext<'_>, block: &BasicBlock) -> Result<()> { - ctx.emitter.label(&ctx.block_label(&block.name, block.id.as_raw())); + ctx.emitter + .label(&ctx.block_label(&block.name, block.id.as_raw())); for inst_id in &block.instructions { emit_instruction_source_marker(ctx, *inst_id)?; lower_inst::lower_instruction(ctx, *inst_id)?; } - let terminator = block - .terminator - .as_ref() - .ok_or_else(|| CodegenIrError::invalid_module(format!("block '{}' has no terminator", block.name)))?; + let terminator = block.terminator.as_ref().ok_or_else(|| { + CodegenIrError::invalid_module(format!("block '{}' has no terminator", block.name)) + })?; lower_term::lower_terminator(ctx, terminator) } /// Emits the source-map marker for an EIR instruction when it carries a real PHP span. fn emit_instruction_source_marker(ctx: &mut FunctionContext<'_>, inst_id: InstId) -> Result<()> { let Some(inst) = ctx.function.instruction(inst_id) else { - return Err(CodegenIrError::missing_entry("instruction", inst_id.as_raw())); + return Err(CodegenIrError::missing_entry( + "instruction", + inst_id.as_raw(), + )); }; let Some(span) = inst.span else { return Ok(()); diff --git a/src/codegen/builtins/arrays/array_chunk.rs b/src/codegen/builtins/arrays/array_chunk.rs deleted file mode 100644 index 7c8e7853d5..0000000000 --- a/src/codegen/builtins/arrays/array_chunk.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Purpose: -//! Emits PHP `array_chunk` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_chunk($array, $size, $preserve_keys)` builtin call. -/// -/// Splits `$array` into chunks of `$size` elements, returning an array of arrays. -/// Calls `__rt_array_chunk` (scalar arrays) or `__rt_array_chunk_refcounted` (refcounted -/// arrays such as those containing strings or nested arrays) via the platform ABI. -/// -/// ## Arguments -/// - `args[0]`: source array to chunk -/// - `args[1]`: chunk size (positive integer) -/// -/// ## Return type -/// `PhpType::Array(Array(inner))` — an array of arrays preserving the inner element type. -/// If the input type cannot be determined, defaults to `Array(Array(Int))`. -/// -/// ## Runtime helpers -/// - `__rt_array_chunk`: for scalar indexed arrays (int/float-only elements) -/// - `__rt_array_chunk_refcounted`: for arrays with refcounted elements (strings, objects, nested arrays) -/// -/// ## ABI notes -/// - x86_64: preserves source array in `rax` during size evaluation, passes array in `rdi`, size in `rsi` -/// - ARM64: pushes array pointer on stack, passes size in `x1`, restores array in `x0` -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_chunk()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let uses_refcounted_runtime = matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - if emitter.target.arch == Arch::X86_64 { - abi::emit_push_reg(emitter, "rax"); // preserve the source indexed array while evaluating the requested chunk size expression - let size_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &size_ty); // unbox a Mixed/Union chunk size into a raw integer - emitter.instruction("mov rsi, rax"); // place the requested chunk size in the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the source indexed array into the first x86_64 runtime argument register - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_chunk_refcounted"); // split the refcounted indexed array into chunk arrays through the x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_chunk"); // split the scalar indexed array into chunk arrays through the x86_64 runtime helper - } - - return match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(Box::new(PhpType::Array(inner)))), - _ => Some(PhpType::Array(Box::new(PhpType::Array(Box::new(PhpType::Int))))), - }; - } - - // -- save array pointer, evaluate chunk size -- - emitter.instruction("str x0, [sp, #-16]!"); // push array pointer onto stack - let size_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &size_ty); // unbox a Mixed/Union chunk size into a raw integer - // -- call runtime to split array into chunks -- - emitter.instruction("mov x1, x0"); // move chunk size to x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer into x0 (first arg) - if uses_refcounted_runtime { - emitter.instruction("bl __rt_array_chunk_refcounted"); // chunk array while retaining borrowed heap elements - } else { - emitter.instruction("bl __rt_array_chunk"); // call runtime: chunk array → x0=array of arrays - } - - match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(Box::new(PhpType::Array(inner)))), - _ => Some(PhpType::Array(Box::new(PhpType::Array(Box::new(PhpType::Int))))), - } -} diff --git a/src/codegen/builtins/arrays/array_column.rs b/src/codegen/builtins/arrays/array_column.rs deleted file mode 100644 index 24020e41ca..0000000000 --- a/src/codegen/builtins/arrays/array_column.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Purpose: -//! Emits PHP `array_column` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Lowers the PHP `array_column($input, $column_key)` call to a target-specific runtime routine. -/// -/// # Arguments -/// * `args[0]` — the input array of associative arrays -/// * `args[1]` — the column key (string) to extract from each row -/// -/// # Type inference -/// - If `$input` is `PhpType::Array(AssocArray { value, .. })`, uses `value` as the result element type. -/// - Otherwise defaults to `PhpType::Str` for the result element type. -/// -/// # Runtime dispatch -/// Calls one of `__rt_array_column_str`, `__rt_array_column_mixed`, `__rt_array_column_ref`, -/// or `__rt_array_column` depending on the inferred value type. Each routine allocates a new -/// indexed array, preserving refcounts for retained payloads. -/// -/// # ABI constraints -/// Pushes/pops the outer array pointer and column key string registers to survive `emit_expr` -/// evaluation order (source-order evaluation, ABI materialization in parameter order). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_column()"); - // -- evaluate array of assoc arrays -- - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let val_ty = match &arr_ty { - PhpType::Array(inner) => match inner.as_ref() { - PhpType::AssocArray { value, .. } => *value.clone(), - _ => PhpType::Str, - }, - _ => PhpType::Str, - }; - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the outer indexed-array pointer while evaluating the requested column key - - // -- evaluate column key (string) -- - emit_expr(&args[1], emitter, ctx, data); - let (key_ptr_reg, key_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, key_ptr_reg, key_len_reg); // preserve the requested column key string while restoring the outer indexed-array pointer - - // -- call runtime -- - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the requested column key into the runtime string-argument registers - abi::emit_pop_reg(emitter, "x0"); // restore the outer indexed-array pointer into the runtime array-argument register - } - Arch::X86_64 => { - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore the requested column key into the SysV string-argument registers - abi::emit_pop_reg(emitter, "rdi"); // restore the outer indexed-array pointer into the SysV first integer argument register - } - } - if val_ty == PhpType::Str { - abi::emit_call_label(emitter, "__rt_array_column_str"); // extract string column values into a new indexed array whose slots own persisted strings - } else if matches!(val_ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_array_column_mixed"); // extract runtime-tagged hash values into boxed Mixed result slots - } else if val_ty.is_refcounted() { - abi::emit_call_label(emitter, "__rt_array_column_ref"); // extract retained heap/object/array column values into a new indexed array - } else { - abi::emit_call_label(emitter, "__rt_array_column"); // extract scalar column values into a new indexed array - } - - Some(PhpType::Array(Box::new(val_ty))) -} diff --git a/src/codegen/builtins/arrays/array_combine.rs b/src/codegen/builtins/arrays/array_combine.rs deleted file mode 100644 index 918372aa0a..0000000000 --- a/src/codegen/builtins/arrays/array_combine.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Purpose: -//! Emits PHP `array_combine` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::{array_key_type_from_value_type, PhpType}; -use super::hash_value_type_tag::hash_value_type_tag; - -/// Emits the `array_combine(keys, values)` builtin call. -/// -/// Keys are emitted first and preserved (x86_64: pushed to stack; ARM64: stored to `[sp]`). -/// Values are then emitted into `x0`/`rax`, and the appropriate runtime helper is called: -/// - `__rt_array_combine` for non-refcounted value types (int, float) -/// - `__rt_array_combine_refcounted` for refcounted value types (string, array, object) -/// On ARM64 keys are passed in `x0`, values in `x1`, and the type tag in `x2`. -/// On x86_64 keys are passed in `rdi`, values in `rsi`, and the type tag in `rdx`. -/// -/// Returns `PhpType::AssocArray` with the combined key/value element types. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_combine()"); - let keys_ty = emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - abi::emit_push_reg(emitter, "rax"); // preserve the indexed array of keys while evaluating the indexed array of values expression - let values_ty = emit_expr(&args[1], emitter, ctx, data); - let (key_elem_ty, value_elem_ty) = match (&keys_ty, &values_ty) { - (PhpType::Array(key), PhpType::Array(value)) => ((**key).clone(), (**value).clone()), - _ => (PhpType::Str, PhpType::Int), - }; - let uses_refcounted_runtime = value_elem_ty.is_refcounted(); - let value_type_tag = hash_value_type_tag(&value_elem_ty); - if !uses_refcounted_runtime { - abi::emit_load_int_immediate(emitter, "rdx", value_type_tag.into()); - emitter.instruction("mov rsi, rax"); // place the indexed array of values in the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the indexed array of keys into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_array_combine"); // build the scalar associative array through the x86_64 runtime helper - } else { - emitter.instruction("mov rcx, rax"); // preserve the indexed array of values while materializing the result hash value_type tag for the refcounted helper path - abi::emit_load_int_immediate(emitter, "rdx", value_type_tag.into()); - emitter.instruction("mov rsi, rcx"); // place the indexed array of values in the second x86_64 runtime argument register for the refcounted helper path - abi::emit_pop_reg(emitter, "rdi"); // restore the indexed array of keys into the first x86_64 runtime argument register for the refcounted helper path - abi::emit_call_label(emitter, "__rt_array_combine_refcounted"); // build the refcounted associative array through the dedicated x86_64 runtime helper - } - - return Some(PhpType::AssocArray { - key: Box::new(array_key_type_from_value_type(key_elem_ty)), - value: Box::new(value_elem_ty), - }); - } - - // -- save keys array, evaluate values array -- - emitter.instruction("str x0, [sp, #-16]!"); // push keys array pointer onto stack - let values_ty = emit_expr(&args[1], emitter, ctx, data); - let (key_elem_ty, value_elem_ty) = match (&keys_ty, &values_ty) { - (PhpType::Array(key), PhpType::Array(value)) => ((**key).clone(), (**value).clone()), - _ => (PhpType::Str, PhpType::Int), - }; - let uses_refcounted_runtime = value_elem_ty.is_refcounted(); - let value_type_tag = hash_value_type_tag(&value_elem_ty); - // -- call runtime to combine keys and values into assoc array -- - emitter.instruction(&format!("mov x2, #{}", value_type_tag)); // x2 = result hash value_type tag - emitter.instruction("mov x1, x0"); // move values array pointer to x1 - emitter.instruction("ldr x0, [sp], #16"); // pop keys array pointer into x0 - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_combine_refcounted" - } else { - "bl __rt_array_combine" - }; - emitter.instruction(runtime_call); // call runtime: combine → x0=new assoc array - - Some(PhpType::AssocArray { - key: Box::new(array_key_type_from_value_type(key_elem_ty)), - value: Box::new(value_elem_ty), - }) -} diff --git a/src/codegen/builtins/arrays/array_diff.rs b/src/codegen/builtins/arrays/array_diff.rs deleted file mode 100644 index 4d3bd38568..0000000000 --- a/src/codegen/builtins/arrays/array_diff.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Purpose: -//! Emits PHP `array_diff` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `array_diff($arr1, $arr2)` builtin call. -/// -/// Compares `$arr1` against `$arr2` and returns all values from `$arr1` that are -/// not present in `$arr2`. Only values are compared; keys are preserved. -/// -/// # Arguments -/// * `args` - Must contain exactly two array expressions (the two input arrays). -/// -/// # Behavior -/// - Pushes the first array pointer, evaluates the second array expression, then -/// calls the appropriate runtime helper (`__rt_array_diff` or `__rt_array_diff_refcounted`) -/// based on whether the first array uses refcounted heap storage. -/// - On x86_64: uses register-based ABI (rdi/rsi for first/second array pointers). -/// - On ARM64: uses stack-based push/pop and x0/x1 for array pointers. -/// - Returns the array type of the first argument if it is already an Array, -/// otherwise returns `Array` as the default value type. -/// -/// # Return type -/// `Some(PhpType::Array(...))` reflecting the first input array's inner type. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_diff()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - abi::emit_push_reg(emitter, "rax"); // preserve the first input array while evaluating the second input array expression - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rsi, rax"); // place the second input array pointer in the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the first input array pointer into the first x86_64 runtime argument register - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_diff_refcounted"); // compute the borrowed-heap-aware array difference through the dedicated x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_diff"); // compute the integer array difference through the x86_64 runtime helper - } - - return match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } - - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - // -- save first array, evaluate second array -- - emitter.instruction("str x0, [sp, #-16]!"); // push first array pointer onto stack - emit_expr(&args[1], emitter, ctx, data); - // -- call runtime to compute value difference -- - emitter.instruction("mov x1, x0"); // move second array pointer to x1 - emitter.instruction("ldr x0, [sp], #16"); // pop first array pointer into x0 - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_diff_refcounted" - } else { - "bl __rt_array_diff" - }; - emitter.instruction(runtime_call); // call runtime: diff arrays → x0=new array - - match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - } -} diff --git a/src/codegen/builtins/arrays/array_diff_key.rs b/src/codegen/builtins/arrays/array_diff_key.rs deleted file mode 100644 index 0380741d12..0000000000 --- a/src/codegen/builtins/arrays/array_diff_key.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Emits PHP `array_diff_key` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `array_diff_key($arr1, $arr2)` by computing the key-wise difference of two associative arrays. -/// -/// # Arguments -/// - `args[0]`: the base associative array whose keys are retained -/// - `args[1]`: the mask associative array whose keys are excluded -/// -/// # Behavior -/// Pushes the first array pointer onto the stack, evaluates the second array, -/// then loads both pointers into the runtime helper argument registers and -/// calls `__rt_array_diff_key` to produce a new hash table containing only -/// keys present in `$arr1` but not in `$arr2`. -/// -/// # Returns -/// `Some(PhpType)` with the type of the first argument (an associative array type); -/// `None` if no type information is available. -/// -/// # ABI constraints -/// - AArch64: first array pointer in `x0`, second array pointer in `x1`; result pointer in `x0`. -/// - X86_64: first array pointer in `rdi`, second array pointer in `rsi`; result pointer in `rax`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_diff_key()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - // -- save first array, evaluate second array -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the first associative-array pointer while evaluating the mask array - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the second associative-array pointer into the second runtime helper argument register - abi::emit_pop_reg(emitter, "x0"); // restore the first associative-array pointer into the first runtime helper argument register - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the second associative-array pointer into the second SysV runtime helper argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the first associative-array pointer into the first SysV runtime helper argument register - } - } - abi::emit_call_label(emitter, "__rt_array_diff_key"); // compute the associative-array key difference and return the filtered hash table pointer - - Some(arr_ty) -} diff --git a/src/codegen/builtins/arrays/array_fill.rs b/src/codegen/builtins/arrays/array_fill.rs deleted file mode 100644 index 547edd7b1f..0000000000 --- a/src/codegen/builtins/arrays/array_fill.rs +++ /dev/null @@ -1,191 +0,0 @@ -//! Purpose: -//! Emits PHP `array_fill` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. -//! - A non-zero start index, or a string fill value, needs a keyed (hash) result because a -//! 0-based indexed array cannot represent keys `start..start+count-1` and the scalar indexed -//! fill cannot store a string pointer+length. Those cases route through `__rt_array_fill_assoc`. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::platform::Arch; -use crate::codegen::runtime_value_tag; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Returns the assoc-result type produced by `__rt_array_fill_assoc` (int keys, boxed values). -fn assoc_fill_type() -> PhpType { - PhpType::AssocArray { - key: Box::new(PhpType::Int), - value: Box::new(PhpType::Mixed), - } -} - -/// Returns true when `array_fill` must build a keyed (hash) array rather than a 0-based -/// indexed one. A non-literal-zero start index produces keys `start..start+count-1`, which a -/// 0-based indexed array cannot represent, so it always goes through the keyed path. A -/// literal-zero start with a string value still uses the dedicated `__rt_array_fill_str` -/// indexed path (the 0-based indexed string array is what `array_fill(0, n, "ab")` should -/// return — `[0=>"ab", 1=>"ab", ...]`, not a hash). -fn needs_assoc_fill(start_arg: &Expr, _value_ty: &PhpType) -> bool { - let start_is_literal_zero = matches!(start_arg.kind, ExprKind::IntLiteral(0)); - !start_is_literal_zero -} - -/// Emits the `array_fill(start_index, count, value)` builtin call. -/// -/// Evaluates arguments left-to-right, pushing `start_index` and `count` on the stack before -/// evaluating `value` to preserve ordering. A literal-zero start with a scalar/refcounted value -/// uses the indexed `__rt_array_fill`/`__rt_array_fill_refcounted` helpers; a non-zero start or -/// a string value routes through `__rt_array_fill_assoc`, which builds a Mixed-valued hash with -/// keys `start..start+count-1`. On x86_64 Linux, delegates to `emit_array_fill_linux_x86_64`. -/// -/// Returns `PhpType::Array(value_ty)` for the indexed path or `AssocArray{Int, Mixed}` for the -/// keyed path. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_fill()"); - if emitter.target.arch == Arch::X86_64 { - return emit_array_fill_linux_x86_64(args, emitter, ctx, data); - } - - let start_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &start_ty); // unbox a Mixed/Union start index into a raw integer - // -- save start index, evaluate count -- - emitter.instruction("str x0, [sp, #-16]!"); // push start index onto stack - emit_expr(&args[1], emitter, ctx, data); - // -- save count, evaluate fill value -- - emitter.instruction("str x0, [sp, #-16]!"); // push count onto stack - let value_ty = emit_expr(&args[2], emitter, ctx, data); - - if needs_assoc_fill(&args[0], &value_ty) { - // -- marshal the fill value into value_lo (x2), value_hi (x3), value_tag (x4) -- - match value_ty.codegen_repr() { - PhpType::Str => { - emitter.instruction("mov x3, x2"); // string length becomes the value high word - emitter.instruction("mov x2, x1"); // string pointer becomes the value low word - } - PhpType::Float => { - emitter.instruction("fmov x2, d0"); // move the float bits into the value low word - emitter.instruction("mov x3, #0"); // floats use no high word - } - _ => { - emitter.instruction("mov x2, x0"); // scalar value or heap pointer becomes the value low word - emitter.instruction("mov x3, #0"); // non-string payloads use no high word - } - } - abi::emit_load_int_immediate(emitter, "x4", runtime_value_tag(&value_ty) as i64); // runtime value tag for per-slot boxing - emitter.instruction("ldr x1, [sp], #16"); // pop count into x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop start index into x0 (first arg) - emitter.instruction("bl __rt_array_fill_assoc"); // build a keyed hash with keys start..start+count-1 - return Some(assoc_fill_type()); - } - - if matches!(value_ty.codegen_repr(), PhpType::Str) { - // -- literal-zero start with a string value: use the dedicated indexed string path -- - // String ABI: x1 = pointer, x2 = length; x0 still holds the count we pushed on the stack. - // Marshal to (x0=count, x1=ptr, x2=len): pop the count, discard the (literal-zero) start. - emitter.instruction("ldr x0, [sp], #16"); // pop count into x0 (first arg) - emitter.instruction("ldr x9, [sp], #16"); // pop and discard the (literal-zero) start index - emitter.instruction("bl __rt_array_fill_str"); // build the indexed string array via repeated push_str - return Some(PhpType::Array(Box::new(PhpType::Str))); - } - - let uses_refcounted_runtime = value_ty.is_refcounted(); - // -- set up three-arg call: start, count, value -- - emitter.instruction("mov x2, x0"); // move fill value to x2 (third arg) - emitter.instruction("ldr x1, [sp], #16"); // pop count into x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop start index into x0 (first arg) - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_fill_refcounted" - } else { - "bl __rt_array_fill" - }; - emitter.instruction(runtime_call); // call runtime: fill array → x0=new array - - Some(PhpType::Array(Box::new(value_ty))) -} - -/// x86_64 Linux-specific entry point for `array_fill`. -/// -/// Uses System V AMD64 ABI: `rdi` = start_index, `rsi` = count, `rdx` = fill value (or -/// value_lo for the keyed path). The keyed path additionally passes value_hi in `rcx` and the -/// runtime value tag in `r8`, then calls `__rt_array_fill_assoc`. -/// -/// Returns `PhpType::Array(value_ty)` for the indexed path or `AssocArray{Int, Mixed}`. -fn emit_array_fill_linux_x86_64( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let start_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &start_ty); // unbox a Mixed/Union start index into a raw integer - abi::emit_push_reg(emitter, "rax"); // preserve the start index while evaluating the count and fill value arguments - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, "rax"); // preserve the count while evaluating the fill value argument - let value_ty = emit_expr(&args[2], emitter, ctx, data); - - if needs_assoc_fill(&args[0], &value_ty) { - // -- marshal the fill value into value_lo (rdx), value_hi (rcx), value_tag (r8) -- - match value_ty.codegen_repr() { - PhpType::Str => { - emitter.instruction("mov rcx, rdx"); // string length becomes the value high word - emitter.instruction("mov rdx, rax"); // string pointer becomes the value low word - } - PhpType::Float => { - emitter.instruction("movq rdx, xmm0"); // move the float bits into the value low word - emitter.instruction("xor rcx, rcx"); // floats use no high word - } - _ => { - emitter.instruction("mov rdx, rax"); // scalar value or heap pointer becomes the value low word - emitter.instruction("xor rcx, rcx"); // non-string payloads use no high word - } - } - abi::emit_load_int_immediate(emitter, "r8", runtime_value_tag(&value_ty) as i64); // runtime value tag for per-slot boxing - abi::emit_pop_reg(emitter, "rsi"); // restore the requested count into the second argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the start index into the first argument register - abi::emit_call_label(emitter, "__rt_array_fill_assoc"); // build a keyed hash with keys start..start+count-1 - return Some(assoc_fill_type()); - } - - if matches!(value_ty.codegen_repr(), PhpType::Str) { - // -- literal-zero start with a string value: use the dedicated indexed string path -- - // String ABI: rax = pointer, rdx = length. Marshal to (rdi=count, rsi=ptr, rdx=len). - abi::emit_push_reg(emitter, "rdx"); // preserve the string length across the rsi move - emitter.instruction("mov rsi, rax"); // string pointer into the second runtime argument register - abi::emit_pop_reg(emitter, "rdx"); // restore the string length into the third runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // pop count into the first runtime argument register - abi::emit_pop_reg(emitter, "r11"); // pop and discard the (literal-zero) start index - abi::emit_call_label(emitter, "__rt_array_fill_str"); // build the indexed string array via repeated push_str - return Some(PhpType::Array(Box::new(PhpType::Str))); - } - - if matches!(value_ty, PhpType::Float) { - emitter.instruction("movq rdx, xmm0"); // move the floating-point fill payload bits into the third x86_64 runtime argument register - } else { - emitter.instruction("mov rdx, rax"); // place the fill payload in the third x86_64 runtime argument register - } - abi::emit_pop_reg(emitter, "rsi"); // restore the requested count into the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the start index into the first x86_64 runtime argument register - if value_ty.is_refcounted() { - abi::emit_call_label(emitter, "__rt_array_fill_refcounted"); // build an indexed array by repeatedly retaining the borrowed heap payload - } else { - abi::emit_call_label(emitter, "__rt_array_fill"); // build a scalar indexed array through the plain fill runtime helper - } - - Some(PhpType::Array(Box::new(value_ty))) -} diff --git a/src/codegen/builtins/arrays/array_fill_keys.rs b/src/codegen/builtins/arrays/array_fill_keys.rs deleted file mode 100644 index 31933fb732..0000000000 --- a/src/codegen/builtins/arrays/array_fill_keys.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! Purpose: -//! Emits PHP `array_fill_keys` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::{array_key_type_from_value_type, PhpType}; -use super::hash_value_type_tag::hash_value_type_tag; - -/// Emits the `array_fill_keys($keys, $value)` builtin call. -/// -/// Dispatches to the x86_64 Linux implementation or uses ARM64 conventions. -/// Pushes the keys array onto the stack, evaluates the fill value into `x0`, -/// calls `__rt_array_fill_keys` (or `_refcounted` variant), and returns an -/// `AssocArray` type with the inferred key element type and value type. -/// -/// # Arguments -/// * `_name` - Unused; present for dispatcher uniformity. -/// * `args` - Two expressions: `$keys` (array of keys) and `$value` (fill value). -/// * `emitter` - Target-specific assembly emitter. -/// * `ctx` - Codegen context (variable layout, ownership state). -/// * `data` - Data section for literals and runtime metadata. -/// -/// # Returns -/// `Some(PhpType::AssocArray { key, value })` describing the result array. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_fill_keys()"); - if emitter.target.arch == Arch::X86_64 { - return emit_array_fill_keys_linux_x86_64(args, emitter, ctx, data); - } - - let keys_ty = emit_expr(&args[0], emitter, ctx, data); - // -- save keys array, evaluate fill value -- - emitter.instruction("str x0, [sp, #-16]!"); // push keys array pointer onto stack - let mut value_ty = emit_expr(&args[1], emitter, ctx, data); - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut value_ty); - let key_elem_ty = match &keys_ty { - PhpType::Array(key) => (**key).clone(), - _ => PhpType::Str, - }; - let uses_refcounted_runtime = value_ty.is_refcounted(); - let value_type_tag = hash_value_type_tag(&value_ty); - // -- call runtime to create assoc array from keys with given value -- - emitter.instruction(&format!("mov x2, #{}", value_type_tag)); // x2 = result hash value_type tag - emitter.instruction("mov x1, x0"); // move fill value to x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop keys array pointer into x0 (first arg) - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_fill_keys_refcounted" - } else { - "bl __rt_array_fill_keys" - }; - emitter.instruction(runtime_call); // call runtime: fill keys → x0=new assoc array - - Some(PhpType::AssocArray { - key: Box::new(array_key_type_from_value_type(key_elem_ty)), - value: Box::new(value_ty), - }) -} - -/// x86_64 Linux implementation of `array_fill_keys` using System V AMD64 ABI. -/// -/// Preserves the keys array in `rax` while evaluating the fill value expression, -/// then arranges arguments per AMD64 calling convention (rdi=keys, rsi=value, rdx=type_tag) -/// before calling the appropriate runtime helper. -/// -/// # Arguments -/// * `args` - Two expressions: `$keys` (indexed array) and `$value` (fill scalar). -/// * `emitter` - x86_64 assembly emitter. -/// * `ctx` - Codegen context. -/// * `data` - Data section. -/// -/// # Returns -/// `Some(PhpType::AssocArray { key, value })` describing the result array. -fn emit_array_fill_keys_linux_x86_64( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let keys_ty = emit_expr(&args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, "rax"); // preserve the indexed array of keys while evaluating the fill payload expression - let mut value_ty = emit_expr(&args[1], emitter, ctx, data); - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut value_ty); - let key_elem_ty = match &keys_ty { - PhpType::Array(key) => (**key).clone(), - _ => PhpType::Str, - }; - let uses_refcounted_runtime = value_ty.is_refcounted(); - let value_type_tag = hash_value_type_tag(&value_ty); - if matches!(value_ty, PhpType::Float) { - emitter.instruction("movq rsi, xmm0"); // move the floating-point fill payload bits into the second x86_64 runtime argument register - } else { - emitter.instruction("mov rsi, rax"); // place the fill payload in the second x86_64 runtime argument register - } - abi::emit_pop_reg(emitter, "rdi"); // restore the indexed array of keys into the first x86_64 runtime argument register - abi::emit_load_int_immediate(emitter, "rdx", value_type_tag.into()); - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_fill_keys_refcounted"); // build an associative array by retaining the shared heap payload for every requested key - } else { - abi::emit_call_label(emitter, "__rt_array_fill_keys"); // build an associative array by reusing the scalar payload for every requested key - } - - Some(PhpType::AssocArray { - key: Box::new(array_key_type_from_value_type(key_elem_ty)), - value: Box::new(value_ty), - }) -} diff --git a/src/codegen/builtins/arrays/array_filter.rs b/src/codegen/builtins/arrays/array_filter.rs deleted file mode 100644 index d6888e2c0c..0000000000 --- a/src/codegen/builtins/arrays/array_filter.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! Purpose: -//! Emits PHP `array_filter` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::array_constants::ARRAY_INT_CONSTANTS; -use crate::types::PhpType; -use super::callback_env; -use super::runtime_callable_array_callback; -use super::runtime_string_callback; - -/// Emits the `array_filter($array, $callback, $mode)` builtin call. -/// -/// Evaluates arguments in PHP source order: array first, then callback. The array pointer -/// is saved to the temporary stack before callback materialization to preserve evaluation -/// order. The appropriate runtime helper is selected based on whether the array element -/// type requires refcounted payload handling. -/// -/// # Arguments -/// - `_name`: Unused; dispatch is handled at the caller level. -/// - `args`: Two or three expressions — the input array, the callback, and optional mode. -/// - `emitter`: Target-aware instruction emitter. -/// - `ctx`: Codegen context carrying variable layout and ownership state. -/// - `data`: Mutable data section for relocations and constants. -/// -/// # Returns -/// `Some(PhpType::Array(...))` with the element type preserved from the input array -/// if known, otherwise `PhpType::Array(Int)` as a safe default. -/// -/// # ABI constraints -/// - Uses `nested_call_reg` for the callback address. -/// - Uses `int_result_reg` as a temporary to hold the array pointer during callback lowering. -/// - Pushes the array pointer before callback materialization and pops it after. -/// - On x86_64: uses `emit_call_label`; on ARM64: uses `bl` directly. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_filter()"); - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - let mode_arg_reg = abi::int_arg_reg_name(emitter.target, 3); - let mode_expr = args.get(2); - let static_mode = mode_expr.and_then(static_filter_mode_value); - let has_dynamic_mode = mode_expr.is_some() && static_mode.is_none(); - - // -- evaluate the array argument (first arg) -- - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let uses_refcounted_runtime = filter_uses_payload_runtime(&arr_ty); - let runtime_label = if uses_refcounted_runtime { - "__rt_array_filter_refcounted" - } else { - "__rt_array_filter" - }; - let mode_for_callback_shape = static_mode.unwrap_or(0); - let visible_arg_types = filter_visible_arg_types(&arr_ty, mode_for_callback_shape); - - // -- save array pointer, then evaluate the callback argument -- - abi::emit_push_reg(emitter, result_reg); // push the source array pointer onto the temporary stack - - if !has_dynamic_mode { - if runtime_string_callback::emit_after_saved_array( - &args[1], - Some(&arr_ty), - visible_arg_types.clone(), - PhpType::Bool, - array_arg_reg, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg( - emitter, - array_arg_reg, - wrapper.array_slot_offset, - ); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - emit_static_filter_mode_arg(emitter, mode_arg_reg, mode_for_callback_shape); - abi::emit_call_label(emitter, runtime_label); - }, - ) { - return filter_return_type(arr_ty); - } - - if let Some(wrapper) = callback_env::emit_callable_array_descriptor_env_after_saved_array( - &args[1], - array_arg_reg, - call_reg, - visible_arg_types.clone(), - PhpType::Bool, - emitter, - ctx, - data, - ) { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - emit_static_filter_mode_arg(emitter, mode_arg_reg, mode_for_callback_shape); - abi::emit_call_label(emitter, runtime_label); - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return filter_return_type(arr_ty); - } - - if runtime_callable_array_callback::emit_after_saved_array( - &args[1], - array_arg_reg, - visible_arg_types.clone(), - PhpType::Bool, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg( - emitter, - array_arg_reg, - wrapper.array_slot_offset, - ); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - emit_static_filter_mode_arg(emitter, mode_arg_reg, mode_for_callback_shape); - abi::emit_call_label(emitter, runtime_label); - }, - ) { - return filter_return_type(arr_ty); - } - - if callback_env::expr_call_needs_descriptor_callback_env(&args[1], ctx) - && callback_env::descriptor_callback_env_supported(&args[1]) - { - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // preserve the selected callable descriptor while recovering the source array - abi::emit_pop_reg(emitter, array_arg_reg); // recover the source array pointer before building the descriptor environment - emitter.instruction(&format!("mov {}, {}", result_reg, call_reg)); // restore the selected callable descriptor as the current result - let wrapper = callback_env::emit_descriptor_callback_env_from_result( - &args[1], - array_arg_reg, - visible_arg_types.clone(), - PhpType::Bool, - emitter, - ctx, - ) - .expect("descriptor callback env support checked before emitting callback"); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - emit_static_filter_mode_arg(emitter, mode_arg_reg, mode_for_callback_shape); - abi::emit_call_label(emitter, runtime_label); - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return filter_return_type(arr_ty); - } - } - - let captures = - callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); - - // -- place callback and array pointer into the runtime argument registers -- - if captures.is_empty() { - let dynamic_mode_loaded = if let Some(mode) = mode_expr.filter(|_| has_dynamic_mode) { - abi::emit_push_reg(emitter, call_reg); // preserve callback address while evaluating the mode argument - emit_expr(mode, emitter, ctx, data); - abi::emit_pop_reg(emitter, call_reg); // restore callback address after mode evaluation - true - } else { - false - }; - abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register - if dynamic_mode_loaded { - emitter.instruction(&format!("mov {}, {}", mode_arg_reg, result_reg)); // forward the runtime-computed mode to the filter helper - } else { - emit_static_filter_mode_arg(emitter, mode_arg_reg, mode_for_callback_shape); - } - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - } else { - abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - visible_arg_types, - emitter, - ctx, - ); - let dynamic_mode_loaded = if let Some(mode) = mode_expr.filter(|_| has_dynamic_mode) { - emit_expr(mode, emitter, ctx, data); - true - } else { - false - }; - if dynamic_mode_loaded { - emitter.instruction(&format!("mov {}, {}", mode_arg_reg, result_reg)); // preserve the runtime-computed mode before loading callback runtime arguments - } else { - emit_static_filter_mode_arg(emitter, mode_arg_reg, mode_for_callback_shape); - } - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, runtime_label); - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return filter_return_type(arr_ty); - } - - if emitter.target.arch == Arch::X86_64 { - abi::emit_call_label(emitter, runtime_label); // call the x86_64 callback-driven filter runtime helper - } else { - emitter.instruction(&format!("bl {}", runtime_label)); // call the ARM64 callback-driven filter runtime helper - } - - filter_return_type(arr_ty) -} - -/// Returns the static integer value for a known `array_filter()` mode expression. -/// -/// Recognizes integer literals and the predefined `ARRAY_FILTER_USE_*` constants. -fn static_filter_mode_value(expr: &Expr) -> Option { - match &expr.kind { - ExprKind::IntLiteral(value) => Some(*value), - ExprKind::ConstRef(name) => ARRAY_INT_CONSTANTS - .iter() - .find_map(|(constant, value)| (*constant == name.as_str()).then_some(*value)), - _ => None, - } -} - -/// Builds the callback visible argument list for the selected `array_filter()` mode. -fn filter_visible_arg_types(arr_ty: &PhpType, mode: i64) -> Vec { - match mode { - 1 => vec![filter_elem_type(arr_ty), PhpType::Int], - 2 => vec![PhpType::Int], - _ => vec![filter_elem_type(arr_ty)], - } -} - -/// Loads a static `array_filter()` mode into the runtime helper's fourth argument register. -fn emit_static_filter_mode_arg(emitter: &mut Emitter, mode_arg_reg: &str, mode: i64) { - abi::emit_load_int_immediate(emitter, mode_arg_reg, mode); -} - -/// Returns the filtered array type, preserving known input element type when possible. -fn filter_return_type(arr_ty: PhpType) -> Option { - match arr_ty { - PhpType::Array(elem_ty) => Some(PhpType::Array(elem_ty)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - } -} - -/// Returns the element type to store in the capture environment for the filtered array. -/// -/// Uses `codegen_repr()` so the environment slot reflects the actual lowered type rather -/// than the PHP-level type (e.g., `Str` becomes `Array(Int)` after array-of-strings encoding). -fn filter_elem_type(arr_ty: &PhpType) -> PhpType { - match arr_ty { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Int, - } -} - -/// Returns `true` if the array element type requires the refcounted runtime helper. -/// -/// An element type requires refcounted handling when its `codegen_repr()` is a string -/// (strings are refcounted in the runtime) or when `is_refcounted()` is true for the -/// inner type. This determines whether `__rt_array_filter_refcounted` or `__rt_array_filter` -/// is called. -fn filter_uses_payload_runtime(arr_ty: &PhpType) -> bool { - matches!( - &arr_ty, - PhpType::Array(inner) - if inner.is_refcounted() || matches!(inner.codegen_repr(), PhpType::Str) - ) -} diff --git a/src/codegen/builtins/arrays/array_flip.rs b/src/codegen/builtins/arrays/array_flip.rs deleted file mode 100644 index 46601b0312..0000000000 --- a/src/codegen/builtins/arrays/array_flip.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Purpose: -//! Emits PHP `array_flip` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::{array_key_type_from_value_type, PhpType}; - -/// Emits code for the PHP `array_flip` builtin, which exchanges array keys and values. -/// -/// # Arguments -/// - `_name`: Unused; matches the dispatcher signature (builtin name is resolved via catalog). -/// - `args`: Must contain exactly one expression producing an array. -/// - `emitter`: Target-aware instruction emitter. -/// - `ctx`: Codegen context (types, locals, class metadata). -/// - `data`: Data section for relocations and static data. -/// -/// # Returns -/// `Some(PhpType)` describing the flipped array type: -/// - `Array` → `AssocArray` (string keys flipped to integer values) -/// - `AssocArray` → `AssocArray` (swaps key and value types) -/// - Other arrays → `AssocArray` (homogeneous fallback) -/// -/// # Runtime helpers -/// - `__rt_array_flip_string`: Used when flipping an `Array` (all string keys). -/// - `__rt_array_flip`: Used for all other array types. -/// -/// # ABI notes -/// - ARM64: passes array pointer in `x0`, result returned in `x0` via `bl helper`. -/// - x86_64: moves array pointer to `rdi` before calling, result in `rax`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_flip()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let result_ty = match &arr_ty { - PhpType::Array(value) => PhpType::AssocArray { - key: Box::new(array_key_type_from_value_type(*value.clone())), - value: Box::new(PhpType::Int), - }, - PhpType::AssocArray { key, value } => PhpType::AssocArray { - key: Box::new(array_key_type_from_value_type(*value.clone())), - value: key.clone(), - }, - _ => PhpType::AssocArray { - key: Box::new(PhpType::Int), - value: Box::new(PhpType::Int), - }, - }; - let helper = match &arr_ty { - PhpType::Array(value) if matches!(value.as_ref(), PhpType::Str) => { - "__rt_array_flip_string" - } - _ => "__rt_array_flip", - }; - - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the source indexed array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, helper); // flip the indexed array into an associative array through the selected runtime helper - return Some(result_ty); - } - - // -- call runtime to swap keys and values -- - emitter.instruction(&format!("bl {}", helper)); // call runtime: flip array → x0=new assoc array - - Some(result_ty) -} diff --git a/src/codegen/builtins/arrays/array_intersect.rs b/src/codegen/builtins/arrays/array_intersect.rs deleted file mode 100644 index 9dd02b2b30..0000000000 --- a/src/codegen/builtins/arrays/array_intersect.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Purpose: -//! Emits PHP `array_intersect` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_intersect` builtin call. -/// -/// Computes the value-based intersection of two arrays, returning a new array -/// containing all entries from the first array whose values appear in the second. -/// -/// # Arguments -/// * `_name` - Unused; present to match the builtin emitter signature convention. -/// * `args` - Two expressions: the base array and the array to intersect against. -/// * `emitter` - Target-specific assembly emitter. -/// * `ctx` - Codegen context (types, locals, etc.). -/// * `data` - Data section for literals and runtime metadata. -/// -/// # Returns -/// `Some(PhpType::Array(...))` matching the input array's inner type, or `Array(Int)` for non-array inputs. -/// -/// # ABI / Runtime Behavior -/// - **x86_64**: preserves first array in `rax` while evaluating second argument (push/pop via `rdi`/`rsi` registers); calls `__rt_array_intersect` or `__rt_array_intersect_refcounted`. -/// - **ARM64**: pushes first array to stack, evaluates second argument into `x0`, pops first array into `x0`; calls `__rt_array_intersect` or `__rt_array_intersect_refcounted`. -/// - Picks the refcounted runtime variant when the input array holds refcounted values (objects or arrays); otherwise uses the non-refcounted variant. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_intersect()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - abi::emit_push_reg(emitter, "rax"); // preserve the first input array while evaluating the second input array expression - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rsi, rax"); // place the second input array pointer in the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the first input array pointer into the first x86_64 runtime argument register - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_intersect_refcounted"); // compute the borrowed-heap-aware array intersection through the dedicated x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_intersect"); // compute the integer array intersection through the x86_64 runtime helper - } - - return match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } - - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - // -- save first array, evaluate second array -- - emitter.instruction("str x0, [sp, #-16]!"); // push first array pointer onto stack - emit_expr(&args[1], emitter, ctx, data); - // -- call runtime to compute value intersection -- - emitter.instruction("mov x1, x0"); // move second array pointer to x1 - emitter.instruction("ldr x0, [sp], #16"); // pop first array pointer into x0 - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_intersect_refcounted" - } else { - "bl __rt_array_intersect" - }; - emitter.instruction(runtime_call); // call runtime: intersect arrays → x0=new array - - match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - } -} diff --git a/src/codegen/builtins/arrays/array_intersect_key.rs b/src/codegen/builtins/arrays/array_intersect_key.rs deleted file mode 100644 index a061c6c4b6..0000000000 --- a/src/codegen/builtins/arrays/array_intersect_key.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Purpose: -//! Emits PHP `array_intersect_key` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_intersect_key($arr1, $arr2)` builtin call. -/// -/// Reduces `$arr1` to only keys present in `$arr2` using runtime helper -/// `__rt_array_intersect_key`. Preserves the first array's type as return type. -/// -/// ## Arguments -/// - `args[0]`: the base associative array to filter -/// - `args[1]`: the mask associative array whose keys define the intersection -/// -/// ## Register/ABI usage -/// - On AArch64: first array pointer in `x0`, second in `x1`, result pointer in `x0` -/// - On x86_64: first array pointer in `rdi`, second in `rsi`, result pointer in `rax` -/// -/// ## Side effects -/// - Re-evaluates both argument expressions (caller must ensure side-effect order) -/// - Clobbers caller-saved registers used for array pointer transport -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_intersect_key()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - // -- save first array, evaluate second array -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the first associative-array pointer while evaluating the mask array - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the second associative-array pointer into the second runtime helper argument register - abi::emit_pop_reg(emitter, "x0"); // restore the first associative-array pointer into the first runtime helper argument register - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the second associative-array pointer into the second SysV runtime helper argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the first associative-array pointer into the first SysV runtime helper argument register - } - } - abi::emit_call_label(emitter, "__rt_array_intersect_key"); // compute the associative-array key intersection and return the filtered hash table pointer - - Some(arr_ty) -} diff --git a/src/codegen/builtins/arrays/array_key_exists.rs b/src/codegen/builtins/arrays/array_key_exists.rs deleted file mode 100644 index d10720214b..0000000000 --- a/src/codegen/builtins/arrays/array_key_exists.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Purpose: -//! Emits PHP `array_key_exists` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_key_exists($key, $array)` builtin call. -/// -/// Dispatches to a different runtime helper based on the array's PHP type: -/// - `AssocArray`: pushes the hash-table pointer, emits the key as a normalized string, -/// then restores both into ABI registers and calls `__rt_hash_get` to check key presence. -/// - Indexed array: pushes the array pointer, evaluates the integer key, then restores both -/// into helper registers and calls `__rt_array_key_exists` to check bounds. -/// -/// Preserves evaluation order by using the stack to save the first argument while computing -/// the second, then materializes all arguments into ABI registers before the call. -/// Returns `PhpType::Bool` in the integer result register on both paths. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_key_exists()"); - - // -- evaluate the array (second arg) first to get its type -- - let arr_ty = emit_expr(&args[1], emitter, ctx, data); - - if matches!(arr_ty, PhpType::AssocArray { .. }) { - // -- associative array: use hash_get to check if key exists -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the hash table pointer while evaluating the associative-array key expression - crate::codegen::emit_normalized_hash_key(&args[0], emitter, ctx, data); - let (key_ptr_reg, key_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, key_ptr_reg, key_len_reg); // preserve the computed associative-array key while restoring the hash-table pointer - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the associative-array key pointer and length into the hash-get helper registers - abi::emit_pop_reg(emitter, "x0"); // restore the associative-array hash-table pointer into the first hash-get helper register - } - Arch::X86_64 => { - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore the associative-array key pointer and length into the SysV hash-get helper registers - abi::emit_pop_reg(emitter, "rdi"); // restore the associative-array hash-table pointer into the first SysV hash-get helper register - } - } - abi::emit_call_label(emitter, "__rt_hash_get"); // lookup the associative-array key and leave the found flag in the integer result register - } else { - // -- indexed array: check if integer key is in bounds -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the indexed-array pointer while evaluating the integer key expression - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the integer key into the indexed-array key-exists helper argument register - abi::emit_pop_reg(emitter, "x0"); // restore the indexed-array pointer into the first helper argument register - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the integer key into the second SysV helper argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the indexed-array pointer into the first SysV helper argument register - } - } - abi::emit_call_label(emitter, "__rt_array_key_exists"); // check whether the integer key lies within the indexed-array bounds - } - - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/arrays/array_keys.rs b/src/codegen/builtins/arrays/array_keys.rs deleted file mode 100644 index 11890c38dc..0000000000 --- a/src/codegen/builtins/arrays/array_keys.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! Purpose: -//! Emits PHP `array_keys` builtin calls over associative or key-aware array data. -//! Owns key/value payload setup and runtime hash-helper invocation for array results or lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array key typing and Mixed payload tags must match the runtime hash-table representation. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `array_keys($arr)` builtin — returns all keys of an array. -/// -/// For `PhpType::AssocArray` keys, iterates the hash table in insertion order, -/// persists string keys via `__rt_str_persist`, boxes mixed keys via `__rt_mixed_from_value`, -/// and returns an `Array`. For indexed arrays, allocates `Array` with keys -/// `[0, 1, …, length-1]` via a counted loop. Both paths preserve ABI register conventions -/// per target (x86_64: rax/rdi/rsi, ARM64: x0/x1/x2) and push/pop preserved registers -/// across runtime helper calls. Stack layout (assoc path): `[iter_index(16)] [result_array(16)] [hash_ptr(16)]`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_keys()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_loaded_keys(&arr_ty, emitter, ctx) -} - -/// Emits assembly for loaded keys. -pub(crate) fn emit_loaded_keys( - arr_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) -> Option { - if let PhpType::AssocArray { key, .. } = &arr_ty { - let key_ty = *key.clone(); - let assoc_key_elem_size = if matches!(key_ty, PhpType::Str) { 16 } else { 8 }; - // -- associative array: iterate hash table and collect normalized PHP keys -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the associative-array hash-table pointer while allocating the result array - - // -- allocate new array for keys -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [x0]"); // load the associative-array entry count to size the result keys array exactly - emitter.instruction(&format!("mov x1, #{}", assoc_key_elem_size)); // materialize the result key element width for associative-array keys - } - Arch::X86_64 => { - emitter.instruction("mov rdi, QWORD PTR [rax]"); // load the associative-array entry count to size the result keys array exactly - emitter.instruction(&format!("mov rsi, {}", assoc_key_elem_size)); // materialize the result key element width for associative-array keys - } - } - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the result keys array with exact associative-array capacity - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the result keys array pointer across associative-array iteration - - // -- push iteration index onto stack -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str xzr, [sp, #-16]!"); // push iter_cursor = 0 (start from the associative-array header head slot) - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one temporary stack slot for the associative-array iterator cursor - emitter.instruction("mov QWORD PTR [rsp], 0"); // initialize the associative-array iterator cursor to the hash-header head sentinel - } - } - - // Stack: [iter_index(16)] [result_array(16)] [hash_ptr(16)] - - let loop_label = ctx.next_label("akeys_assoc_loop"); - let end_label = ctx.next_label("akeys_assoc_end"); - emitter.label(&loop_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #32]"); // load the associative-array hash-table pointer for the next insertion-order iteration step - emitter.instruction("ldr x1, [sp]"); // load the current associative-array iterator cursor - emitter.instruction("bl __rt_hash_iter_next"); // advance one associative-array insertion-order entry and return its key plus payload - emitter.instruction("cmn x0, #1"); // has associative-array iteration reached the done sentinel? - emitter.instruction(&format!("b.eq {}", end_label)); // stop once every associative-array key has been collected - emitter.instruction("str x0, [sp]"); // save the updated associative-array iterator cursor for the next loop step - emitter.instruction("ldr x9, [sp, #16]"); // load the result keys array pointer from the fixed stack layout - emitter.instruction("ldr x10, [x9]"); // load the current result keys array length before appending one more key - match &key_ty { - PhpType::Int | PhpType::Bool => { - emitter.instruction("add x11, x9, #24"); // point at the integer-key result payload region - emitter.instruction("str x1, [x11, x10, lsl #3]"); // store the normalized integer key into the next result keys slot - } - PhpType::Str => { - emitter.instruction("stp x9, x10, [sp, #-16]!"); // preserve result array pointer and length across key persistence - emitter.instruction("bl __rt_str_persist"); // copy the borrowed hash key so array_keys() owns its string result - emitter.instruction("ldp x9, x10, [sp], #16"); // restore result array pointer and length after key persistence - emitter.instruction("lsl x11, x10, #4"); // convert the result keys array length into a 16-byte string-slot offset - emitter.instruction("add x11, x9, x11"); // advance from the result keys array header to the selected string slot - emitter.instruction("add x11, x11, #24"); // skip the fixed indexed-array header to land on the string payload region - emitter.instruction("str x1, [x11]"); // store the owned associative-array key pointer into the next result keys slot - emitter.instruction("str x2, [x11, #8]"); // store the owned associative-array key length into the next result keys slot - } - _ => { - let key_string = ctx.next_label("akeys_assoc_key_string"); - let key_boxed = ctx.next_label("akeys_assoc_key_boxed"); - emitter.instruction("stp x9, x10, [sp, #-16]!"); // preserve result array pointer and length across mixed key boxing - emitter.instruction("cmn x2, #1"); // check whether this associative-array key is stored as an integer - emitter.instruction(&format!("b.ne {}", key_string)); // string keys need string-tagged mixed boxing - emitter.instruction("mov x0, #0"); // runtime tag 0 = integer key - emitter.instruction("mov x2, xzr"); // integer mixed payloads do not use the high word - emitter.instruction("bl __rt_mixed_from_value"); // box the integer key into an owned mixed cell - emitter.instruction(&format!("b {}", key_boxed)); // skip the string-key boxing path - emitter.label(&key_string); - emitter.instruction("mov x0, #1"); // runtime tag 1 = string key - emitter.instruction("bl __rt_mixed_from_value"); // persist and box the string key into an owned mixed cell - emitter.label(&key_boxed); - emitter.instruction("ldp x9, x10, [sp], #16"); // restore result array pointer and length after mixed key boxing - emitter.instruction("add x11, x9, #24"); // point at the mixed-key result payload region - emitter.instruction("str x0, [x11, x10, lsl #3]"); // store the boxed mixed key pointer into the next result keys slot - } - } - emitter.instruction("add x10, x10, #1"); // increment the result keys array length after storing one more key - emitter.instruction("str x10, [x9]"); // persist the updated result keys array length in the header - emitter.instruction(&format!("b {}", loop_label)); // continue collecting associative-array keys until iteration completes - } - Arch::X86_64 => { - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // load the associative-array hash-table pointer for the next insertion-order iteration step - emitter.instruction("mov rsi, QWORD PTR [rsp]"); // load the current associative-array iterator cursor - emitter.instruction("call __rt_hash_iter_next"); // advance one associative-array insertion-order entry and return its key plus payload - emitter.instruction("cmp rax, -1"); // has associative-array iteration reached the done sentinel? - emitter.instruction(&format!("je {}", end_label)); // stop once every associative-array key has been collected - emitter.instruction("mov QWORD PTR [rsp], rax"); // save the updated associative-array iterator cursor for the next loop step - emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load the result keys array pointer from the fixed stack layout - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the current result keys array length before appending one more key - match &key_ty { - PhpType::Int | PhpType::Bool => { - emitter.instruction("mov QWORD PTR [r10 + r11 * 8 + 24], rdi"); // store the normalized integer key into the next result keys slot - } - PhpType::Str => { - emitter.instruction("sub rsp, 16"); // reserve a temporary slot for result array state during key persistence - emitter.instruction("mov QWORD PTR [rsp], r10"); // preserve the result keys array pointer across key persistence - emitter.instruction("mov QWORD PTR [rsp + 8], r11"); // preserve the current result keys array length across key persistence - emitter.instruction("mov rax, rdi"); // move the borrowed hash key pointer into the string-persist helper input register - emitter.instruction("call __rt_str_persist"); // copy the borrowed hash key so array_keys() owns its string result - emitter.instruction("mov r10, QWORD PTR [rsp]"); // restore the result keys array pointer after key persistence - emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // restore the current result keys array length after key persistence - emitter.instruction("add rsp, 16"); // release the temporary result-array state slot - emitter.instruction("mov rcx, r11"); // copy the current result keys array length before scaling it into a string-slot offset - emitter.instruction("shl rcx, 4"); // convert the result keys array length into a 16-byte string-slot offset - emitter.instruction("add rcx, r10"); // advance from the result keys array header to the selected string slot - emitter.instruction("add rcx, 24"); // skip the fixed indexed-array header to land on the string payload region - emitter.instruction("mov QWORD PTR [rcx], rax"); // store the owned associative-array key pointer into the next result keys slot - emitter.instruction("mov QWORD PTR [rcx + 8], rdx"); // store the owned associative-array key length into the next result keys slot - } - _ => { - let key_string = ctx.next_label("akeys_assoc_key_string"); - let key_boxed = ctx.next_label("akeys_assoc_key_boxed"); - emitter.instruction("sub rsp, 16"); // reserve a temporary slot for result array state during mixed key boxing - emitter.instruction("mov QWORD PTR [rsp], r10"); // preserve the result keys array pointer across mixed key boxing - emitter.instruction("mov QWORD PTR [rsp + 8], r11"); // preserve the current result keys array length across mixed key boxing - emitter.instruction("cmp rdx, -1"); // check whether this associative-array key is stored as an integer - emitter.instruction(&format!("jne {}", key_string)); // string keys need string-tagged mixed boxing - emitter.instruction("xor esi, esi"); // integer mixed payloads do not use the high word - emitter.instruction("mov eax, 0"); // runtime tag 0 = integer key - emitter.instruction("call __rt_mixed_from_value"); // box the integer key into an owned mixed cell - emitter.instruction(&format!("jmp {}", key_boxed)); // skip the string-key boxing path - emitter.label(&key_string); - emitter.instruction("mov rsi, rdx"); // move the string key length into the mixed helper high-word register - emitter.instruction("mov eax, 1"); // runtime tag 1 = string key - emitter.instruction("call __rt_mixed_from_value"); // persist and box the string key into an owned mixed cell - emitter.label(&key_boxed); - emitter.instruction("mov r10, QWORD PTR [rsp]"); // restore the result keys array pointer after mixed key boxing - emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // restore the current result keys array length after mixed key boxing - emitter.instruction("add rsp, 16"); // release the temporary result-array state slot - emitter.instruction("mov QWORD PTR [r10 + r11 * 8 + 24], rax"); // store the boxed mixed key pointer into the next result keys slot - } - } - emitter.instruction("add r11, 1"); // increment the result keys array length after storing one more key - emitter.instruction("mov QWORD PTR [r10], r11"); // persist the updated result keys array length in the header - emitter.instruction(&format!("jmp {}", loop_label)); // continue collecting associative-array keys until iteration completes - } - } - - emitter.label(&end_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("add sp, sp, #16"); // drop the associative-array iterator cursor stack slot - emitter.instruction("ldr x0, [sp], #16"); // pop the result keys array pointer into the standard integer result register - emitter.instruction("add sp, sp, #16"); // drop the preserved associative-array hash-table pointer stack slot - } - Arch::X86_64 => { - emitter.instruction("add rsp, 16"); // drop the associative-array iterator cursor stack slot - emitter.instruction("mov rax, QWORD PTR [rsp]"); // move the result keys array pointer into the standard integer result register - emitter.instruction("add rsp, 16"); // drop the preserved result keys array pointer after loading it into the result register - emitter.instruction("add rsp, 16"); // drop the preserved associative-array hash-table pointer stack slot - } - } - - return Some(PhpType::Array(Box::new(key_ty))); - } - - // -- indexed array: return [0, 1, 2, ...] -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [x0]"); // load the source array length so the indexed keys result can be allocated exactly - emitter.instruction("str x9, [sp, #-16]!"); // preserve the source array length on the stack for the loop bound and final length store - emitter.instruction("mov x0, x9"); // pass the source array length as the exact result array capacity - emitter.instruction("mov x1, #8"); // integer key arrays use 8-byte scalar payload slots - } - Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rax]"); // load the source array length so the indexed keys result can be allocated exactly - emitter.instruction("sub rsp, 16"); // reserve one temporary stack slot for the indexed keys loop bound - emitter.instruction("mov QWORD PTR [rsp], r10"); // preserve the source array length on the stack for the loop bound and final length store - emitter.instruction("mov rdi, r10"); // pass the source array length as the exact result array capacity - emitter.instruction("mov rsi, 8"); // integer key arrays use 8-byte scalar payload slots - } - } - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the indexed keys result array with exact source-array capacity - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the indexed keys result array pointer across the fill loop - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str xzr, [sp, #-16]!"); // push the initial indexed keys loop counter - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one temporary stack slot for the indexed keys loop counter - emitter.instruction("mov QWORD PTR [rsp], 0"); // initialize the indexed keys loop counter to zero - } - } - let loop_label = ctx.next_label("akeys_loop"); - let end_label = ctx.next_label("akeys_end"); - emitter.label(&loop_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x12, [sp]"); // load the current indexed keys loop counter from the stack - emitter.instruction("ldr x9, [sp, #32]"); // reload the source array length from the fixed stack layout - emitter.instruction("cmp x12, x9"); // have we written every integer key from 0 to length - 1? - emitter.instruction(&format!("b.ge {}", end_label)); // stop once the indexed keys array is fully materialized - emitter.instruction("ldr x0, [sp, #16]"); // load the result keys array pointer from the fixed stack layout - emitter.instruction("add x10, x0, #24"); // point at the indexed-array payload region just after the fixed header - emitter.instruction("str x12, [x10, x12, lsl #3]"); // store the current loop counter as the next integer key payload - emitter.instruction("add x12, x12, #1"); // increment the indexed keys loop counter after storing one more key - emitter.instruction("str x12, [sp]"); // persist the updated indexed keys loop counter for the next iteration - emitter.instruction(&format!("b {}", loop_label)); // continue filling the indexed keys result array - } - Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rsp]"); // load the current indexed keys loop counter from the stack - emitter.instruction("mov r11, QWORD PTR [rsp + 32]"); // reload the source array length from the fixed stack layout - emitter.instruction("cmp r10, r11"); // have we written every integer key from 0 to length - 1? - emitter.instruction(&format!("jge {}", end_label)); // stop once the indexed keys array is fully materialized - emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // load the result keys array pointer from the fixed stack layout - emitter.instruction("mov QWORD PTR [rcx + r10 * 8 + 24], r10"); // store the current loop counter as the next integer key payload - emitter.instruction("add r10, 1"); // increment the indexed keys loop counter after storing one more key - emitter.instruction("mov QWORD PTR [rsp], r10"); // persist the updated indexed keys loop counter for the next iteration - emitter.instruction(&format!("jmp {}", loop_label)); // continue filling the indexed keys result array - } - } - emitter.label(&end_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("add sp, sp, #16"); // drop the indexed keys loop counter stack slot - emitter.instruction("ldr x0, [sp, #0]"); // reload the result keys array pointer before finalizing its logical length - emitter.instruction("ldr x9, [sp, #16]"); // reload the exact source array length from the remaining stack layout - emitter.instruction("str x9, [x0]"); // stamp the indexed keys result array length once the payload slots are filled - emitter.instruction("ldr x0, [sp], #16"); // pop the finalized result keys array pointer into the standard integer result register - emitter.instruction("add sp, sp, #16"); // drop the preserved source array length stack slot - } - Arch::X86_64 => { - emitter.instruction("add rsp, 16"); // drop the indexed keys loop counter stack slot - emitter.instruction("mov rax, QWORD PTR [rsp]"); // reload the result keys array pointer before finalizing its logical length - emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // reload the exact source array length from the remaining stack layout - emitter.instruction("mov QWORD PTR [rax], r10"); // stamp the indexed keys result array length once the payload slots are filled - emitter.instruction("add rsp, 16"); // drop the preserved result keys array pointer after loading it into the result register - emitter.instruction("add rsp, 16"); // drop the preserved source array length stack slot - } - } - - Some(PhpType::Array(Box::new(PhpType::Int))) -} diff --git a/src/codegen/builtins/arrays/array_map.rs b/src/codegen/builtins/arrays/array_map.rs deleted file mode 100644 index 4886b4b319..0000000000 --- a/src/codegen/builtins/arrays/array_map.rs +++ /dev/null @@ -1,320 +0,0 @@ -//! Purpose: -//! Emits PHP `array_map` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::abi; -use crate::codegen::callable_dispatch::{self, RuntimeCallableSelector}; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::array_map_callback_returns_str::callback_returns_str; -use super::callback_env; -use super::call_user_func_array; -use super::runtime_callable_array_callback; - -/// Emits the `array_map` builtin call. -/// -/// Lowers `array_map(callback, array)` to a runtime helper call, selecting -/// between `__rt_array_map` (scalar results) and `__rt_array_map_str` (string -/// results) based on the inferred callback return type. -/// -/// ## Evaluation order -/// The callback expression is evaluated first into `call_reg`, then the array -/// argument is evaluated. Both are pushed to the temporary stack before the -/// runtime call so they occupy the first two integer argument registers. -/// -/// ## Capture handling -/// When `callback_env::materialize_callback_address` reports captures, a wrapper -/// environment is built on the temporary stack with the callback entry point in -/// slot 0, the array pointer in the last slot, and capture values in between. -/// The wrapper label address is passed as the first argument, the array pointer -/// as the second, and the environment pointer as the third. -/// Branch-shaped captured callable expressions store the selected descriptor -/// itself in the environment and invoke it through the uniform descriptor invoker. -/// -/// ## Runtime helpers -/// - `__rt_array_map`: result array element type is `PhpType::Int` -/// - `__rt_array_map_str`: result array element type is `PhpType::Str` -/// - `__rt_array_map_str_owned`: descriptor-backed string results that are already owned -/// -/// Returns `Some(PhpType::Array(Box::new(element_type)))` where element type is -/// `Str` if `callback_returns_str` is true, otherwise `Int`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_map()"); - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - - // -- determine callback return type at compile time -- - let returns_str = callback_returns_str(args, ctx); - let source_array_ty = crate::codegen::functions::infer_contextual_type(&args[1], ctx); - let source_elem_ty = match &source_array_ty { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Int, - }; - - if call_user_func_array::callback_is_runtime_string(&args[0], ctx) { - emit_runtime_string_descriptor_map( - &args[0], - &args[1], - &source_array_ty, - &source_elem_ty, - emitter, - ctx, - data, - ); - return Some(PhpType::Array(Box::new(PhpType::Mixed))); - } - - if let Some(array_callback) = - callback_env::resolve_callable_array_descriptor_callback(&args[0], ctx, data) - { - let descriptor_return_type = if matches!( - array_callback.sig.return_type.codegen_repr(), - PhpType::Str - ) { - PhpType::Str - } else { - PhpType::Int - }; - let descriptor_prefix_types = array_callback - .receiver_prefix - .iter() - .map(|(_, ty)| ty.clone()) - .collect(); - let wrapper = callback_env::emit_descriptor_callback_env_from_static_descriptor( - &array_callback.descriptor_label, - vec![source_elem_ty.clone()], - descriptor_prefix_types, - descriptor_return_type.clone(), - emitter, - ctx, - ); - if let Some((receiver, receiver_ty)) = &array_callback.receiver_prefix { - emit_expr(receiver, emitter, ctx, data); - callback_env::store_descriptor_callback_prefix_result( - &wrapper, - 0, - receiver_ty, - emitter, - ); - } - - let _arr_ty = emit_expr(&args[1], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", array_arg_reg, result_reg)); // preserve the mapped array pointer before descriptor callback setup - callback_env::store_descriptor_callback_array_reg(&wrapper, array_arg_reg, emitter); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - if matches!(descriptor_return_type, PhpType::Str) { - abi::emit_call_label(emitter, "__rt_array_map_str_owned"); // call the string map helper with a callable-array descriptor environment - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Array(Box::new(PhpType::Str))); - } - abi::emit_call_label(emitter, "__rt_array_map"); // call the scalar map helper with a callable-array descriptor environment - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Array(Box::new(PhpType::Int))); - } - - if runtime_callable_array_callback::emit_before_array( - &args[0], - &args[1], - array_arg_reg, - vec![source_elem_ty.clone()], - PhpType::Mixed, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_map_mixed"); // call the mixed-result map helper with a runtime callable-array descriptor environment - }, - ) { - return Some(PhpType::Array(Box::new(PhpType::Mixed))); - } - - if callback_env::expr_call_needs_descriptor_callback_env(&args[0], ctx) - && callback_env::descriptor_callback_env_supported(&args[0]) - { - // -- evaluate the selected descriptor before the mapped array, matching PHP source order -- - emit_expr(&args[0], emitter, ctx, data); - let retained_borrowed = - callback_env::retain_borrowed_descriptor_callback_result(&args[0], emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the selected callable descriptor across mapped-array evaluation - - // -- evaluate the array argument after the callback expression -- - let _arr_ty = emit_expr(&args[1], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", array_arg_reg, result_reg)); // preserve the mapped array pointer before restoring the descriptor - abi::emit_pop_reg(emitter, result_reg); // restore the selected callable descriptor as the current result - - let descriptor_return_type = if returns_str { - PhpType::Str - } else { - PhpType::Int - }; - let wrapper = if retained_borrowed { - callback_env::emit_descriptor_callback_env_from_retained_result( - &args[0], - array_arg_reg, - vec![source_elem_ty.clone()], - descriptor_return_type.clone(), - emitter, - ctx, - ) - } else { - callback_env::emit_descriptor_callback_env_from_result( - &args[0], - array_arg_reg, - vec![source_elem_ty.clone()], - descriptor_return_type.clone(), - emitter, - ctx, - ) - } - .expect("descriptor callback env support checked before emitting callback"); - - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - if returns_str { - abi::emit_call_label(emitter, "__rt_array_map_str_owned"); // call the string map helper that consumes descriptor-owned string results - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Array(Box::new(PhpType::Str))); - } - abi::emit_call_label(emitter, "__rt_array_map"); // call the scalar array_map runtime helper with a descriptor environment - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Array(Box::new(PhpType::Int))); - } - - // -- evaluate the callback argument first, matching PHP source order -- - let captures = - callback_env::materialize_callback_address(&args[0], call_reg, emitter, ctx, data); - abi::emit_push_reg(emitter, call_reg); // save the callback address across mapped-array evaluation - - // -- evaluate the array argument -- - let _arr_ty = emit_expr(&args[1], emitter, ctx, data); - - // -- save array pointer before preparing runtime arguments -- - abi::emit_push_reg(emitter, result_reg); // push the array pointer onto the temporary stack - - if captures.is_empty() { - abi::emit_pop_reg(emitter, array_arg_reg); // pop the mapped array pointer into the second runtime argument register - abi::emit_pop_reg(emitter, callback_arg_reg); // pop the callback address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - } else { - abi::emit_pop_reg(emitter, result_reg); // recover the mapped array pointer before building the capture environment - abi::emit_pop_reg(emitter, call_reg); // recover the callback entry point for env slot zero - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![source_elem_ty.clone()], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - if returns_str { - abi::emit_call_label(emitter, "__rt_array_map_str"); // call the string-producing array_map runtime helper - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Array(Box::new(PhpType::Str))); - } - abi::emit_call_label(emitter, "__rt_array_map"); // call the scalar array_map runtime helper - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Array(Box::new(PhpType::Int))); - } - - if returns_str { - abi::emit_call_label(emitter, "__rt_array_map_str"); // call the string-producing array_map runtime helper - Some(PhpType::Array(Box::new(PhpType::Str))) - } else { - abi::emit_call_label(emitter, "__rt_array_map"); // call the scalar array_map runtime helper - Some(PhpType::Array(Box::new(PhpType::Int))) - } -} - -/// Emits runtime-string callback selection through descriptor-backed `array_map()`. -fn emit_runtime_string_descriptor_map( - callback: &Expr, - array: &Expr, - source_array_ty: &PhpType, - source_elem_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - - let callback_ty = emit_expr(callback, emitter, ctx, data); - debug_assert!(matches!(callback_ty.codegen_repr(), PhpType::Str)); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime string callback name across mapped-array evaluation - - let _array_ty = emit_expr(array, emitter, ctx, data); - abi::emit_push_reg(emitter, result_reg); // preserve the mapped array while selecting the runtime string descriptor - - let cases = callable_dispatch::runtime_callable_cases(ctx, data, &[], Some(source_array_ty)); - let done_label = ctx.next_label("array_map_runtime_string_done"); - let selector = RuntimeCallableSelector::StringNameStack { - ptr_offset: 16, - len_offset: 24, - call_reg, - }; - - for case in &cases { - let next_case = ctx.next_label("array_map_runtime_string_next"); - callable_dispatch::emit_branch_if_callable_case_mismatch( - &selector, - case, - &next_case, - emitter, - ctx, - data, - ); - abi::emit_load_temporary_stack_slot(emitter, array_arg_reg, 0); - let wrapper = callback_env::emit_descriptor_callback_env_from_static_descriptor( - &case.descriptor_label, - vec![source_elem_ty.clone()], - Vec::new(), - PhpType::Mixed, - emitter, - ctx, - ); - callback_env::store_descriptor_callback_array_reg(&wrapper, array_arg_reg, emitter); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_map_mixed"); // map through the selected runtime string descriptor invoker - callback_env::release_descriptor_callback_env(&wrapper, emitter); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - - call_user_func_array::emit_dynamic_string_callback_abort(emitter, data); - emitter.label(&done_label); - abi::emit_release_temporary_stack(emitter, 32); // discard the saved mapped array and runtime string callback name -} diff --git a/src/codegen/builtins/arrays/array_map_callback_returns_str.rs b/src/codegen/builtins/arrays/array_map_callback_returns_str.rs deleted file mode 100644 index 1bb7b13a98..0000000000 --- a/src/codegen/builtins/arrays/array_map_callback_returns_str.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Purpose: -//! Determines when array_map callback lowering should allocate string element storage. -//! Bridges callable targets, closure metadata, and inferred PHP return types for result arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::array_map::emit()`. -//! -//! Key details: -//! - Return-type guesses must stay conservative so runtime array payload shape remains valid. - -use crate::codegen::context::Context; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver, StmtKind}; -use crate::types::PhpType; - -use super::array_map_expr_is_str::expr_is_str; - -/// Infers whether an `array_map` callback returns a PHP `string` type. -/// -/// Examines the callback expression's AST to determine if its return type is -/// guaranteed to be `PhpType::Str`. This drives whether the result array's -/// element storage can be sized for string payloads. -/// -/// ## Expression handling -/// -/// - **Closure**: Scans the body for a terminal `return` statement and delegates -/// to `expr_is_str` on the returned expression. Returns `false` if no return -/// is found (conservative: ambiguous returns default to non-string). -/// - **StringLiteral**: Looks up the function signature by name and checks -/// `return_type == PhpType::Str`. -/// - **Variable**: Looks up the closure signature stored under the variable name -/// in `ctx.closure_sigs` and checks the return type. -/// - **FirstClassCallable**: Dispatches to function, static method, or instance -/// method lookup via `ctx.functions`, `ctx.classes`, and contextual type -/// inference. -/// -/// Returns `false` for any unrecognized or dynamic callback form, preserving -/// conservative runtime array layout behavior. -pub(super) fn callback_returns_str(args: &[Expr], ctx: &Context) -> bool { - callback_expr_returns_str(&args[0], ctx) -} - -/// Infers whether one callback expression has a statically string-returning signature. -fn callback_expr_returns_str(callback: &Expr, ctx: &Context) -> bool { - match &callback.kind { - ExprKind::Closure { body, .. } => { - for stmt in body { - if let StmtKind::Return(Some(expr)) = &stmt.kind { - return expr_is_str(expr); - } - } - false - } - ExprKind::StringLiteral(name) => { - if let Some(sig) = ctx.functions.get(name) { - return sig.return_type == PhpType::Str; - } - false - } - ExprKind::Variable(name) => ctx - .closure_sigs - .get(name) - .map(|sig| sig.return_type == PhpType::Str) - .or_else(|| { - ctx.callable_array_targets - .get(name) - .map(|target| callable_target_returns_str(target, ctx)) - }) - .or_else(|| { - ctx.first_class_callable_targets.get(name).map(|target| { - crate::codegen::expr::calls::first_class_callable_sig(target, ctx) - .map(|sig| sig.return_type == PhpType::Str) - .unwrap_or(false) - }) - }) - .unwrap_or(false), - ExprKind::FirstClassCallable(target) => match target { - CallableTarget::Function(name) => ctx - .functions - .get(name.as_str()) - .map(|sig| sig.return_type == PhpType::Str) - .unwrap_or(false), - CallableTarget::StaticMethod { receiver, method } => { - let class_name = match receiver { - StaticReceiver::Named(name) => Some(name.as_str().to_string()), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.parent.clone()), - }; - class_name - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.static_methods.get(method)) - .map(|sig| sig.return_type == PhpType::Str) - .unwrap_or(false) - } - CallableTarget::Method { object, method } => { - let object_ty = crate::codegen::functions::infer_contextual_type(object, ctx); - let Some(class_name) = crate::codegen::functions::singular_object_class(&object_ty) - else { - return false; - }; - ctx.classes - .get(class_name) - .and_then(|class_info| class_info.methods.get(method)) - .map(|sig| sig.return_type == PhpType::Str) - .unwrap_or(false) - } - }, - ExprKind::FunctionCall { name, .. } => ctx - .callable_return_sigs - .get(name.as_str()) - .map(|sig| sig.return_type == PhpType::Str) - .unwrap_or(false), - ExprKind::Assignment { value, .. } => callback_expr_returns_str(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => { - callback_expr_returns_str(then_expr, ctx) - && callback_expr_returns_str(else_expr, ctx) - } - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - callback_expr_returns_str(value, ctx) - && callback_expr_returns_str(default, ctx) - } - _ => false, - } -} - -/// Returns true when a callable-array target has a statically string return type. -fn callable_target_returns_str(target: &CallableTarget, ctx: &Context) -> bool { - match target { - CallableTarget::Function(name) => ctx - .functions - .get(name.as_str()) - .map(|sig| sig.return_type == PhpType::Str) - .unwrap_or(false), - CallableTarget::StaticMethod { receiver, method } => { - let class_name = match receiver { - StaticReceiver::Named(name) => Some(name.as_str().to_string()), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.parent.clone()), - }; - class_name - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.static_methods.get(method)) - .map(|sig| sig.return_type == PhpType::Str) - .unwrap_or(false) - } - CallableTarget::Method { object, method } => { - let object_ty = crate::codegen::functions::infer_contextual_type(object, ctx); - let Some(class_name) = crate::codegen::functions::singular_object_class(&object_ty) - else { - return false; - }; - ctx.classes - .get(class_name) - .and_then(|class_info| class_info.methods.get(method)) - .map(|sig| sig.return_type == PhpType::Str) - .unwrap_or(false) - } - } -} diff --git a/src/codegen/builtins/arrays/array_map_expr_is_str.rs b/src/codegen/builtins/arrays/array_map_expr_is_str.rs deleted file mode 100644 index 1269fa62aa..0000000000 --- a/src/codegen/builtins/arrays/array_map_expr_is_str.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Classifies array_map callback expressions whose return value can be treated as a string. -//! Keeps AST-level callback inspection separate from array_map emission mechanics. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::array_map_callback_returns_str::callback_returns_str()`. -//! -//! Key details: -//! - Only use syntactic facts here; semantic callable validation remains in the type checker. - -use crate::parser::ast::{BinOp, Expr, ExprKind}; - -/// Returns true if the expression syntactically produces a string result. -/// -/// Recognizes: string literals, `.` (concat) binary ops, explicit `(string)` casts, -/// and calls to known string-returning builtins (`substr`, `strtolower`, `strtoupper`, -/// `trim`, `ltrim`, `rtrim`, `str_repeat`, `strrev`, `chr`, `str_replace`, `ucfirst`, -/// `lcfirst`, `ucwords`, `str_pad`, `implode`, `join`, `sprintf`, `str_word_count`, -/// `nl2br`, `wordwrap`, `number_format`, `chunk_split`, `md5`, `sha1`, `hash`). -/// -/// This is a purely syntactic check; no semantic callable validation is performed. -pub(super) fn expr_is_str(expr: &Expr) -> bool { - match &expr.kind { - ExprKind::StringLiteral(_) => true, - ExprKind::BinaryOp { - op: BinOp::Concat, .. - } => true, - ExprKind::FunctionCall { name, .. } => { - matches!( - name.as_str(), - "substr" - | "strtolower" - | "strtoupper" - | "trim" - | "ltrim" - | "rtrim" - | "str_repeat" - | "strrev" - | "chr" - | "str_replace" - | "ucfirst" - | "lcfirst" - | "ucwords" - | "str_pad" - | "implode" - | "join" - | "sprintf" - | "str_word_count" - | "nl2br" - | "wordwrap" - | "number_format" - | "chunk_split" - | "md5" - | "sha1" - | "hash" - ) - } - ExprKind::Cast { - target: crate::parser::ast::CastType::String, - .. - } => true, - _ => false, - } -} diff --git a/src/codegen/builtins/arrays/array_merge.rs b/src/codegen/builtins/arrays/array_merge.rs deleted file mode 100644 index 8075d333ec..0000000000 --- a/src/codegen/builtins/arrays/array_merge.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Purpose: -//! Emits PHP `array_merge` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::functions; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `array_merge` builtin. -/// -/// Combines two PHP array operands into a single merged array at runtime. -/// The returned `PhpType` reflects the first operand's inner type, or `Array(Int)` -/// when the first operand is a scalar. -/// -/// # Arguments -/// * `_name` — unused, present to match the builtin emitter signature -/// * `args` — two PHP expressions: `[0]` is the first array, `[1]` is the second -/// * `emitter` — target-specific assembly emitter -/// * `ctx` — codegen context (frame layout, variables, class metadata) -/// * `data` — data section for relocations and static data -/// -/// # ABI behavior -/// * **x86_64**: pushes first array pointer in `rax` to the stack, evaluates second -/// array into `rax`, then moves pointers into `rdi`/`rsi` for the runtime call. -/// * **ARM64**: pushes first array pointer to the stack, evaluates second into `x0`, -/// then loads both pointers into `x0`/`x1` for the runtime call. -/// -/// # Runtime helpers -/// * `__rt_array_merge` — merges two scalar/indexed arrays (no refcount management) -/// * `__rt_array_merge_refcounted` — merges arrays with refcounted elements, retaining -/// borrowed heap references -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_merge()"); - let second_arr_ty = args - .get(1) - .map(|arg| functions::infer_contextual_type(arg, ctx)); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let result_ty = array_merge_result_type(arr_ty.clone(), second_arr_ty.as_ref()); - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - if emitter.target.arch == Arch::X86_64 { - abi::emit_push_reg(emitter, "rax"); // preserve the first scalar indexed-array pointer while evaluating the second merge operand - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rsi, rax"); // move the second scalar indexed-array pointer into the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the first scalar indexed-array pointer into the first x86_64 runtime argument register - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_merge_refcounted"); // merge the two refcounted indexed arrays through the x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_merge"); // merge the two scalar indexed arrays through the x86_64 runtime helper - } - - return Some(result_ty); - } - - // -- save first array, evaluate second array -- - emitter.instruction("str x0, [sp, #-16]!"); // push first array pointer onto stack - emit_expr(&args[1], emitter, ctx, data); - // -- call runtime to merge two arrays -- - emitter.instruction("mov x1, x0"); // move second array pointer to x1 - emitter.instruction("ldr x0, [sp], #16"); // pop first array pointer into x0 - if uses_refcounted_runtime { - emitter.instruction("bl __rt_array_merge_refcounted"); // merge arrays while retaining borrowed heap elements - } else { - emitter.instruction("bl __rt_array_merge"); // call runtime: merge arrays → x0=new array - } - - Some(result_ty) -} - -/// Infers the legacy emitter result type for `array_merge()`. -/// -/// The runtime helper can copy scalar 8-byte payloads from the right operand even when -/// the first operand is statically empty, so the result may adopt the right element type -/// for that supported subset. -fn array_merge_result_type(first: PhpType, second: Option<&PhpType>) -> PhpType { - match first { - PhpType::Array(elem) if is_empty_array_element_type(elem.as_ref()) => match second { - Some(PhpType::Array(right)) if is_scalar_merge_element_type(right.as_ref()) => { - PhpType::Array(right.clone()) - } - _ => PhpType::Array(elem), - }, - PhpType::Array(elem) => PhpType::Array(elem), - _ => PhpType::Array(Box::new(PhpType::Int)), - } -} - -/// 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 - ) -} diff --git a/src/codegen/builtins/arrays/array_pad.rs b/src/codegen/builtins/arrays/array_pad.rs deleted file mode 100644 index 33818558b9..0000000000 --- a/src/codegen/builtins/arrays/array_pad.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Purpose: -//! Emits PHP `array_pad` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_pad($array, $target_size, $pad_value)` builtin call. -/// -/// Saves the input array pointer, evaluates `$target_size` and `$pad_value` from left to right, -/// then calls the appropriate runtime helper (`__rt_array_pad` or `__rt_array_pad_refcounted`). -/// The chosen runtime path depends on whether the input array is refcounted. -/// Returns the type of the resulting padded array (preserves the inner type for Array inputs). -/// -/// # Arguments -/// * `_name` - Unused; present to match the builtin emitter signature. -/// * `args` - Three expressions: the input array, the target size, and the pad value. -/// * `emitter` - The assembly emitter for the target architecture. -/// * `ctx` - Compilation context (types, locals, etc.). -/// * `data` - Data section for relocations and constants. -/// -/// # Architecture -/// - **x86_64**: Uses `push`/`pop` to preserve registers across argument evaluation. -/// - **ARM64**: Uses pre-decrement store/load to push arguments onto the stack. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_pad()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - if emitter.target.arch == Arch::X86_64 { - abi::emit_push_reg(emitter, "rax"); // preserve the source scalar indexed-array pointer while evaluating the target size expression - let size_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &size_ty); // unbox a Mixed/Union target size into a raw integer - abi::emit_push_reg(emitter, "rax"); // preserve the requested target size while evaluating the scalar pad value - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction("mov rdx, rax"); // move the scalar pad value into the third x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rsi"); // restore the requested target size into the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the source scalar indexed-array pointer into the first x86_64 runtime argument register - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_pad_refcounted"); // pad the refcounted indexed array through the x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_pad"); // pad the scalar indexed array through the x86_64 runtime helper - } - - return match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } - - // -- save array pointer, evaluate target size -- - emitter.instruction("str x0, [sp, #-16]!"); // push array pointer onto stack - let size_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &size_ty); // unbox a Mixed/Union target size into a raw integer - // -- save target size, evaluate pad value -- - emitter.instruction("str x0, [sp, #-16]!"); // push target size onto stack - emit_expr(&args[2], emitter, ctx, data); - // -- set up three-arg call: array, size, value -- - emitter.instruction("mov x2, x0"); // move pad value to x2 (third arg) - emitter.instruction("ldr x1, [sp], #16"); // pop target size into x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer into x0 (first arg) - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_pad_refcounted" - } else { - "bl __rt_array_pad" - }; - emitter.instruction(runtime_call); // call runtime: pad array → x0=new array - - match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - } -} diff --git a/src/codegen/builtins/arrays/array_pop.rs b/src/codegen/builtins/arrays/array_pop.rs deleted file mode 100644 index 8f9de75d2c..0000000000 --- a/src/codegen/builtins/arrays/array_pop.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! Purpose: -//! Emits PHP `array_pop` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::abi; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the PHP `array_pop` builtin: removes and returns the last element of an indexed array. -/// -/// For x86_64: calls `ensure_unique_arg` and `store_mutating_arg` to handle COW and write back -/// the modified array pointer to caller storage, then loads the array header from `rax` to -/// obtain/modify the length and load the popped element into registers. -/// -/// For ARM64: loads array header from `x0`, decrements length, and materializes the popped -/// element into return registers (`x0`/`d0` for scalars, `x1`/`x2` for strings). -/// -/// On empty array: returns a null sentinel (i64::MAX - 1 on x86_64, 0x7FFFFFFFFFFFFFFE on ARM64). -/// -/// # Arguments -/// * `_name` - unused (builtin dispatches by name) -/// * `args` - must contain exactly one argument: the array expression -/// * `emitter` - target-aware code emitter -/// * `ctx` - codegen context (labels, frame layout, variable allocation) -/// * `data` - data section for constants -/// -/// # Returns -/// `Some(elem_ty)` where `elem_ty` is the element type (PhpType::Int, PhpType::Str, or PhpType::Array -/// for arrays-of-arrays), or `None` if the array type could not be determined. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_pop()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let elem_ty = match &arr_ty { - PhpType::Array(t) => *t.clone(), - _ => PhpType::Int, - }; - - let empty_label = ctx.next_label("array_pop_empty"); - let end_label = ctx.next_label("array_pop_end"); - let tagged_int_result = - crate::codegen::sentinels::null_repr_is_tagged() && matches!(elem_ty, PhpType::Int); - - if emitter.target.arch == Arch::X86_64 { - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - emitter.instruction("mov r10, QWORD PTR [rax]"); // load the current indexed-array length before checking whether the pop operation is empty - emitter.instruction(&format!("test r10, r10")); // check whether the indexed array currently stores any elements - emitter.instruction(&format!("jz {}", empty_label)); // return null when array_pop runs on an empty indexed array - emitter.instruction("sub r10, 1"); // decrement the indexed-array length to point at the removed last element - emitter.instruction("mov QWORD PTR [rax], r10"); // persist the decremented indexed-array length back into the array header - match &elem_ty { - PhpType::Str => { - emitter.instruction("lea r11, [rax + 24]"); // compute the first string-slot payload address in the source indexed array - emitter.instruction("shl r10, 4"); // scale the removed-element index by the 16-byte string-slot size - emitter.instruction("add r11, r10"); // advance to the removed string-slot payload within the indexed array - emitter.instruction("mov rax, QWORD PTR [r11]"); // load the removed string pointer into the primary x86_64 string result register - emitter.instruction("mov rdx, QWORD PTR [r11 + 8]"); // load the removed string length into the secondary x86_64 string result register - } - _ => { - emitter.instruction("lea r11, [rax + 24]"); // compute the first scalar-slot payload address in the source indexed array - emitter.instruction("mov rax, QWORD PTR [r11 + r10 * 8]"); // load the removed scalar payload from the last live indexed-array slot - } - } - if tagged_int_result { - crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - } - emitter.instruction(&format!("jmp {}", end_label)); // skip the empty-array null sentinel path after loading the removed payload - - emitter.label(&empty_label); - if tagged_int_result { - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - } else { - abi::emit_load_int_immediate(emitter, "rax", NULL_SENTINEL); // materialize the shared null sentinel as the empty-array result on x86_64 - } - emitter.label(&end_label); - - if tagged_int_result { - return Some(PhpType::TaggedScalar); - } - return Some(elem_ty); - } - - // -- check if array is empty -- - emitter.instruction("ldr x9, [x0]"); // load current array length into x9 - emitter.instruction(&format!("cbz x9, {}", empty_label)); // if length == 0, jump to empty handler - - // -- decrement array length to remove last element -- - emitter.instruction("sub x9, x9, #1"); // decrement length by 1 - emitter.instruction("str x9, [x0]"); // store decremented length back to array header - match &elem_ty { - PhpType::Int => { - // -- load the popped integer element -- - emitter.instruction("add x0, x0, #24"); // advance past array header (24 bytes) to data area - emitter.instruction("ldr x0, [x0, x9, lsl #3]"); // load int at index x9 (offset = x9 * 8 bytes) - } - PhpType::Str => { - // -- load the popped string element (ptr + len) -- - emitter.instruction("lsl x10, x9, #4"); // multiply index by 16 (each string entry = 16 bytes) - emitter.instruction("add x0, x0, x10"); // advance pointer by element offset - emitter.instruction("add x0, x0, #24"); // skip past array header to data area - emitter.instruction("ldr x1, [x0]"); // load string pointer from element - emitter.instruction("ldr x2, [x0, #8]"); // load string length from element + 8 - } - _ => {} - } - if tagged_int_result { - crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - } - emitter.instruction(&format!("b {}", end_label)); // skip empty handler - - // -- empty array: return null sentinel -- - emitter.label(&empty_label); - if tagged_int_result { - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - } else { - let sentinel = NULL_SENTINEL as u64; - emitter.instruction(&format!("movz x0, #0x{:X}", sentinel & 0xFFFF)); // load null sentinel bits [15:0] - emitter.instruction(&format!("movk x0, #0x{:X}, lsl #16", (sentinel >> 16) & 0xFFFF)); // load null sentinel bits [31:16] - emitter.instruction(&format!("movk x0, #0x{:X}, lsl #32", (sentinel >> 32) & 0xFFFF)); // load null sentinel bits [47:32] - emitter.instruction(&format!("movk x0, #0x{:X}, lsl #48", (sentinel >> 48) & 0xFFFF)); // load null sentinel bits [63:48] = 0x7FFFFFFFFFFFFFFE - } - - emitter.label(&end_label); - - if tagged_int_result { - return Some(PhpType::TaggedScalar); - } - Some(elem_ty) -} diff --git a/src/codegen/builtins/arrays/array_product.rs b/src/codegen/builtins/arrays/array_product.rs deleted file mode 100644 index 2a98fb180d..0000000000 --- a/src/codegen/builtins/arrays/array_product.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Purpose: -//! Emits PHP `array_product` builtin calls for array values. -//! Materializes arguments and delegates payload work to the matching runtime helper or inline lowering. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array element type and ownership assumptions must match the type checker and runtime layout. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code to compute the product of all numeric values in a PHP array. -/// -/// Arguments: -/// - `args[0]` must be the array expression (already emitted by caller). -/// -/// ABI: -/// - x86_64: passes array pointer via `rdi`, returns product in `rax` as `PhpType::Int`. -/// - ARM64: calls `__rt_array_product` runtime helper, returns product in `x0` as `PhpType::Int`. -/// -/// Side effects: calls `__rt_array_product` runtime routine which iterates the array. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_product()"); - emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the source scalar indexed-array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_array_product"); // multiply the scalar indexed-array payloads through the x86_64 runtime helper - return Some(PhpType::Int); - } - - // -- call runtime to compute product of all array elements -- - emitter.instruction("bl __rt_array_product"); // call runtime: multiply array elements → x0=product - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/arrays/array_push.rs b/src/codegen/builtins/arrays/array_push.rs deleted file mode 100644 index 8ab86171f7..0000000000 --- a/src/codegen/builtins/arrays/array_push.rs +++ /dev/null @@ -1,283 +0,0 @@ -//! Purpose: -//! Emits PHP `array_push` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::abi; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{emit_expr, expr_result_heap_ownership}; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emit the `array_push` builtin call. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_push()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emit_array_push_linux_x86_64(args, &arr_ty, emitter, ctx, data); - return Some(PhpType::Void); - } - - // -- save array pointer, evaluate value to push -- - emitter.instruction("str x0, [sp, #-16]!"); // push array pointer onto stack - let elem_ty = indexed_array_elem_type(&arr_ty); - let source_owned = expr_result_heap_ownership(&args[1]) == HeapOwnership::Owned; - let mut val_ty = emit_expr(&args[1], emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut val_ty); - let effective_elem_ty = effective_indexed_push_type(&elem_ty, &val_ty, ctx); - let converted_to_mixed = - matches!(effective_elem_ty, PhpType::Mixed) && !matches!(elem_ty, PhpType::Mixed); - let mut boxed_value_for_mixed = false; - if matches!(effective_elem_ty, PhpType::Mixed) - && !matches!(val_ty, PhpType::Mixed | PhpType::Union(_)) - { - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, &args[1], &val_ty, - ); - val_ty = PhpType::Mixed; - boxed_value_for_mixed = true; - } else if matches!(effective_elem_ty, PhpType::Mixed) && matches!(val_ty, PhpType::Union(_)) { - val_ty = PhpType::Mixed; - } - let release_after_refcounted_push = boxed_value_for_mixed - || boxed_iterable - || (source_owned - && matches!( - val_ty, - PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Object(_) - )); - emitter.instruction("ldr x9, [sp], #16"); // pop saved array pointer into x9 - if elem_ty != effective_elem_ty { - update_array_push_arg_type(&args[0], &effective_elem_ty, ctx); - } - if converted_to_mixed { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed pushed value across mixed-array conversion - emitter.instruction("mov x0, x9"); // pass the current indexed-array pointer to the mixed conversion helper - abi::emit_load_int_immediate( - emitter, - "x1", - crate::codegen::runtime_value_tag(&elem_ty) as i64, - ); - abi::emit_call_label(emitter, "__rt_array_to_mixed"); // box existing typed slots before array_push stores a heterogeneous value - emitter.instruction("mov x9, x0"); // keep the converted indexed-array pointer as the push receiver - emitter.instruction("ldr x0, [sp], #16"); // restore the boxed pushed value after conversion - } - match &val_ty { - PhpType::Int | PhpType::Bool => { - // -- push integer/bool value onto array -- - emitter.instruction("mov x1, x0"); // move integer value to x1 (second arg) - emitter.instruction("mov x0, x9"); // move array pointer to x0 (first arg) - emitter.instruction("bl __rt_array_push_int"); // call runtime: append integer to array - } - PhpType::Float => { - // -- push float value onto array (store as 8-byte int via bit cast) -- - emitter.instruction("fmov x1, d0"); // move float bits to integer register - emitter.instruction("mov x0, x9"); // move array pointer to x0 (first arg) - emitter.instruction("bl __rt_array_push_int"); // call runtime: append float bits as 8 bytes - } - PhpType::Str => { - // -- push string to array (push_str persists to heap internally) -- - emitter.instruction("mov x0, x9"); // move array pointer to x0 - emitter.instruction("bl __rt_array_push_str"); // call runtime: persist + append string to array - } - PhpType::Mixed | PhpType::Union(_) | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { - // -- push nested refcounted pointer onto array -- - if release_after_refcounted_push { - abi::emit_push_reg(emitter, "x0"); - } - emitter.instruction("mov x1, x0"); // move pointer value to x1 - emitter.instruction("mov x0, x9"); // move outer array pointer to x0 - emitter.instruction("bl __rt_array_push_refcounted"); // append retained pointer and stamp array metadata - if release_after_refcounted_push { - crate::codegen::emit_release_pushed_refcounted_temp_after_array_push(emitter, &val_ty); - } - } - PhpType::Callable => { - // -- push callable descriptor pointer onto array as a plain 8-byte scalar -- - emitter.instruction("mov x1, x0"); // move callable descriptor pointer value to x1 - emitter.instruction("mov x0, x9"); // move outer array pointer to x0 - emitter.instruction("bl __rt_array_push_int"); // append callable descriptor bits as a plain scalar slot - } - _ => {} - } - - // -- update stored array pointer (may have changed due to COW splitting or reallocation) -- - emit_store_mutating_arg(emitter, ctx, &args[0]); - - Some(PhpType::Void) -} - -/// Emits `array_push` codegen for the x86_64 Linux target. -/// Saves the array pointer before evaluating the value to push, restores it afterward, -/// then calls the appropriate runtime helper based on the value type (int, float, string, -/// refcounted). Handles COW splitting and mixed-type conversion when needed, then -/// publishes the possibly-reallocated array pointer back through the mutating argument slot. -fn emit_array_push_linux_x86_64( - args: &[Expr], - arr_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - abi::emit_push_reg(emitter, "rax"); // preserve the indexed-array pointer while evaluating the appended value - let elem_ty = indexed_array_elem_type(arr_ty); - let source_owned = expr_result_heap_ownership(&args[1]) == HeapOwnership::Owned; - let mut val_ty = emit_expr(&args[1], emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut val_ty); - let effective_elem_ty = effective_indexed_push_type(&elem_ty, &val_ty, ctx); - let converted_to_mixed = - matches!(effective_elem_ty, PhpType::Mixed) && !matches!(elem_ty, PhpType::Mixed); - let mut boxed_value_for_mixed = false; - if matches!(effective_elem_ty, PhpType::Mixed) - && !matches!(val_ty, PhpType::Mixed | PhpType::Union(_)) - { - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, &args[1], &val_ty, - ); - val_ty = PhpType::Mixed; - boxed_value_for_mixed = true; - } else if matches!(effective_elem_ty, PhpType::Mixed) && matches!(val_ty, PhpType::Union(_)) { - val_ty = PhpType::Mixed; - } - let release_after_refcounted_push = boxed_value_for_mixed - || boxed_iterable - || (source_owned - && matches!( - val_ty, - PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Object(_) - )); - abi::emit_pop_reg(emitter, "r11"); // restore the indexed-array pointer after evaluating the appended value - if elem_ty != effective_elem_ty { - update_array_push_arg_type(&args[0], &effective_elem_ty, ctx); - } - if converted_to_mixed { - abi::emit_push_reg(emitter, "rax"); // preserve the boxed pushed value across mixed-array conversion - emitter.instruction("mov rdi, r11"); // pass the current indexed-array pointer to the mixed conversion helper - abi::emit_load_int_immediate( - emitter, - "rsi", - crate::codegen::runtime_value_tag(&elem_ty) as i64, - ); - abi::emit_call_label(emitter, "__rt_array_to_mixed"); // box existing typed slots before array_push stores a heterogeneous value - emitter.instruction("mov r11, rax"); // keep the converted indexed-array pointer as the push receiver - abi::emit_pop_reg(emitter, "rax"); // restore the boxed pushed value after conversion - } - match &val_ty { - PhpType::Int | PhpType::Bool => { - emitter.instruction("mov rsi, rax"); // place the appended scalar payload in the x86_64 runtime value register - emitter.instruction("mov rdi, r11"); // place the indexed-array pointer in the x86_64 runtime receiver register - abi::emit_call_label(emitter, "__rt_array_push_int"); // append the scalar payload and return the possibly-grown indexed-array pointer - } - PhpType::Float => { - emitter.instruction("movq rsi, xmm0"); // move the floating-point payload bits into the scalar append register - emitter.instruction("mov rdi, r11"); // place the indexed-array pointer in the x86_64 runtime receiver register - abi::emit_call_label(emitter, "__rt_array_push_int"); // append the floating-point payload bits as an 8-byte scalar slot - } - PhpType::Str => { - emitter.instruction("mov rsi, rax"); // place the appended string pointer in the x86_64 runtime payload register - emitter.instruction("mov rdi, r11"); // place the indexed-array pointer in the x86_64 runtime receiver register - abi::emit_call_label(emitter, "__rt_array_push_str"); // persist and append the string payload, returning the possibly-grown indexed-array pointer - } - PhpType::Callable => { - emitter.instruction("mov rsi, rax"); // place the callable descriptor pointer bits in the x86_64 scalar append register - emitter.instruction("mov rdi, r11"); // place the indexed-array pointer in the x86_64 runtime receiver register - abi::emit_call_label(emitter, "__rt_array_push_int"); // append the callable descriptor pointer bits as a plain scalar slot - } - PhpType::Mixed | PhpType::Union(_) | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { - if release_after_refcounted_push { - abi::emit_push_reg(emitter, "rax"); - } - emitter.instruction("mov rsi, rax"); // place the retained refcounted payload pointer in the x86_64 runtime child register - emitter.instruction("mov rdi, r11"); // place the indexed-array pointer in the x86_64 runtime receiver register - abi::emit_call_label(emitter, "__rt_array_push_refcounted"); // append the retained heap payload and stamp the indexed-array value_type metadata - if release_after_refcounted_push { - crate::codegen::emit_release_pushed_refcounted_temp_after_array_push(emitter, &val_ty); - } - } - _ => {} - } - - emit_store_mutating_arg(emitter, ctx, &args[0]); // publish the possibly-grown indexed-array pointer back through the mutating argument slot -} - -/// Returns the element type of an indexed array type. -/// For `PhpType::Array(elem_ty)` returns the unwrapped element type; -/// for all other array types (including `PhpType::AssocArray`), defaults to `PhpType::Int`. -fn indexed_array_elem_type(arr_ty: &PhpType) -> PhpType { - match arr_ty { - PhpType::Array(elem_ty) => *elem_ty.clone(), - _ => PhpType::Int, - } -} - -/// Determines the effective element type after a push operation given the existing array element -/// type and the type of the value being pushed. -/// -/// Rules: -/// - If the existing type is `Never`, returns the value type (or `Mixed` for `Union`). -/// - If the value is `Never`, returns the existing type unchanged. -/// - If either type is `Mixed` or `Union`, returns `Mixed`. -/// - If both types match, returns that type. -/// - For `Object` vs `Object`, returns the common object type via `ctx.common_object_type`, -/// falling back to `Mixed` if unrelated. -/// - Otherwise returns `Mixed`. -fn effective_indexed_push_type(existing: &PhpType, value: &PhpType, ctx: &Context) -> PhpType { - if matches!(existing, PhpType::Never) { - return if matches!(value, PhpType::Union(_)) { - PhpType::Mixed - } else { - value.clone() - }; - } - if matches!(value, PhpType::Never) { - return existing.clone(); - } - if matches!(existing, PhpType::Mixed) || matches!(value, PhpType::Mixed | PhpType::Union(_)) { - PhpType::Mixed - } else if existing == value { - existing.clone() - } else if let (PhpType::Object(left), PhpType::Object(right)) = (existing, value) { - ctx.common_object_type(left, right).unwrap_or(PhpType::Mixed) - } else { - PhpType::Mixed - } -} - -/// Updates the type annotation for a variable that received a pushed value, wrapping the -/// element type back into `PhpType::Array` and updating the variable's type and ownership -/// in the context. Only applies to simple variable expressions; no-op for other forms. -fn update_array_push_arg_type(arg: &Expr, elem_ty: &PhpType, ctx: &mut Context) { - if let crate::parser::ast::ExprKind::Variable(name) = &arg.kind { - let updated_ty = PhpType::Array(Box::new(elem_ty.clone())); - ctx.update_var_type_and_ownership( - name, - updated_ty.clone(), - crate::codegen::context::HeapOwnership::local_owner_for_type(&updated_ty), - ); - } -} diff --git a/src/codegen/builtins/arrays/array_rand.rs b/src/codegen/builtins/arrays/array_rand.rs deleted file mode 100644 index 9164582527..0000000000 --- a/src/codegen/builtins/arrays/array_rand.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Purpose: -//! Emits PHP `array_rand` builtin calls for array values. -//! Materializes arguments and delegates payload work to the matching runtime helper or inline lowering. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array element type and ownership assumptions must match the type checker and runtime layout. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `array_rand` builtin. -/// -/// # Arguments -/// - `args[0]`: the input array expression; evaluated and its pointer placed in the -/// appropriate argument register (`rdi` on x86_64, `x0` on ARM64). -/// - `emitter`: used to emit instructions and comments. -/// - `ctx`: carries variable layout and codegen state. -/// - `data`: data section for relocations and constants. -/// -/// # Returns -/// `Some(PhpType::Int)` — the selected random array key is returned in `x0`/`rax` -/// depending on target. -/// -/// # Codegen behavior -/// - x86_64: moves the array pointer from `rax` to `rdi`, calls `__rt_array_rand`. -/// - ARM64: calls `__rt_array_rand` directly (array pointer already in `x0`). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_rand()"); - emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the source indexed-array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_array_rand"); // choose a random scalar indexed-array key through the x86_64 runtime helper - return Some(PhpType::Int); - } - - // -- call runtime to pick a random index from array -- - emitter.instruction("bl __rt_array_rand"); // call runtime: random index → x0=random key - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/arrays/array_reduce.rs b/src/codegen/builtins/arrays/array_reduce.rs deleted file mode 100644 index 0d1456e811..0000000000 --- a/src/codegen/builtins/arrays/array_reduce.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Purpose: -//! Emits PHP `array_reduce` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::callback_env; -use super::runtime_callable_array_callback; -use super::runtime_string_callback; - -/// Emits the `array_reduce($input, $callback, $initial)` builtin call. -/// -/// `args[0]` (array) is evaluated first, `args[1]` (callback) second, and -/// `args[2]` (initial) last — preserving PHP source evaluation order. -/// -/// For non-capturing callbacks: pushes the array pointer to the temporary stack, -/// materializes the callback address, evaluates the initial value, then calls -/// `__rt_array_reduce` with registers set to [callback, array, initial, 0]. -/// -/// For capturing callbacks: recovers the array pointer, builds a capture -/// environment via `callback_env::emit_captured_callback_env` (which rewrites -/// the callback to a wrapper), then calls `__rt_array_reduce` with the wrapped -/// callback and environment pointer. Branch-shaped captured callable expressions -/// use descriptor-backed environments so runtime-selected receivers/captures are -/// preserved through the uniform invoker. Releases the temporary stack after the call. -/// -/// # Returns -/// `Some(PhpType::Int)` — `array_reduce` always returns an integer in this compiler. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_reduce()"); - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let initial_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - let env_arg_reg = abi::int_arg_reg_name(emitter.target, 3); - let source_elem_ty = match crate::codegen::functions::infer_contextual_type(&args[0], ctx) { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Int, - }; - - // -- evaluate the array argument, then the callback argument -- - emit_expr(&args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, result_reg); // push the source array pointer onto the temporary stack - - if runtime_string_callback::emit_after_saved_array( - &args[1], - None, - vec![PhpType::Int, source_elem_ty.clone()], - PhpType::Int, - array_arg_reg, - emitter, - ctx, - data, - |wrapper, emitter, ctx, data| { - // -- evaluate initial value (third arg) -- - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", initial_arg_reg, result_reg)); // place the initial accumulator in the third runtime argument register - - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper with a runtime string descriptor - }, - ) { - return Some(PhpType::Int); - } - - if let Some(wrapper) = callback_env::emit_callable_array_descriptor_env_after_saved_array( - &args[1], - array_arg_reg, - call_reg, - vec![PhpType::Int, source_elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - data, - ) { - // -- evaluate initial value (third arg) -- - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", initial_arg_reg, result_reg)); // place the initial accumulator in the third runtime argument register - - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper with a callable-array descriptor environment - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Int); - } - - if runtime_callable_array_callback::emit_after_saved_array( - &args[1], - array_arg_reg, - vec![PhpType::Int, source_elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - data, - |wrapper, emitter, ctx, data| { - // -- evaluate initial value (third arg) -- - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", initial_arg_reg, result_reg)); // place the initial accumulator in the third runtime argument register - - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper with a runtime callable-array descriptor - }, - ) { - return Some(PhpType::Int); - } - - if callback_env::expr_call_needs_descriptor_callback_env(&args[1], ctx) - && callback_env::descriptor_callback_env_supported(&args[1]) - { - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // preserve the selected callable descriptor while recovering the source array - abi::emit_pop_reg(emitter, array_arg_reg); // recover the source array pointer before building the descriptor environment - emitter.instruction(&format!("mov {}, {}", result_reg, call_reg)); // restore the selected callable descriptor as the current result - let wrapper = callback_env::emit_descriptor_callback_env_from_result( - &args[1], - array_arg_reg, - vec![PhpType::Int, source_elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - ) - .expect("descriptor callback env support checked before emitting callback"); - - // -- evaluate initial value (third arg) -- - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", initial_arg_reg, result_reg)); // place the initial accumulator in the third runtime argument register - - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper with a descriptor environment - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Int); - } - - let captures = - callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); - - if !captures.is_empty() { - abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![PhpType::Int, source_elem_ty], - emitter, - ctx, - ); - - // -- evaluate initial value (third arg) -- - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", initial_arg_reg, result_reg)); // place the initial accumulator in the third runtime argument register - - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper with a capture environment - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Int); - } - - abi::emit_push_reg(emitter, call_reg); // save the callback address across initial-value evaluation - - // -- evaluate initial value (third arg) -- - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", initial_arg_reg, result_reg)); // place the initial accumulator in the third runtime argument register - - // -- place callback and array pointer into the runtime argument registers -- - abi::emit_pop_reg(emitter, callback_arg_reg); // restore the callback function address into the first runtime argument register - abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - abi::emit_call_label(emitter, "__rt_array_reduce"); // call the callback-driven reduce runtime helper - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/arrays/array_reverse.rs b/src/codegen/builtins/arrays/array_reverse.rs deleted file mode 100644 index 94d3558848..0000000000 --- a/src/codegen/builtins/arrays/array_reverse.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Purpose: -//! Emits PHP `array_reverse` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `array_reverse` builtin. -/// -/// # Arguments -/// - `_name`: Unused name for dispatcher compatibility (builtin is identified by signature). -/// - `args[0]`: The array expression to reverse. Must be evaluated first; result is in `x0`/`rax`. -/// - `emitter`: Target-aware instruction emitter. -/// - `ctx`: Codegen context carrying variable layout and class metadata. -/// - `data`: Read-only data section for relocations and static data. -/// -/// # Returns -/// `Some(PhpType::Array(...))` matching the input array's element type, or -/// `Some(PhpType::Array(Box::new(PhpType::Int)))` if the input type is not an `Array` -/// (defaulting to `int`-indexed array on return). -/// -/// # Behavior -/// - Evaluates `args[0]` to produce the source array in `x0`/`rax`. -/// - On x86_64: moves the array pointer to `rdi` (first calling-convention arg) and calls -/// `__rt_array_reverse` or `__rt_array_reverse_refcounted` based on `arr_ty`. -/// - On ARM64: uses `bl` to call `__rt_array_reverse` or `__rt_array_reverse_refcounted`. -/// - Result array is returned in `x0`/`rax`. -/// - Preserves `ctx` state; emits a `"array_reverse()"` comment for debug traceability. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_reverse()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the source scalar indexed-array pointer into the first x86_64 runtime argument register - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_reverse_refcounted"); // reverse the refcounted indexed-array payloads through the x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_reverse"); // reverse the scalar indexed-array payloads through the x86_64 runtime helper - } - - return match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } - - // -- call runtime to create reversed copy of array -- - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_reverse_refcounted" - } else { - "bl __rt_array_reverse" - }; - emitter.instruction(runtime_call); // call runtime: reverse array → x0=new array - - match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - } -} diff --git a/src/codegen/builtins/arrays/array_search.rs b/src/codegen/builtins/arrays/array_search.rs deleted file mode 100644 index f2ab98386a..0000000000 --- a/src/codegen/builtins/arrays/array_search.rs +++ /dev/null @@ -1,334 +0,0 @@ -//! Purpose: -//! Emits PHP `array_search` builtin calls for array values. -//! Materializes arguments and delegates payload work to the matching runtime helper or inline lowering. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array element type and ownership assumptions must match the type checker and runtime layout. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_search($needle, $array)` builtin call. -/// -/// For associative arrays: performs inline insertion-order linear search, comparing each -/// entry's value against the needle. Returns the matching key as `PhpType::Mixed` — integer -/// keys use tag 0, string keys use tag 1. Not-found returns bool false (tag 3). -/// -/// For indexed arrays: calls the `__rt_array_search` runtime helper, then boxes the raw -/// integer result (found index ≥ 0 or not-found sentinel -1) into `PhpType::Mixed` so that -/// index 0 remains distinguishable from false. -/// -/// Stack layout (top to bottom) for associative path: -/// sp+0: iter_index (16 bytes) -/// sp+16: needle (16 bytes) -/// sp+32: hash_table_ptr (16 bytes) -/// -/// Arguments: -/// * `args[0]` — the needle to search for -/// * `args[1]` — the array to search in (evaluated first to determine array type) -/// -/// Returns: `Some(PhpType::Mixed)` always (PHP array_search returns int|false, which maps to Mixed). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_search()"); - - // -- evaluate array (second arg) first to get its type -- - let arr_ty = emit_expr(&args[1], emitter, ctx, data); - - if let PhpType::AssocArray { value, .. } = &arr_ty { - let val_ty = *value.clone(); - // -- save hash table pointer, evaluate needle -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the associative-array hash-table pointer while evaluating the searched needle - - let needle_ty = emit_expr(&args[0], emitter, ctx, data); - - let found_label = ctx.next_label("asearch_assoc_found"); - let end_label = ctx.next_label("asearch_assoc_end"); - let loop_label = ctx.next_label("asearch_assoc_loop"); - let skip_label = ctx.next_label("asearch_assoc_skip"); - let mixed_mismatch_label = ctx.next_label("asearch_assoc_mixed_mismatch"); - - match &val_ty { - PhpType::Str => { - // -- needle is a string in x1/x2, save it -- - abi::emit_push_reg_pair(emitter, abi::string_result_regs(emitter).0, abi::string_result_regs(emitter).1); // preserve the string needle across the associative-array iteration loop - } - PhpType::Mixed if matches!(needle_ty, PhpType::Str) => { - abi::emit_push_reg_pair(emitter, abi::string_result_regs(emitter).0, abi::string_result_regs(emitter).1); // preserve the string needle across mixed associative-array iteration - } - PhpType::Mixed if matches!(needle_ty, PhpType::Float) => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmov x0, d0"); // move the float needle bits into an integer register for mixed-entry comparison - } - Arch::X86_64 => { - emitter.instruction("movq rax, xmm0"); // move the float needle bits into the integer result register for mixed-entry comparison - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the float needle bits across the associative-array iteration loop - } - _ => { - // -- needle is an integer/bool in x0, save it -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the scalar needle across the associative-array iteration loop - } - } - - // -- push iteration index onto stack -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str xzr, [sp, #-16]!"); // push iter_cursor = 0 (start from hash header head) - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one temporary stack slot for the associative-array iterator cursor - emitter.instruction("mov QWORD PTR [rsp], 0"); // initialize the associative-array iterator cursor to the hash-header head sentinel - } - } - - // Stack layout (top to bottom): - // sp+0: iter_index (16 bytes) - // sp+16: needle (16 bytes) - // sp+32: hash_table_ptr (16 bytes) - - emitter.label(&loop_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #32]"); // load the associative-array hash table pointer for the next insertion-order iteration step - emitter.instruction("ldr x1, [sp]"); // load the current associative-array iterator cursor - emitter.instruction("bl __rt_hash_iter_next"); // advance one associative-array insertion-order entry and return its key plus payload - emitter.instruction("cmn x0, #1"); // has the associative-array iterator reached the done sentinel? - emitter.instruction(&format!("b.eq {}", end_label)); // stop searching once associative-array iteration has completed - emitter.instruction("str x0, [sp]"); // save the updated associative-array iterator cursor for the next loop iteration - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the current associative-array key so it can be returned on match - - match &val_ty { - PhpType::Str => { - emitter.instruction("mov x1, x3"); // move the associative-array entry string pointer into the first string-compare register - emitter.instruction("mov x2, x4"); // move the associative-array entry string length into the paired string-compare register - emitter.instruction("ldp x3, x4, [sp, #32]"); // reload the saved string needle from the stack frame under the preserved key pair - emitter.instruction("bl __rt_str_eq"); // compare the associative-array entry string value against the searched needle - emitter.instruction(&format!("cbnz x0, {}", found_label)); // stop once the searched string matches the current associative-array value - } - PhpType::Mixed => { - let expected_tag = crate::codegen::runtime_value_tag(&needle_ty); - emitter.instruction(&format!("mov x6, #{}", expected_tag)); // materialize the expected mixed-entry runtime tag for the searched needle - emitter.instruction("cmp x5, x6"); // does the current associative-array mixed entry match the searched needle kind? - emitter.instruction(&format!("b.ne {}", mixed_mismatch_label)); // skip associative-array entries whose mixed kind differs from the needle - match &needle_ty { - PhpType::Str => { - emitter.instruction("mov x1, x3"); // move the associative-array mixed entry string pointer into the first string-compare register - emitter.instruction("mov x2, x4"); // move the associative-array mixed entry string length into the paired string-compare register - emitter.instruction("ldp x3, x4, [sp, #32]"); // reload the saved string needle under the preserved associative-array key pair - emitter.instruction("bl __rt_str_eq"); // compare the associative-array mixed string entry against the searched needle - emitter.instruction(&format!("cbnz x0, {}", found_label)); // stop once the associative-array mixed string value matches the needle - } - PhpType::Void => { - emitter.instruction(&format!("b {}", found_label)); // null needles match associative-array entries tagged null - } - _ => { - emitter.instruction("ldr x6, [sp, #32]"); // reload the saved scalar mixed needle payload under the preserved associative-array key pair - emitter.instruction("cmp x3, x6"); // compare the associative-array mixed entry payload against the searched scalar needle - emitter.instruction(&format!("b.eq {}", found_label)); // stop once the associative-array mixed scalar payload matches the needle - } - } - emitter.label(&mixed_mismatch_label); - } - _ => { - emitter.instruction("ldr x5, [sp, #32]"); // reload the saved scalar needle payload under the preserved associative-array key pair - emitter.instruction("cmp x3, x5"); // compare the associative-array entry payload against the searched scalar needle - emitter.instruction(&format!("b.eq {}", found_label)); // stop once the associative-array entry payload matches the searched scalar needle - } - } - emitter.instruction("add sp, sp, #16"); // drop the preserved associative-array key after a non-matching iteration step - emitter.instruction(&format!("b {}", loop_label)); // continue scanning the remaining associative-array insertion-order entries - - emitter.label(&found_label); - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the matching normalized associative-array key - let found_string_key = ctx.next_label("asearch_assoc_found_string_key"); - let found_key_boxed = ctx.next_label("asearch_assoc_found_key_boxed"); - emitter.instruction("cmn x2, #1"); // check whether the matching key is an integer key - emitter.instruction(&format!("b.ne {}", found_string_key)); // string keys need string-tagged mixed boxing - emitter.instruction("mov x0, #0"); // runtime tag 0 = integer key - emitter.instruction("mov x2, xzr"); // integer mixed payloads do not use the high word - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the matching integer key so false remains distinguishable from 0 - emitter.instruction(&format!("b {}", found_key_boxed)); // skip the string-key boxing path - emitter.label(&found_string_key); - emitter.instruction("mov x0, #1"); // runtime tag 1 = string key - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist and box the matching string key - emitter.label(&found_key_boxed); - emitter.instruction(&format!("b {}", skip_label)); // jump to the common associative-array cleanup once a match is found - - emitter.label(&end_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for an associative array_search() miss - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false for an associative array_search() miss - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible not-found semantics - } - Arch::X86_64 => { - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // load the associative-array hash table pointer for the next insertion-order iteration step - emitter.instruction("mov rsi, QWORD PTR [rsp]"); // load the current associative-array iterator cursor - emitter.instruction("call __rt_hash_iter_next"); // advance one associative-array insertion-order entry and return its key plus payload - emitter.instruction("cmp rax, -1"); // has the associative-array iterator reached the done sentinel? - emitter.instruction(&format!("je {}", end_label)); // stop searching once associative-array iteration has completed - emitter.instruction("mov QWORD PTR [rsp], rax"); // save the updated associative-array iterator cursor for the next loop iteration - abi::emit_push_reg_pair(emitter, "rdi", "rdx"); // preserve the current associative-array key so it can be returned on match - - match &val_ty { - PhpType::Str => { - emitter.instruction("mov rdi, rcx"); // move the associative-array entry string pointer into the first string-compare register - emitter.instruction("mov rsi, r8"); // move the associative-array entry string length into the paired string-compare register - emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // reload the saved string needle pointer under the preserved associative-array key pair - emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // reload the saved string needle length under the preserved associative-array key pair - emitter.instruction("call __rt_str_eq"); // compare the associative-array entry string value against the searched needle - emitter.instruction("test rax, rax"); // did the associative-array string value match the searched needle? - emitter.instruction(&format!("jne {}", found_label)); // stop once the searched string matches the current associative-array value - } - PhpType::Mixed => { - let expected_tag = crate::codegen::runtime_value_tag(&needle_ty) as i64; - abi::emit_load_int_immediate(emitter, "r10", expected_tag); // materialize the expected mixed-entry runtime tag for the searched needle - emitter.instruction("cmp r9, r10"); // does the current associative-array mixed entry match the searched needle kind? - emitter.instruction(&format!("jne {}", mixed_mismatch_label)); // skip associative-array entries whose mixed kind differs from the needle - match &needle_ty { - PhpType::Str => { - emitter.instruction("mov rdi, rcx"); // move the associative-array mixed entry string pointer into the first string-compare register - emitter.instruction("mov rsi, r8"); // move the associative-array mixed entry string length into the paired string-compare register - emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // reload the saved string needle pointer under the preserved associative-array key pair - emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // reload the saved string needle length under the preserved associative-array key pair - emitter.instruction("call __rt_str_eq"); // compare the associative-array mixed string entry against the searched needle - emitter.instruction("test rax, rax"); // did the associative-array mixed string entry match the searched needle? - emitter.instruction(&format!("jne {}", found_label)); // stop once the associative-array mixed string value matches the needle - } - PhpType::Void => { - emitter.instruction(&format!("jmp {}", found_label)); // null needles match associative-array entries tagged null - } - _ => { - emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // reload the saved scalar mixed needle payload under the preserved associative-array key pair - emitter.instruction("cmp rcx, r10"); // compare the associative-array mixed entry payload against the searched scalar needle - emitter.instruction(&format!("je {}", found_label)); // stop once the associative-array mixed scalar payload matches the needle - } - } - emitter.label(&mixed_mismatch_label); - } - _ => { - emitter.instruction("mov r10, QWORD PTR [rsp + 32]"); // reload the saved scalar needle payload under the preserved associative-array key pair - emitter.instruction("cmp rcx, r10"); // compare the associative-array entry payload against the searched scalar needle - emitter.instruction(&format!("je {}", found_label)); // stop once the associative-array entry payload matches the searched scalar needle - } - } - emitter.instruction("add rsp, 16"); // drop the preserved associative-array key after a non-matching iteration step - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning the remaining associative-array insertion-order entries - - emitter.label(&found_label); - abi::emit_pop_reg_pair(emitter, "rdi", "rdx"); // restore the matching normalized associative-array key - let found_string_key = ctx.next_label("asearch_assoc_found_string_key"); - let found_key_boxed = ctx.next_label("asearch_assoc_found_key_boxed"); - emitter.instruction("cmp rdx, -1"); // check whether the matching key is an integer key - emitter.instruction(&format!("jne {}", found_string_key)); // string keys need string-tagged mixed boxing - emitter.instruction("xor esi, esi"); // integer mixed payloads do not use the high word - emitter.instruction("mov eax, 0"); // runtime tag 0 = integer key - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the matching integer key so false remains distinguishable from 0 - emitter.instruction(&format!("jmp {}", found_key_boxed)); // skip the string-key boxing path - emitter.label(&found_string_key); - emitter.instruction("mov rsi, rdx"); // move the string key length into the mixed helper high-word register - emitter.instruction("mov eax, 1"); // runtime tag 1 = string key - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist and box the matching string key - emitter.label(&found_key_boxed); - emitter.instruction(&format!("jmp {}", skip_label)); // jump to the common associative-array cleanup once a match is found - - emitter.label(&end_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for an associative array_search() miss - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false for an associative array_search() miss - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible not-found semantics - } - } - - emitter.label(&skip_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("add sp, sp, #48"); // drop the remaining associative-array iterator cursor, needle, and hash-table stack slots - } - Arch::X86_64 => { - emitter.instruction("add rsp, 48"); // drop the remaining associative-array iterator cursor, needle, and hash-table stack slots - } - } - - return Some(PhpType::Mixed); - } - - // -- indexed array: use runtime for linear search -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the indexed-array pointer while evaluating the searched needle - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the indexed-array needle into the second helper argument register - abi::emit_pop_reg(emitter, "x0"); // restore the indexed-array pointer into the first helper argument register - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the indexed-array needle into the second SysV helper argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the indexed-array pointer into the first SysV helper argument register - } - } - abi::emit_call_label(emitter, "__rt_array_search"); // search the indexed-array values and return the first matching index or -1 - box_index_search_result(emitter, ctx); - - Some(PhpType::Mixed) -} - -/// Boxes a raw integer index result from `__rt_array_search` into a `PhpType::Mixed` value. -/// -/// Takes a raw result: index ≥ 0 means found (box as integer tag 0), -1 means not-found -/// (box as bool false tag 3). Uses conditional branches and `__rt_mixed_from_value` to produce -/// the correct boxed Mixed representation. The distinction between index 0 and false is -/// preserved by the boxing scheme. -/// -/// AArch64: result in x0; X86_64: result in rax. -fn box_index_search_result(emitter: &mut Emitter, ctx: &mut Context) { - let found_label = ctx.next_label("asearch_index_found"); - let end_label = ctx.next_label("asearch_index_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // distinguish a found index from the indexed array_search() not-found sentinel - emitter.instruction(&format!("b.ge {}", found_label)); // box a found index as an integer result - emitter.instruction("mov x1, #0"); // false payload = 0 for an indexed array_search() miss - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false for an indexed array_search() miss - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false so index 0 remains distinguishable from not found - emitter.instruction(&format!("b {}", end_label)); // skip the integer boxing path after a miss - emitter.label(&found_label); - emitter.instruction("mov x1, x0"); // move the found index into the mixed helper payload register - emitter.instruction("mov x2, #0"); // integer mixed payloads do not use a high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = int for found array_search() indexes - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the found integer index as mixed - emitter.label(&end_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // distinguish a found index from the indexed array_search() not-found sentinel - emitter.instruction(&format!("jge {}", found_label)); // box a found index as an integer result - emitter.instruction("xor edi, edi"); // false payload = 0 for an indexed array_search() miss - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false for an indexed array_search() miss - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false so index 0 remains distinguishable from not found - emitter.instruction(&format!("jmp {}", end_label)); // skip the integer boxing path after a miss - emitter.label(&found_label); - emitter.instruction("mov rdi, rax"); // move the found index into the mixed helper payload register - emitter.instruction("xor esi, esi"); // integer mixed payloads do not use a high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = int for found array_search() indexes - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the found integer index as mixed - emitter.label(&end_label); - } - } -} diff --git a/src/codegen/builtins/arrays/array_shift.rs b/src/codegen/builtins/arrays/array_shift.rs deleted file mode 100644 index 01e4da66f0..0000000000 --- a/src/codegen/builtins/arrays/array_shift.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Purpose: -//! Emits PHP `array_shift` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `array_shift()` builtin, which removes and returns the first element. -/// -/// ## Inputs -/// - `args[0]`: the array to shift from (passed by reference, mutated in place) -/// - `emitter`: target-aware assembly emitter -/// - `ctx`: codegen context (carries variable layout, caller storage for ref-like args) -/// - `data`: data section for literals and runtime metadata -/// -/// ## Outputs -/// - Returns `Option`: the element type that was removed (`Int` if the array -/// type is non-array or unknown, `inner` element type if wrapped in `PhpType::Array`) -/// -/// ## Side effects & invariants -/// - Uses COW (copy-on-write): calls `ensure_unique_arg` to ensure the array is uniquely -/// owned before mutation to avoid modifying shared storage. -/// - Calls `store_mutating_arg` to write the replacement array pointer back to the -/// caller's variable slot after the runtime helper mutates the array in place. -/// - On ARM64: uses `bl __rt_array_shift`; on x86_64: uses `mov rdi, rax` then -/// `bl __rt_array_shift` to pass the array pointer via the first integer argument register. -/// - Preserves source evaluation order; argument side effects occur before the runtime call. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_shift()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let elem_ty = match &arr_ty { - PhpType::Array(inner) => (**inner).clone(), - _ => PhpType::Int, - }; - let tagged_int_result = - crate::codegen::sentinels::null_repr_is_tagged() && matches!(elem_ty, PhpType::Int); - if emitter.target.arch == Arch::X86_64 { - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - if tagged_int_result { - // distinguish the empty-array case by length before the helper consumes the - // pointer, so the result can carry a real null tag instead of the sentinel - let empty_label = ctx.next_label("array_shift_empty"); - let end_label = ctx.next_label("array_shift_end"); - emitter.instruction("mov r10, QWORD PTR [rax]"); // load the indexed-array length before deciding whether the shift is empty - emitter.instruction("test r10, r10"); // check whether the indexed array currently stores any elements - emitter.instruction(&format!("jz {}", empty_label)); // produce a tagged null when array_shift runs on an empty indexed array - emitter.instruction("mov rdi, rax"); // move the unique indexed-array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_array_shift"); // remove and return the first scalar indexed-array element through the x86_64 runtime helper - crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - emitter.instruction(&format!("jmp {}", end_label)); // skip the empty-array tagged-null path after the successful shift - emitter.label(&empty_label); - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - emitter.label(&end_label); - return Some(PhpType::TaggedScalar); - } - emitter.instruction("mov rdi, rax"); // move the unique indexed-array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_array_shift"); // remove and return the first scalar indexed-array element through the x86_64 runtime helper - return Some(elem_ty); - } - - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - if tagged_int_result { - // distinguish the empty-array case by length before the helper consumes the - // pointer, so the result can carry a real null tag instead of the sentinel - let empty_label = ctx.next_label("array_shift_empty"); - let end_label = ctx.next_label("array_shift_end"); - emitter.instruction("ldr x9, [x0]"); // load the indexed-array length before deciding whether the shift is empty - emitter.instruction(&format!("cbz x9, {}", empty_label)); // produce a tagged null when array_shift runs on an empty indexed array - emitter.instruction("bl __rt_array_shift"); // call runtime: shift first element -> x0=removed element - crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - emitter.instruction(&format!("b {}", end_label)); // skip the empty-array tagged-null path after the successful shift - emitter.label(&empty_label); - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - emitter.label(&end_label); - return Some(PhpType::TaggedScalar); - } - // -- call runtime to remove and return first element -- - emitter.instruction("bl __rt_array_shift"); // call runtime: shift first element -> x0=removed element - - Some(elem_ty) -} diff --git a/src/codegen/builtins/arrays/array_slice.rs b/src/codegen/builtins/arrays/array_slice.rs deleted file mode 100644 index dab3283c0a..0000000000 --- a/src/codegen/builtins/arrays/array_slice.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Purpose: -//! Emits PHP `array_slice` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_slice($array, $offset, $length)` builtin call. -/// -/// Evaluates arguments in source order, materializes them into ABI register order, -/// and calls `__rt_array_slice` (scalar) or `__rt_array_slice_refcounted` (refcounted -/// elements) depending on the source array's element type. On x86_64 uses register- -/// based argument passing; on ARM64 uses stack-based argument passing with x0–x2. -/// A missing `$length` is signaled by passing -1 to request "until end of array". -/// -/// # Arguments -/// * `args[0]` — source array expression -/// * `args[1]` — byte offset into the array -/// * `args[2]` — optional slice length; absent means rest of array -/// -/// # Returns -/// `PhpType::Array` preserving the inner element type from the source array, or -/// `PhpType::Array(Int)` when the source type is non-array (treated as integer-indexed). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_slice()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - if emitter.target.arch == Arch::X86_64 { - abi::emit_push_reg(emitter, "rax"); // preserve the source indexed-array pointer while evaluating the slice offset - let offset_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &offset_ty); // unbox a Mixed/Union slice offset into a raw integer - if args.len() > 2 { - abi::emit_push_reg(emitter, "rax"); // preserve the requested slice offset while evaluating the slice length - let length_ty = emit_expr(&args[2], emitter, ctx, data); - coerce_to_int(emitter, &length_ty); // unbox a Mixed/Union slice length into a raw integer - emitter.instruction("mov rdx, rax"); // move the requested slice length into the third x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rsi"); // restore the requested slice offset into the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the source indexed-array pointer into the first x86_64 runtime argument register - } else { - emitter.instruction("mov rsi, rax"); // move the requested slice offset into the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the source indexed-array pointer into the first x86_64 runtime argument register - emitter.instruction("mov rdx, -1"); // use -1 as the x86_64 runtime sentinel for slicing until the end of the source array - } - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_slice_refcounted"); // extract the refcounted indexed-array slice through the x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_slice"); // extract the scalar indexed-array slice through the x86_64 runtime helper - } - - return match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } - - // -- save array pointer, evaluate offset -- - emitter.instruction("str x0, [sp, #-16]!"); // push array pointer onto stack - let offset_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &offset_ty); // unbox a Mixed/Union slice offset into a raw integer - if args.len() > 2 { - // -- save offset, evaluate length -- - emitter.instruction("str x0, [sp, #-16]!"); // push offset onto stack - let length_ty = emit_expr(&args[2], emitter, ctx, data); - coerce_to_int(emitter, &length_ty); // unbox a Mixed/Union slice length into a raw integer - // -- set up three-arg call: array, offset, length -- - emitter.instruction("mov x2, x0"); // move length to x2 (third arg) - emitter.instruction("ldr x1, [sp], #16"); // pop offset into x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer into x0 (first arg) - } else { - // -- set up two-arg call: array, offset (length = rest of array) -- - emitter.instruction("mov x1, x0"); // move offset to x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer into x0 (first arg) - emitter.instruction("mov x2, #-1"); // length = -1 signals "until end of array" - } - // -- call runtime to extract slice -- - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_slice_refcounted" - } else { - "bl __rt_array_slice" - }; - emitter.instruction(runtime_call); // call runtime: slice array → x0=new array - - match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - } -} diff --git a/src/codegen/builtins/arrays/array_splice.rs b/src/codegen/builtins/arrays/array_splice.rs deleted file mode 100644 index 7073ba29a3..0000000000 --- a/src/codegen/builtins/arrays/array_splice.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Purpose: -//! Emits PHP `array_splice` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_splice($array, $offset, $length, $replacement)` builtin call. -/// -/// Removes the portion of `$array` starting at `$offset` (negative = from end) and -/// optionally replaces it with `$replacement`. The array is mutated in place (ref-like -/// COW semantics). Returns the removed elements as a new indexed array. -/// -/// ## COW semantics -/// - `emit_ensure_unique_arg` guarantees `$array` is uniquely held before mutation. -/// - `emit_store_mutating_arg` writes the mutated array pointer back to the caller's -/// storage slot so the caller sees the change. -/// -/// ## Argument order -/// - Args are evaluated in source order; temporaries are saved on the stack so the -/// array pointer is preserved across offset/length/replacement evaluation. -/// - On x86_64: arguments arrive in `rdi`, `rsi`, `rdx`; on ARM64: `x0`, `x1`, `x2`. -/// - `-1` for `$length` signals "remove until end" (handled by the runtime helper). -/// -/// ## Runtime helpers -/// - `__rt_array_splice` for non-refcounted (scalar) arrays. -/// - `__rt_array_splice_refcounted` for refcounted arrays (inner element type is refcounted). -/// -/// ## Return type -/// Returns `PhpType::Array` wrapping the inner type of `$array` (preserves element type -/// for non-refcounted case; `PhpType::Int` fallback for the removed-elements return -/// when the original type is unknown, matching PHP's `array_splice` returning `array`). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_splice()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - if emitter.target.arch == Arch::X86_64 { - abi::emit_push_reg(emitter, "rax"); // preserve the unique indexed-array pointer while evaluating the splice offset - let offset_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &offset_ty); // unbox a Mixed/Union splice offset into a raw integer - if args.len() > 2 { - abi::emit_push_reg(emitter, "rax"); // preserve the requested splice offset while evaluating the removal length - let length_ty = emit_expr(&args[2], emitter, ctx, data); - coerce_to_int(emitter, &length_ty); // unbox a Mixed/Union removal length into a raw integer - emitter.instruction("mov rdx, rax"); // move the removal length into the third x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rsi"); // restore the splice offset into the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the unique indexed-array pointer into the first x86_64 runtime argument register - } else { - emitter.instruction("mov rsi, rax"); // move the splice offset into the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the unique indexed-array pointer into the first x86_64 runtime argument register - emitter.instruction("mov rdx, -1"); // use -1 as the x86_64 runtime sentinel for removing until the end of the source array - } - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_splice_refcounted"); // remove the requested refcounted indexed-array slice through the x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_splice"); // remove the requested scalar indexed-array slice through the x86_64 runtime helper - } - - return match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } - - // -- save array pointer, evaluate offset -- - emitter.instruction("str x0, [sp, #-16]!"); // push array pointer onto stack - let offset_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &offset_ty); // unbox a Mixed/Union splice offset into a raw integer - if args.len() > 2 { - // -- save offset, evaluate length -- - emitter.instruction("str x0, [sp, #-16]!"); // push offset onto stack - let length_ty = emit_expr(&args[2], emitter, ctx, data); - coerce_to_int(emitter, &length_ty); // unbox a Mixed/Union removal length into a raw integer - // -- set up three-arg call: array, offset, length -- - emitter.instruction("mov x2, x0"); // move length to x2 (third arg) - emitter.instruction("ldr x1, [sp], #16"); // pop offset into x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer into x0 (first arg) - } else { - // -- set up two-arg call: array, offset (remove rest) -- - emitter.instruction("mov x1, x0"); // move offset to x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer into x0 (first arg) - emitter.instruction("mov x2, #-1"); // length = -1 signals "remove until end" - } - // -- call runtime to splice array -- - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_splice_refcounted" - } else { - "bl __rt_array_splice" - }; - emitter.instruction(runtime_call); // call runtime: splice array → x0=removed elements array - - match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - } -} diff --git a/src/codegen/builtins/arrays/array_sum.rs b/src/codegen/builtins/arrays/array_sum.rs deleted file mode 100644 index 763b3cde94..0000000000 --- a/src/codegen/builtins/arrays/array_sum.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Purpose: -//! Emits PHP `array_sum` builtin calls for array values. -//! Materializes arguments and delegates payload work to the matching runtime helper or inline lowering. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array element type and ownership assumptions must match the type checker and runtime layout. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code to compute the sum of all numeric values in a PHP `array` argument. -/// -/// ## Arguments -/// - `args[0]` — the array expression to sum; evaluated and loaded into `rax` before the call. -/// - `_name` — unused; matches the builtin dispatch signature. -/// -/// ## Codegen -/// - Evaluates `args[0]` into `rax`. -/// - **x86_64**: copies `rax` → `rdi` (first integer arg register), then calls `__rt_array_sum`. -/// - **ARM64**: directly calls `__rt_array_sum` with the value already in `x0`. -/// - Both architectures return the integer sum in `x0`/`rax` via the runtime helper. -/// -/// ## Returns -/// `Some(PhpType::Int)` — the summed integer result. Runtime helper handles empty arrays and non-integer elements per PHP semantics. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_sum()"); - emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the source scalar indexed-array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_array_sum"); // add the scalar indexed-array payloads through the x86_64 runtime helper - return Some(PhpType::Int); - } - - // -- call runtime to compute sum of all array elements -- - emitter.instruction("bl __rt_array_sum"); // call runtime: sum array elements → x0=sum - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/arrays/array_unique.rs b/src/codegen/builtins/arrays/array_unique.rs deleted file mode 100644 index 1a23cdc892..0000000000 --- a/src/codegen/builtins/arrays/array_unique.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Purpose: -//! Emits PHP `array_unique` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_unique` builtin call, removing duplicate values from an indexed array. -/// -/// Arguments: -/// - `args[0]`: the source array expression -/// -/// Runtime helpers: -/// - `__rt_array_unique` for scalar indexed arrays -/// - `__rt_array_unique_refcounted` for refcounted indexed arrays -/// -/// On x86_64: moves the source array pointer from `rax` to `rdi` before the call. -/// On ARM64: uses `bl` with the appropriate helper label. -/// -/// Returns an `Array(Int)` type (keys are renumbered sequentially as integers). -/// The `_name` parameter is unused; builtin resolution is handled by the caller. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_unique()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let uses_refcounted_runtime = - matches!(&arr_ty, PhpType::Array(inner) if inner.is_refcounted()); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the source scalar indexed-array pointer into the first x86_64 runtime argument register - if uses_refcounted_runtime { - abi::emit_call_label(emitter, "__rt_array_unique_refcounted"); // deduplicate the refcounted indexed-array payloads through the x86_64 runtime helper - } else { - abi::emit_call_label(emitter, "__rt_array_unique"); // deduplicate the scalar indexed-array payloads through the x86_64 runtime helper - } - - return match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - }; - } - - // -- call runtime to create array with duplicate values removed -- - let runtime_call = if uses_refcounted_runtime { - "bl __rt_array_unique_refcounted" - } else { - "bl __rt_array_unique" - }; - emitter.instruction(runtime_call); // call runtime: deduplicate array → x0=new array - - match arr_ty { - PhpType::Array(inner) => Some(PhpType::Array(inner)), - _ => Some(PhpType::Array(Box::new(PhpType::Int))), - } -} diff --git a/src/codegen/builtins/arrays/array_unshift.rs b/src/codegen/builtins/arrays/array_unshift.rs deleted file mode 100644 index bf22c57fb5..0000000000 --- a/src/codegen/builtins/arrays/array_unshift.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Purpose: -//! Emits PHP `array_unshift` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `array_unshift` builtin call, which prepends a value to an array in place. -/// -/// # Arguments -/// - `_name`: Unused; the builtin name is implicit. -/// - `args[0]`: The array to modify (mutating/ref-like). -/// - `args[1]`: The value to prepend. -/// -/// # Returns -/// Always `PhpType::Int` (the new array length), matching PHP's return value. -/// -/// # Codegen strategy -/// 1. Ensures the array argument is uniquely owned (COW). -/// 2. Stores the array pointer back to caller storage. -/// 3. Evaluates the prepend value while preserving the array pointer. -/// 4. Calls `__rt_array_unshift` with array pointer (x0/di) and value (x1/si) registers. -/// 5. The runtime returns the new array length in x0/di. -/// -/// # ABI notes -/// - x86_64: pushes `rax` to preserve the unique array pointer while evaluating the payload, -/// then moves array pointer to `rdi` and payload to `rsi` before the call. -/// - ARM64: pushes array pointer to the stack, evaluates the payload into x0, -/// then swaps to x0=array, x1=payload via stack load. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_unshift()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - abi::emit_push_reg(emitter, "rax"); // preserve the unique indexed-array pointer while evaluating the prepended scalar payload - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rsi, rax"); // move the prepended scalar payload into the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the unique indexed-array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_array_unshift"); // prepend the scalar payload through the x86_64 runtime helper and return the new length - return Some(PhpType::Int); - } - - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - // -- save array pointer, evaluate value to prepend -- - emitter.instruction("str x0, [sp, #-16]!"); // push array pointer onto stack - emit_expr(&args[1], emitter, ctx, data); - // -- call runtime to prepend value to array -- - emitter.instruction("mov x1, x0"); // move value to x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer into x0 (first arg) - emitter.instruction("bl __rt_array_unshift"); // call runtime: prepend value → x0=new count - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/arrays/array_values.rs b/src/codegen/builtins/arrays/array_values.rs deleted file mode 100644 index 6135eccc37..0000000000 --- a/src/codegen/builtins/arrays/array_values.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! Purpose: -//! Emits PHP `array_values` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `array_values()` builtin. -/// -/// For associative arrays, iterates the hash table in insertion order and collects -/// all values into a new indexed array. For indexed arrays, returns the input unchanged -/// (with a reference-count increment to satisfy call-semantics as an owned result). -/// -/// - Input: `args[0]` must be an array-typed expression. The PhpType of `args[0]` drives -/// the element layout of the result array. -/// - Output: Returns `PhpType::Array(Box::new(value_type))` wrapping the element type. -/// String values are persisted via `__rt_str_persist`. Refcounted values have their -/// reference counts incremented via `__rt_incref`. Mixed values are boxed if not -/// already boxed, or incref'd if already boxed. -/// - Stack layout: [iter_index(16)] [result_array(16)] [hash_ptr(16)] during iteration. -/// All temporaries are cleaned up before return. Result pointer returned in the ABI -/// integer result register. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_values()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_loaded_values(&arr_ty, emitter, ctx, data) -} - -/// Emits assembly for loaded values. -pub(crate) fn emit_loaded_values( - arr_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - if let PhpType::AssocArray { value, .. } = &arr_ty { - let val_ty = *value.clone(); - // -- associative array: iterate hash table and collect values -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the associative-array hash-table pointer while allocating the result values array - - // -- allocate new indexed array for values -- - let elem_size = match &val_ty { - PhpType::Str => 16, - _ => 8, - }; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [x0]"); // load the associative-array entry count to size the result values array exactly - emitter.instruction(&format!("mov x1, #{}", elem_size)); // choose the indexed-array element size that matches the associative-array value representation - } - Arch::X86_64 => { - emitter.instruction("mov rdi, QWORD PTR [rax]"); // load the associative-array entry count to size the result values array exactly - emitter.instruction(&format!("mov rsi, {}", elem_size)); // choose the indexed-array element size that matches the associative-array value representation - } - } - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the result values array with exact associative-array capacity - crate::codegen::expr::arrays::emit_array_value_type_stamp( - emitter, - abi::int_result_reg(emitter), - &val_ty, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the result values array pointer across associative-array iteration - - // -- push iteration index onto stack -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str xzr, [sp, #-16]!"); // push iter_cursor = 0 (start from the associative-array header head slot) - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one temporary stack slot for the associative-array iterator cursor - emitter.instruction("mov QWORD PTR [rsp], 0"); // initialize the associative-array iterator cursor to the hash-header head sentinel - } - } - - // Stack: [iter_index(16)] [result_array(16)] [hash_ptr(16)] - - let loop_label = ctx.next_label("avals_assoc_loop"); - let end_label = ctx.next_label("avals_assoc_end"); - emitter.label(&loop_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #32]"); // load the associative-array hash-table pointer for the next insertion-order iteration step - emitter.instruction("ldr x1, [sp]"); // load the current associative-array iterator cursor - emitter.instruction("bl __rt_hash_iter_next"); // advance one associative-array insertion-order entry and return its key plus payload - emitter.instruction("cmn x0, #1"); // has associative-array iteration reached the done sentinel? - emitter.instruction(&format!("b.eq {}", end_label)); // stop once every associative-array value has been collected - emitter.instruction("str x0, [sp]"); // save the updated associative-array iterator cursor for the next loop step - - match &val_ty { - PhpType::Str => { - emitter.instruction("mov x1, x3"); // move the associative-array string value pointer into the string-persist input register - emitter.instruction("mov x2, x4"); // move the associative-array string value length into the paired string-persist input register - emitter.instruction("bl __rt_str_persist"); // persist the associative-array string value so the result array owns stable string storage - emitter.instruction("ldr x9, [sp, #16]"); // load the result values array pointer from the fixed stack layout - emitter.instruction("ldr x10, [x9]"); // load the current result values array length before appending one more value - emitter.instruction("lsl x11, x10, #4"); // convert the result values array length into a 16-byte string-slot offset - emitter.instruction("add x11, x9, x11"); // advance from the result values array header to the selected string slot - emitter.instruction("add x11, x11, #24"); // skip the fixed indexed-array header to land on the string payload region - emitter.instruction("str x1, [x11]"); // store the owned string pointer into the next result values slot - emitter.instruction("str x2, [x11, #8]"); // store the owned string length into the next result values slot - emitter.instruction("add x10, x10, #1"); // increment the result values array length after storing one more string - emitter.instruction("str x10, [x9]"); // persist the updated result values array length in the header - } - PhpType::Mixed => { - let reuse_box = ctx.next_label("avals_assoc_reuse_mixed"); - let store_box = ctx.next_label("avals_assoc_store_mixed"); - emitter.instruction("cmp x5, #7"); // does this associative-array entry already store a boxed mixed value? - emitter.instruction(&format!("b.eq {}", reuse_box)); // reuse existing mixed boxes instead of nesting them - super::super::super::emit_box_runtime_payload_as_mixed(emitter, "x5", "x3", "x4"); // box the borrowed associative-array payload into an owned mixed cell - emitter.instruction(&format!("b {}", store_box)); // skip the mixed-box reuse path once boxing is done - emitter.label(&reuse_box); - emitter.instruction("mov x0, x3"); // move the existing mixed box pointer into the incref helper input register - emitter.instruction("bl __rt_incref"); // retain the shared mixed box for the result values array - emitter.label(&store_box); - emitter.instruction("ldr x9, [sp, #16]"); // load the result values array pointer from the fixed stack layout - emitter.instruction("ldr x10, [x9]"); // load the current result values array length before appending one more value - emitter.instruction("add x11, x9, #24"); // point at the result values array payload region just after the fixed header - emitter.instruction("str x0, [x11, x10, lsl #3]"); // store the owned mixed box pointer into the next result values slot - emitter.instruction("add x10, x10, #1"); // increment the result values array length after storing one more mixed box - emitter.instruction("str x10, [x9]"); // persist the updated result values array length in the header - } - _ => { - if val_ty.is_refcounted() { - emitter.instruction("mov x0, x3"); // move the borrowed heap pointer into the incref helper input register before the result array stores it - emitter.instruction("bl __rt_incref"); // retain the borrowed heap value for the result values array - } - emitter.instruction("ldr x9, [sp, #16]"); // load the result values array pointer from the fixed stack layout - emitter.instruction("ldr x10, [x9]"); // load the current result values array length before appending one more value - emitter.instruction("add x11, x9, #24"); // point at the result values array payload region just after the fixed header - if val_ty.is_refcounted() { - emitter.instruction("str x0, [x11, x10, lsl #3]"); // store the retained heap pointer into the next result values slot after incref - } else { - emitter.instruction("str x3, [x11, x10, lsl #3]"); // store the associative-array value payload into the next result values slot - } - emitter.instruction("add x10, x10, #1"); // increment the result values array length after storing one more value - emitter.instruction("str x10, [x9]"); // persist the updated result values array length in the header - } - } - emitter.instruction(&format!("b {}", loop_label)); // continue collecting associative-array values until iteration completes - } - Arch::X86_64 => { - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // load the associative-array hash-table pointer for the next insertion-order iteration step - emitter.instruction("mov rsi, QWORD PTR [rsp]"); // load the current associative-array iterator cursor - emitter.instruction("call __rt_hash_iter_next"); // advance one associative-array insertion-order entry and return its key plus payload - emitter.instruction("cmp rax, -1"); // has associative-array iteration reached the done sentinel? - emitter.instruction(&format!("je {}", end_label)); // stop once every associative-array value has been collected - emitter.instruction("mov QWORD PTR [rsp], rax"); // save the updated associative-array iterator cursor for the next loop step - - match &val_ty { - PhpType::Str => { - emitter.instruction("mov rax, rcx"); // move the associative-array string value pointer into the x86_64 string-persist input register - emitter.instruction("mov rdx, r8"); // move the associative-array string value length into the paired x86_64 string-persist input register - emitter.instruction("call __rt_str_persist"); // persist the associative-array string value so the result array owns stable string storage - emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load the result values array pointer from the fixed stack layout - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the current result values array length before appending one more value - emitter.instruction("mov rcx, r11"); // copy the current result values array length before scaling it into a string-slot offset - emitter.instruction("shl rcx, 4"); // convert the result values array length into a 16-byte string-slot offset - emitter.instruction("add rcx, r10"); // advance from the result values array header to the selected string slot - emitter.instruction("add rcx, 24"); // skip the fixed indexed-array header to land on the string payload region - emitter.instruction("mov QWORD PTR [rcx], rax"); // store the owned string pointer into the next result values slot - emitter.instruction("mov QWORD PTR [rcx + 8], rdx"); // store the owned string length into the next result values slot - emitter.instruction("add r11, 1"); // increment the result values array length after storing one more string - emitter.instruction("mov QWORD PTR [r10], r11"); // persist the updated result values array length in the header - } - PhpType::Mixed => { - let reuse_box = ctx.next_label("avals_assoc_reuse_mixed"); - let store_box = ctx.next_label("avals_assoc_store_mixed"); - emitter.instruction("cmp r9, 7"); // does this associative-array entry already store a boxed mixed value? - emitter.instruction(&format!("je {}", reuse_box)); // reuse existing mixed boxes instead of nesting them - super::super::super::emit_box_runtime_payload_as_mixed(emitter, "r9", "rcx", "r8"); // box the borrowed associative-array payload into an owned mixed cell - emitter.instruction(&format!("jmp {}", store_box)); // skip the mixed-box reuse path once boxing is done - emitter.label(&reuse_box); - emitter.instruction("mov rax, rcx"); // move the existing mixed box pointer into the incref helper input register - emitter.instruction("call __rt_incref"); // retain the shared mixed box for the result values array - emitter.label(&store_box); - emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load the result values array pointer from the fixed stack layout - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the current result values array length before appending one more value - emitter.instruction("mov QWORD PTR [r10 + r11 * 8 + 24], rax"); // store the owned mixed box pointer into the next result values slot - emitter.instruction("add r11, 1"); // increment the result values array length after storing one more mixed box - emitter.instruction("mov QWORD PTR [r10], r11"); // persist the updated result values array length in the header - } - _ => { - if val_ty.is_refcounted() { - emitter.instruction("mov rax, rcx"); // move the borrowed heap pointer into the incref helper input register before the result array stores it - emitter.instruction("call __rt_incref"); // retain the borrowed heap value for the result values array - emitter.instruction("mov rcx, rax"); // keep the retained heap pointer in the payload register used for the final store - } - emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load the result values array pointer from the fixed stack layout - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the current result values array length before appending one more value - emitter.instruction("mov QWORD PTR [r10 + r11 * 8 + 24], rcx"); // store the associative-array value payload into the next result values slot - emitter.instruction("add r11, 1"); // increment the result values array length after storing one more value - emitter.instruction("mov QWORD PTR [r10], r11"); // persist the updated result values array length in the header - } - } - emitter.instruction(&format!("jmp {}", loop_label)); // continue collecting associative-array values until iteration completes - } - } - - emitter.label(&end_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("add sp, sp, #16"); // drop the associative-array iterator cursor stack slot - emitter.instruction("ldr x0, [sp], #16"); // pop the result values array pointer into the standard integer result register - emitter.instruction("add sp, sp, #16"); // drop the preserved associative-array hash-table pointer stack slot - } - Arch::X86_64 => { - emitter.instruction("add rsp, 16"); // drop the associative-array iterator cursor stack slot - emitter.instruction("mov rax, QWORD PTR [rsp]"); // move the result values array pointer into the standard integer result register - emitter.instruction("add rsp, 16"); // drop the preserved result values array pointer after loading it into the result register - emitter.instruction("add rsp, 16"); // drop the preserved associative-array hash-table pointer stack slot - } - } - - return Some(PhpType::Array(Box::new(val_ty))); - } - - // -- indexed array: array_values is a no-op, but the call still returns a new alias -- - abi::emit_incref_if_refcounted(emitter, &arr_ty); // retain the borrowed indexed array because function-call expressions are treated as owned results by callers - Some(arr_ty.clone()) -} diff --git a/src/codegen/builtins/arrays/array_walk.rs b/src/codegen/builtins/arrays/array_walk.rs deleted file mode 100644 index 6d7ee296b9..0000000000 --- a/src/codegen/builtins/arrays/array_walk.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Purpose: -//! Emits PHP `array_walk` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::callback_env; -use super::runtime_callable_array_callback; -use super::runtime_string_callback; - -/// Lowers a `array_walk($array, $callback, $userdata?)` call into target assembly. -/// Evaluates the array argument first, then the callback argument, preserving PHP source -/// evaluation order. When the callback requires captures, emits a capture-environment -/// wrapper and calls `__rt_array_walk` with the environment; otherwise passes the bare -/// callback address and a null userdata pointer. Branch-shaped captured callable -/// expressions use descriptor-backed environments so receiver/capture metadata survives -/// runtime selection. Returns `PhpType::Void` on success. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("array_walk()"); - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - - // -- evaluate the array argument (first arg) -- - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let source_elem_ty = match &arr_ty { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Int, - }; - - // -- save array pointer -- - abi::emit_push_reg(emitter, result_reg); // push the source array pointer onto the temporary stack - - if runtime_string_callback::emit_after_saved_array( - &args[1], - Some(&arr_ty), - vec![source_elem_ty.clone()], - PhpType::Void, - array_arg_reg, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper with a runtime string descriptor - }, - ) { - return Some(PhpType::Void); - } - - if let Some(wrapper) = callback_env::emit_callable_array_descriptor_env_after_saved_array( - &args[1], - array_arg_reg, - call_reg, - vec![source_elem_ty.clone()], - PhpType::Void, - emitter, - ctx, - data, - ) { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper with a callable-array descriptor environment - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Void); - } - - if runtime_callable_array_callback::emit_after_saved_array( - &args[1], - array_arg_reg, - vec![source_elem_ty.clone()], - PhpType::Void, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper with a runtime callable-array descriptor - }, - ) { - return Some(PhpType::Void); - } - - if callback_env::expr_call_needs_descriptor_callback_env(&args[1], ctx) - && callback_env::descriptor_callback_env_supported(&args[1]) - { - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // preserve the selected callable descriptor while recovering the source array - abi::emit_pop_reg(emitter, array_arg_reg); // recover the source array pointer before building the descriptor environment - emitter.instruction(&format!("mov {}, {}", result_reg, call_reg)); // restore the selected callable descriptor as the current result - let wrapper = callback_env::emit_descriptor_callback_env_from_result( - &args[1], - array_arg_reg, - vec![source_elem_ty.clone()], - PhpType::Void, - emitter, - ctx, - ) - .expect("descriptor callback env support checked before emitting callback"); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper with a descriptor environment - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Void); - } - - // -- evaluate the callback argument and resolve its function address -- - let captures = - callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); - - // -- place callback and array pointer into the runtime argument registers -- - if !captures.is_empty() { - abi::emit_pop_reg(emitter, result_reg); // recover the source array pointer before building the capture environment - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![source_elem_ty], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper with a capture environment - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Void); - } else { - abi::emit_pop_reg(emitter, array_arg_reg); // pop the source array pointer into the second runtime argument register - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the callback function address into the first runtime argument register - } - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - abi::emit_call_label(emitter, "__rt_array_walk"); // call the callback-driven walk runtime helper - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/arsort.rs b/src/codegen/builtins/arrays/arsort.rs deleted file mode 100644 index 67863d7567..0000000000 --- a/src/codegen/builtins/arrays/arsort.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Purpose: -//! Emits PHP `arsort` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `arsort` builtin, which sorts an associative array by values -/// in descending order while maintaining key-to-value associations. -/// -/// Inputs: -/// - `args[0]`: the array expression to sort (mutated in place) -/// - `emitter`: target assembly emitter -/// - `ctx`: codegen context (carries variable layout, ownership state) -/// - `data`: data section for embedded literals -/// -/// Behavior: -/// - Evaluates the array expression and captures its type. -/// - Prepares the array for mutation via COW (copy-on-write) if needed. -/// - Stores the array pointer back to the caller-side storage for ref-like semantics. -/// - Calls `__rt_arsort` to perform the sort in-place. -/// -/// Returns `Some(PhpType::Void)` on success. -/// -/// Note: `_name` is unused; the catalog resolves the builtin by canonical name. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("arsort()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - // -- sort associative array by values descending, maintaining key association -- - abi::emit_call_label(emitter, "__rt_arsort"); // call the target-aware runtime helper that sorts array values descending while preserving key association - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/asort.rs b/src/codegen/builtins/arrays/asort.rs deleted file mode 100644 index 81d2e254fd..0000000000 --- a/src/codegen/builtins/arrays/asort.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Purpose: -//! Emits PHP `asort` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `asort` builtin, which sorts an array by values -/// in ascending order while maintaining key-to-value associations. -/// -/// This function: -/// - Evaluates the array expression and prepares it for COW (copy-on-write) -/// - Handles the ref-like mutation semantics so the caller's storage is updated -/// - Calls the `__rt_asort` runtime helper -/// -/// # Arguments -/// - `_name`: Unused, matching the builtin emitter signature -/// - `args`: Must contain exactly one array argument -/// - `emitter`: Assembly emitter for the current target -/// - `ctx`: Codegen context with variable layout and metadata -/// - `data`: Data section for constants/literals -/// -/// # Returns -/// Always returns `Some(PhpType::Void)` since `asort` has no return value -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("asort()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - // -- sort associative array by values, maintaining key association -- - abi::emit_call_label(emitter, "__rt_asort"); // call the target-aware runtime helper that sorts array values ascending while preserving key association - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/buffer_free.rs b/src/codegen/builtins/arrays/buffer_free.rs deleted file mode 100644 index 86f87e6408..0000000000 --- a/src/codegen/builtins/arrays/buffer_free.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `buffer_free` operations for runtime buffer values. -//! Keeps buffer pointer/length ABI handling near array-like builtin dispatch. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Buffer helpers operate on raw runtime handles and must not treat them as PHP arrays. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits the `buffer_free` builtin call, releasing a runtime buffer and -/// nullifying its local stack slot. -/// -/// - Loads the buffer handle via `emit_expr` (pointer in x1, length in x2). -/// - Calls `__rt_heap_free` to release the header and contiguous payload. -/// - For local variable targets (not ref params, globals, or statics), zeros -/// the stack slot so subsequent use trips the null-buffer fatal helper. -/// - Returns `Some(PhpType::Void)`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("buffer_free()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_heap_free"); // release the buffer header and contiguous payload through the target-aware heap helper - - // -- nullify the local stack slot so use-after-free hits a null check -- - // The type checker restricts buffer_free() to plain local variables only - // (no ref params, globals, or statics), so writing xzr to the stack slot - // is always the correct nullification path here. - if let ExprKind::Variable(var_name) = &args[0].kind { - if let Some(var) = ctx.variables.get(var_name) { - if !ctx.ref_params.contains(var_name) - && !ctx.global_vars.contains(var_name) - && !ctx.static_vars.contains(var_name) - { - abi::emit_store_zero_to_local_slot(emitter, var.stack_offset); // zero the local stack slot so subsequent buffer accesses trip the null-buffer fatal helper - } - } - } - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/buffer_len.rs b/src/codegen/builtins/arrays/buffer_len.rs deleted file mode 100644 index ac3378f787..0000000000 --- a/src/codegen/builtins/arrays/buffer_len.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `buffer_len` operations for runtime buffer values. -//! Keeps buffer pointer/length ABI handling near array-like builtin dispatch. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Buffer helpers operate on raw runtime handles and must not treat them as PHP arrays. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the `buffer_len` builtin call. -/// -/// Validates the argument is a buffer type (emits a warning if not), then calls -/// the runtime helper `__rt_buffer_len` to extract the logical element count from -/// the buffer header. Returns `PhpType::Int` unconditionally. -/// -/// # Arguments -/// * `name` - Unused, present for dispatcher signature uniformity. -/// * `args` - Must contain exactly one expression evaluating to a buffer. -/// * `emitter` - Target-aware assembly emitter. -/// * `ctx` - Codegen context carrying variable layouts and metadata. -/// * `data` - Data section for constants and metadata tables. -/// -/// # Returns -/// Always returns `Some(PhpType::Int)` representing the buffer's element count. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let buf_ty = emit_expr(&args[0], emitter, ctx, data); - if !matches!(buf_ty, PhpType::Buffer(_)) { - emitter.comment("WARNING: buffer_len() received a non-buffer argument"); - } - abi::emit_call_label(emitter, "__rt_buffer_len"); // load the logical element count from the buffer header through the target-aware runtime helper - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/arrays/call_user_func.rs b/src/codegen/builtins/arrays/call_user_func.rs deleted file mode 100644 index 7f54a064d5..0000000000 --- a/src/codegen/builtins/arrays/call_user_func.rs +++ /dev/null @@ -1,352 +0,0 @@ -//! Purpose: -//! Emits PHP `call_user_func` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{emit_expr, expr_result_heap_ownership}; -use crate::codegen::expr::calls::args; -use crate::codegen::abi; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; -use super::callback_env; -use super::callable_forms; -use super::call_user_func_array; -use super::descriptor_arg_builder; -use super::super::callable_lookup::{lookup_function, FunctionLookup}; - -/// Emits `call_user_func($callback, ...$args)` builtin calls. -/// -/// Dispatches to extern/builtin when the first argument is a string literal known -/// at compile time. Otherwise, materializes the callback address, evaluates all -/// remaining arguments in PHP source order (including by-reference and default -/// parameter padding), pushes captures as hidden arguments, then emits the call -/// via `blr`. -/// -/// Arguments: -/// - `args[0]`: callback (string literal function name, closure, or first-class callable) -/// - `args[1..]`: arguments to pass through to the callback -/// -/// Returns the inferred return type from the callback's signature, defaulting to `Int` -/// when the signature cannot be determined. -/// -/// ABI constraints: -/// - Callback address is placed in `call_reg` before `blr`. -/// - Arguments are materialized as outgoing args via `materialize_outgoing_args`. -/// - On x86_64, concat offset is saved/restored around the nested call. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("call_user_func()"); - if let ExprKind::StringLiteral(name) = &args[0].kind { - match lookup_function(ctx, name) { - Some(FunctionLookup::Extern(extern_name)) => { - return Some(crate::codegen::ffi::emit_extern_call( - &extern_name, - &args[1..], - args[0].span, - emitter, - ctx, - data, - )); - } - Some(FunctionLookup::Builtin(builtin_name)) => { - if let Some(ret_ty) = crate::codegen::builtins::emit_builtin_call( - &builtin_name, - &args[1..], - args[0].span, - emitter, - ctx, - data, - ) { - return Some(ret_ty); - } - } - Some(FunctionLookup::UserFunction(_)) | Some(FunctionLookup::IncludeVariant(_)) | None => {} - } - } - if let Some(ret_ty) = callable_forms::emit_call_user_func_form( - &args[0], - &args[1..], - emitter, - ctx, - data, - ) { - return Some(ret_ty); - } - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - let call_reg = abi::nested_call_reg(emitter); - if call_user_func_array::callback_is_runtime_string(&args[0], ctx) { - let arg_array = Expr::new( - ExprKind::ArrayLiteral(args[1..].to_vec()), - args[0].span, - ); - let ret_ty = call_user_func_array::emit_dynamic_string_callback_with_array_expr( - &args[0], - &arg_array, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - return Some(ret_ty); - } - if let Some(ret_ty) = emit_descriptor_backed_call_user_func( - &args[0], - &args[1..], - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ) { - return Some(ret_ty); - } - - // -- resolve callback function address -- - let is_callable_expr = matches!( - &args[0].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let precomputed_sig = crate::codegen::callables::callable_sig(&args[0], ctx); - let captures = - callback_env::materialize_callback_address(&args[0], call_reg, emitter, ctx, data); - let sig: Option = if is_callable_expr { - ctx.deferred_closures - .last() - .map(|deferred| deferred.sig.clone()) - } else { - precomputed_sig - }; - let ret_ty = sig - .as_ref() - .map(|sig| sig.return_type.clone()) - .unwrap_or(PhpType::Int); - - // -- evaluate remaining arguments and push onto stack -- - let mut arg_types = Vec::new(); - for (i, arg) in args[1..].iter().enumerate() { - let is_ref = sig - .as_ref() - .and_then(|sig| sig.ref_params.get(i)) - .copied() - .unwrap_or(false); - let target_ty = args::declared_target_ty(sig.as_ref(), i); - if is_ref { - if let ExprKind::Variable(var_name) = &arg.kind { - if !args::emit_ref_arg_variable_address(var_name, "call_user_func ref arg", emitter, ctx) { - panic!("call_user_func() by-reference callback argument variable not found"); - } - } else { - panic!("call_user_func() by-reference callback argument must be a variable"); - } - args::push_arg_value(emitter, &PhpType::Int); - arg_types.push(PhpType::Int); - continue; - } - - let pushed_ty = args::push_expr_arg(arg, target_ty, emitter, ctx, data); - arg_types.push(pushed_ty); - } - - if let Some(sig) = &sig { - let visible_param_count = sig.params.len(); - let regular_param_count = if sig.variadic.is_some() { - visible_param_count.saturating_sub(1) - } else { - visible_param_count - }; - for i in arg_types.len()..regular_param_count { - if let Some(Some(default_expr)) = sig.defaults.get(i) { - let target_ty = sig.params.get(i).map(|(_, ty)| ty); - let pushed_ty = args::push_expr_arg(default_expr, target_ty, emitter, ctx, data); - arg_types.push(pushed_ty); - } - } - } - callback_env::push_captures_as_hidden_args(&captures, emitter, ctx, &mut arg_types); - - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); - - // -- load callback address and call via blr -- - if !save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - abi::emit_call_reg(emitter, call_reg); - if save_concat_before_args { - abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - abi::emit_release_temporary_stack(emitter, overflow_bytes); - } - - Some(ret_ty) -} - -/// Emits descriptor-invoker dispatch for callable values already represented as descriptors. -/// -/// Variable arguments are encoded as invoker-only reference-cell markers when -/// the signature has by-reference slots, or when the static signature is not -/// known and the generated descriptor invoker must decide from runtime metadata. -/// Captures, including by-ref captures, remain descriptor-owned and are loaded -/// by the generated invoker. -#[allow(clippy::too_many_arguments)] -fn emit_descriptor_backed_call_user_func( - callback: &Expr, - callback_args: &[Expr], - call_reg: &str, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let sig = descriptor_invoker_sig(callback, ctx)?; - let ownership = expr_result_heap_ownership(callback); - if !matches!(ownership, HeapOwnership::Owned | HeapOwnership::Borrowed) { - return None; - } - - let _callback_ty = emit_expr(callback, emitter, ctx, data); - let sig = if matches!(callback.kind, ExprKind::Closure { .. }) { - ctx.deferred_closures.last().map(|deferred| deferred.sig.clone()) - } else { - sig - }; - if matches!(ownership, HeapOwnership::Borrowed) { - crate::codegen::callable_descriptor::emit_retain_current_descriptor(emitter); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the callable descriptor while building call_user_func() arguments - - let encode_variable_refs = should_encode_invoker_ref_args(sig.as_ref(), callback_args); - if let Some(sig) = sig.as_ref() { - validate_descriptor_call_user_func_ref_args(sig, callback_args); - } - let arr_ty = if encode_variable_refs { - descriptor_arg_builder::emit_indexed_invoker_arg_array( - callback_args, - encode_variable_refs, - emitter, - ctx, - data, - ) - } else { - let arg_array = Expr::new( - ExprKind::ArrayLiteral(callback_args.to_vec()), - callback.span, - ); - emit_expr(&arg_array, emitter, ctx, data) - }; - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the owned call_user_func() argument array for invocation and cleanup - abi::emit_load_temporary_stack_slot(emitter, call_reg, 16); - call_user_func_array::emit_call_descriptor_array_invoker( - call_user_func_array::LoadedArraySource::TemporaryStackSlot(0), - &arr_ty, - call_reg, - concat_saved_before_args, - emitter, - ctx, - data, - ); - release_owned_arg_array_after_mixed_result(&arr_ty, emitter); - release_preserved_descriptor_after_mixed_result(emitter); - Some(PhpType::Mixed) -} - -/// Returns whether call_user_func() should encode variable args as ref-cell markers. -fn should_encode_invoker_ref_args(sig: Option<&FunctionSig>, callback_args: &[Expr]) -> bool { - if !callback_args - .iter() - .any(|arg| matches!(arg.kind, ExprKind::Variable(_))) - { - return false; - } - sig.is_none_or(|sig| sig.ref_params.iter().any(|is_ref| *is_ref)) -} - -/// Preserves PHP's explicit by-reference argument rule for statically known callbacks. -fn validate_descriptor_call_user_func_ref_args(sig: &FunctionSig, callback_args: &[Expr]) { - for (i, arg) in callback_args.iter().enumerate() { - if sig.ref_params.get(i).copied().unwrap_or(false) - && !matches!(arg.kind, ExprKind::Variable(_)) - { - panic!("call_user_func() by-reference callback argument must be a variable"); - } - } -} - -/// Releases the synthetic call_user_func() argument array while preserving the call result. -fn release_owned_arg_array_after_mixed_result(arr_ty: &PhpType, emitter: &mut Emitter) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed call result while releasing the synthetic argument array - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, arr_ty); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the boxed call result after argument-array cleanup - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved synthetic argument-array slot -} - -/// Returns optional callable signature metadata for expressions that produce descriptor values. -fn descriptor_invoker_sig(callback: &Expr, ctx: &Context) -> Option> { - if matches!(callback.kind, ExprKind::StringLiteral(_)) { - return None; - } - if matches!(callback.kind, ExprKind::Closure { .. }) { - return Some(None); - } - if matches!(&callback.kind, ExprKind::Variable(name) if ctx.ref_params.contains(name)) { - return None; - } - match &callback.kind { - ExprKind::Variable(_) - | ExprKind::ArrayAccess { .. } - | ExprKind::PropertyAccess { .. } - | ExprKind::DynamicPropertyAccess { .. } - | ExprKind::StaticPropertyAccess { .. } - | ExprKind::FirstClassCallable(_) - | ExprKind::FunctionCall { .. } - | ExprKind::MethodCall { .. } - | ExprKind::StaticMethodCall { .. } - | ExprKind::ExprCall { .. } - | ExprKind::Assignment { .. } - | ExprKind::Ternary { .. } - | ExprKind::ShortTernary { .. } - | ExprKind::NullCoalesce { .. } => {} - _ => return None, - } - let static_sig = crate::codegen::callables::callable_sig(callback, ctx); - if static_sig.is_some() - || matches!( - crate::codegen::functions::infer_contextual_type(callback, ctx).codegen_repr(), - PhpType::Callable - ) - { - Some(static_sig) - } else { - None - } -} - -/// Releases the preserved callback descriptor while keeping the boxed Mixed call result live. -fn release_preserved_descriptor_after_mixed_result(emitter: &mut Emitter) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve call_user_func() result while releasing the callback descriptor owner - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - crate::codegen::callable_descriptor::emit_release_current_descriptor(emitter); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the boxed Mixed call_user_func() result - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callable descriptor slot -} diff --git a/src/codegen/builtins/arrays/call_user_func_array.rs b/src/codegen/builtins/arrays/call_user_func_array.rs deleted file mode 100644 index 95f85bd530..0000000000 --- a/src/codegen/builtins/arrays/call_user_func_array.rs +++ /dev/null @@ -1,3308 +0,0 @@ -//! Purpose: -//! Emits PHP `call_user_func_array` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{emit_expr, expr_result_heap_ownership}; -use crate::codegen::expr::calls::args; -use crate::codegen::platform::Arch; -use crate::codegen::abi; -use crate::codegen::callable_dispatch::{ - self, RuntimeCallableCase, RuntimeCallableSelector, -}; -use crate::codegen::callable_descriptor; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver}; -use crate::types::{FunctionSig, PhpType}; -use super::callback_env; -use super::callable_forms; -use super::super::callable_lookup::{lookup_function, FunctionLookup}; - -/// Internal boxed-Mixed tag used only inside descriptor-invoker argument arrays. -pub(crate) const INVOKER_ARG_REF_CELL_TAG: i64 = 11; - -/// Stamps the heap header of a runtime array with a runtime value-type tag derived from -/// `elem_ty`. Used by `call_user_func_array` to mark variadic tail arrays so the runtime -/// can distinguish element types without compile-time layout information. -/// -/// - `array_reg`: register holding the array pointer. -/// - `elem_ty`: the element type whose runtime tag is written into the packed array kind word. -/// Preserves the indexed-array kind and persistent COW flag from the heap header. -fn emit_array_value_type_stamp(emitter: &mut Emitter, array_reg: &str, elem_ty: &PhpType) { - let value_type_tag = match elem_ty { - PhpType::Float => 2, - PhpType::Bool => 3, - PhpType::Str => 1, - PhpType::Array(_) => 4, - PhpType::AssocArray { .. } => 5, - PhpType::Object(_) => 6, - PhpType::Mixed => 7, - PhpType::Union(_) => 7, - PhpType::Void => 8, - _ => return, - }; - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("ldr x10, [{}, #-8]", array_reg)); // load the packed array kind word from the heap header - emitter.instruction("mov x12, #0x80ff"); // preserve the indexed-array kind and persistent COW flag - emitter.instruction("and x10, x10, x12"); // keep only the persistent indexed-array metadata bits - emitter.instruction(&format!("mov x11, #{}", value_type_tag)); // materialize the runtime array value_type tag - emitter.instruction("lsl x11, x11, #8"); // move the value_type tag into the packed kind-word byte lane - emitter.instruction("orr x10, x10, x11"); // combine the heap kind with the array value_type tag - emitter.instruction(&format!("str x10, [{}, #-8]", array_reg)); // persist the packed array kind word in the heap header - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov r10, QWORD PTR [{} - 8]", array_reg)); // load the packed array kind word from the heap header - emitter.instruction("mov rdx, 0xffffffff000080ff"); // materialize the x86_64 indexed-array metadata preservation mask - emitter.instruction("and r10, rdx"); // preserve heap marker, indexed-array kind, and persistent COW bits - emitter.instruction(&format!("mov rcx, {}", value_type_tag)); // materialize the runtime array value_type tag - emitter.instruction("shl rcx, 8"); // move the value_type tag into the packed kind-word byte lane - emitter.instruction("or r10, rcx"); // combine the heap kind with the array value_type tag - emitter.instruction(&format!("mov QWORD PTR [{} - 8], r10", array_reg)); // persist the packed array kind word in the heap header - } - } -} - -#[derive(Clone, Copy)] -pub(crate) enum LoadedArraySource { - Result, - TemporaryStackSlot(usize), - ArgumentRegister(usize), -} - -#[derive(Clone, Copy)] -pub(crate) enum LoadedDescriptorSource { - TemporaryStackSlot(usize), -} - -/// Loads a previously materialized callback argument array into `dest_reg`. -fn emit_loaded_array_source_to_reg( - array_source: LoadedArraySource, - dest_reg: &str, - emitter: &mut Emitter, -) { - match array_source { - LoadedArraySource::Result => { - emitter.instruction(&format!("mov {}, {}", dest_reg, abi::int_result_reg(emitter))); // preserve the callback-argument array pointer from the result register - } - LoadedArraySource::TemporaryStackSlot(offset) => { - abi::emit_load_temporary_stack_slot(emitter, dest_reg, offset); - } - LoadedArraySource::ArgumentRegister(index) => { - let arg_reg = abi::int_arg_reg_name(emitter.target, index); - if arg_reg != dest_reg { - emitter.instruction(&format!("mov {}, {}", dest_reg, arg_reg)); // copy the callback-argument array from the invoker ABI register - } - } - } -} - -/// Loads a previously preserved callable descriptor into `dest_reg`. -fn emit_loaded_descriptor_source_to_reg( - descriptor_source: LoadedDescriptorSource, - dest_reg: &str, - emitter: &mut Emitter, -) { - match descriptor_source { - LoadedDescriptorSource::TemporaryStackSlot(offset) => { - abi::emit_load_temporary_stack_slot(emitter, dest_reg, offset); - } - } -} - -/// Adjusts a descriptor stack source when this frame also preserved the argument array. -fn descriptor_source_after_array_push( - descriptor_source: Option, - pushed_array: bool, -) -> Option { - descriptor_source.map(|source| match source { - LoadedDescriptorSource::TemporaryStackSlot(offset) => { - LoadedDescriptorSource::TemporaryStackSlot(offset + if pushed_array { 16 } else { 0 }) - } - }) -} - -/// Adjusts an argument-array source after preserving a descriptor on the temporary stack. -fn array_source_after_descriptor_push(array_source: LoadedArraySource) -> LoadedArraySource { - match array_source { - LoadedArraySource::TemporaryStackSlot(offset) => { - LoadedArraySource::TemporaryStackSlot(offset + 16) - } - LoadedArraySource::Result => LoadedArraySource::Result, - LoadedArraySource::ArgumentRegister(index) => LoadedArraySource::ArgumentRegister(index), - } -} - -/// Resolves a callback to its entry address and preserves its descriptor when available. -fn materialize_callback_address_and_preserve_descriptor( - callback: &Expr, - call_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> (Vec<(String, PhpType, bool)>, Option) { - match &callback.kind { - ExprKind::StringLiteral(name) => { - let resolved_name = match lookup_function(ctx, name) { - Some(FunctionLookup::UserFunction(name)) - | Some(FunctionLookup::IncludeVariant(name)) => name, - _ => name.clone(), - }; - let label = crate::names::function_symbol(&resolved_name); - abi::emit_symbol_address(emitter, call_reg, &label); - (Vec::new(), None) - } - ExprKind::Variable(name) => { - let var = ctx.variables.get(name).expect("undefined callback variable"); - abi::load_at_offset(emitter, call_reg, var.stack_offset); // load the callback descriptor from the callable variable slot - if ctx.ref_params.contains(name) { - abi::emit_load_from_address(emitter, call_reg, call_reg, 0); - } - abi::emit_push_reg(emitter, call_reg); // preserve the callable descriptor for descriptor-invoker dispatch - callable_descriptor::emit_load_entry_from_descriptor(emitter, call_reg, call_reg); - ( - crate::codegen::callables::callable_captures(callback, ctx), - Some(LoadedDescriptorSource::TemporaryStackSlot(0)), - ) - } - _ => { - emit_expr(callback, emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, abi::int_result_reg(emitter))); // keep the evaluated callback descriptor in the nested-call scratch register - abi::emit_push_reg(emitter, call_reg); // preserve the evaluated callable descriptor for descriptor-invoker dispatch - callable_descriptor::emit_load_entry_from_descriptor(emitter, call_reg, call_reg); - ( - crate::codegen::callables::callable_captures(callback, ctx), - Some(LoadedDescriptorSource::TemporaryStackSlot(0)), - ) - } - } -} - -/// Emits code for the `call_user_func_array($callback, $args)` builtin. -/// Dispatches to extern/builtin call handlers when the callback is statically resolvable, -/// otherwise falls through to full callback resolution, array element extraction, argument -/// materialization, and indirect call via the resolved function address. -/// -/// - `$callback` (args[0]): a string naming a function, a Closure, a first-class callable, -/// or any other expression the resolver can materialize into a function pointer + signature. -/// - `$args` (args[1]): an array whose elements are unpacked as positional call arguments. -/// -/// Returns the return type of the invoked callback, or `PhpType::Void` if unresolvable. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("call_user_func_array()"); - if let (ExprKind::StringLiteral(name), ExprKind::ArrayLiteral(elems)) = - (&args[0].kind, &args[1].kind) - { - match lookup_function(ctx, name) { - Some(FunctionLookup::Extern(extern_name)) => { - return Some(crate::codegen::ffi::emit_extern_call( - &extern_name, - elems, - args[0].span, - emitter, - ctx, - data, - )); - } - Some(FunctionLookup::Builtin(builtin_name)) => { - if let Some(ret_ty) = crate::codegen::builtins::emit_builtin_call( - &builtin_name, - elems, - args[0].span, - emitter, - ctx, - data, - ) { - return Some(ret_ty); - } - } - Some(FunctionLookup::UserFunction(_)) | Some(FunctionLookup::IncludeVariant(_)) | None => {} - } - } - if let Some(ret_ty) = callable_forms::emit_call_user_func_array_form( - &args[0], - &args[1], - emitter, - ctx, - data, - ) { - return Some(ret_ty); - } - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - let call_reg = abi::nested_call_reg(emitter); - if callback_is_runtime_string(&args[0], ctx) { - let ret_ty = emit_dynamic_string_callback_with_array_expr( - &args[0], - &args[1], - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - return Some(ret_ty); - } - if expr_call_needs_descriptor_invoker(&args[0], ctx) { - if let Some(ret_ty) = emit_descriptor_invoker_call_user_func_array_expr( - &args[0], - &args[1], - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ) { - return Some(ret_ty); - } - } - - // -- resolve callback function address and signature -- - let direct_fcc_function = - crate::codegen::callables::direct_first_class_function_sig(&args[0], ctx); - let precomputed_sig = direct_fcc_function - .as_ref() - .map(|(_, sig)| sig.clone()) - .or_else(|| crate::codegen::callables::callable_sig(&args[0], ctx)); - let (captures, descriptor_source) = if let Some((resolved_name, _)) = direct_fcc_function.as_ref() { - let label = crate::names::function_symbol(resolved_name); - abi::emit_symbol_address(emitter, call_reg, &label); - (Vec::new(), None) - } else { - materialize_callback_address_and_preserve_descriptor( - &args[0], - call_reg, - emitter, - ctx, - data, - ) - }; - let sig = - if direct_fcc_function.is_none() - && matches!( - &args[0].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ) - { - Some( - ctx.deferred_closures - .last() - .expect("call_user_func_array: missing synthesized callable signature") - .sig - .clone(), - ) - } else { - precomputed_sig - }; - - let ret_ty = if let Some(sig) = sig { - if sig.ref_params.iter().any(|is_ref| *is_ref) { - let arr_ty = emit_expr(&args[1], emitter, ctx, data); - let literal_arg_elems = match &args[1].kind { - ExprKind::ArrayLiteral(elems) => Some(elems.as_slice()), - _ => None, - }; - if literal_arg_elems.is_none() { - if let Some(source) = descriptor_source { - emit_loaded_descriptor_source_to_reg(source, call_reg, emitter); - emit_call_descriptor_array_invoker( - LoadedArraySource::Result, - &arr_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - PhpType::Mixed - } else { - emit_loaded_array_callback_call( - LoadedArraySource::Result, - &arr_ty, - None, - call_reg, - &captures, - &sig, - save_concat_before_args, - emitter, - ctx, - data, - ) - } - } else { - emit_loaded_array_callback_call( - LoadedArraySource::Result, - &arr_ty, - literal_arg_elems, - call_reg, - &captures, - &sig, - save_concat_before_args, - emitter, - ctx, - data, - ) - } - } else { - let inferred_arg_array_ty = - crate::codegen::functions::infer_contextual_type(&args[1], ctx); - if should_use_unknown_indexed_dispatch(&sig, &inferred_arg_array_ty, &args[1]) { - let arr_ty = emit_expr(&args[1], emitter, ctx, data); - emit_loaded_array_unknown_callback_call( - LoadedArraySource::Result, - &arr_ty, - call_reg, - &captures, - descriptor_source, - save_concat_before_args, - emitter, - ctx, - data, - ) - } else if matches!(inferred_arg_array_ty, PhpType::AssocArray { .. }) - && sig.variadic.is_some() - { - let arr_ty = emit_expr(&args[1], emitter, ctx, data); - emit_loaded_array_callback_call( - LoadedArraySource::Result, - &arr_ty, - None, - call_reg, - &captures, - &sig, - save_concat_before_args, - emitter, - ctx, - data, - ) - } else { - emit_spread_callback_call_from_array_expr( - &args[1], - call_reg, - &captures, - &sig, - save_concat_before_args, - emitter, - ctx, - data, - ) - } - } - } else { - // Evaluate the array argument (second arg) - let arr_ty = emit_expr(&args[1], emitter, ctx, data); - emit_loaded_array_unknown_callback_call( - LoadedArraySource::Result, - &arr_ty, - call_reg, - &captures, - descriptor_source, - save_concat_before_args, - emitter, - ctx, - data, - ) - }; - - if descriptor_source.is_some() { - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callable descriptor after call_user_func_array() - } - - Some(ret_ty) -} - -/// Returns true when use unknown indexed dispatch. -fn should_use_unknown_indexed_dispatch( - sig: &FunctionSig, - arg_array_ty: &PhpType, - arg_array: &Expr, -) -> bool { - matches!(arg_array_ty, PhpType::Array(_)) - && !matches!(arg_array.kind, ExprKind::ArrayLiteral(_)) - && sig.variadic.is_none() - && sig.ref_params.iter().all(|is_ref| !*is_ref) - && sig.defaults.iter().all(Option::is_none) - && sig.declared_params.iter().all(|declared| !*declared) -} - -/// Provides the Callback is runtime string helper used by the call user func array module. -pub(crate) fn callback_is_runtime_string(callback: &Expr, ctx: &Context) -> bool { - !matches!(callback.kind, ExprKind::StringLiteral(_)) - && matches!( - crate::codegen::functions::infer_contextual_type(callback, ctx).codegen_repr(), - PhpType::Str - ) -} - -/// Emits descriptor-invoker dispatch for branch-shaped captured `call_user_func_array()` callbacks. -/// -/// Branch expressions can select receiver-bound or captured descriptors at runtime, so the -/// invocation must load captures from the selected descriptor instead of trying to use a -/// direct ABI call with compile-time hidden arguments. -#[allow(clippy::too_many_arguments)] -fn emit_descriptor_invoker_call_user_func_array_expr( - callback: &Expr, - arg_array: &Expr, - call_reg: &str, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let ownership = callable_descriptor_result_ownership(callback); - if !matches!(ownership, HeapOwnership::Owned | HeapOwnership::Borrowed) { - return None; - } - - let sig = crate::codegen::callables::callable_sig(callback, ctx); - let _callback_ty = emit_expr(callback, emitter, ctx, data); - if matches!(ownership, HeapOwnership::Borrowed) { - callable_descriptor::emit_retain_current_descriptor(emitter); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the selected callable descriptor while building call_user_func_array() args - - let (arr_ty, release_arg_array) = emit_descriptor_invoker_arg_array_for_call_user_func_array( - arg_array, - sig.as_ref(), - emitter, - ctx, - data, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor-invoker argument array for invocation and cleanup - abi::emit_load_temporary_stack_slot(emitter, call_reg, 16); - emit_call_descriptor_array_invoker( - LoadedArraySource::TemporaryStackSlot(0), - &arr_ty, - call_reg, - concat_saved_before_args, - emitter, - ctx, - data, - ); - release_preserved_descriptor_invoker_arg_array_after_mixed_result( - &arr_ty, - release_arg_array, - emitter, - ); - release_preserved_descriptor_after_mixed_result(emitter); - Some(PhpType::Mixed) -} - -/// Returns the ownership class for a callable descriptor expression result. -fn callable_descriptor_result_ownership(callback: &Expr) -> HeapOwnership { - if matches!(callback.kind, ExprKind::Assignment { .. }) { - return HeapOwnership::Borrowed; - } - expr_result_heap_ownership(callback) -} - -/// Emits or reuses the argument container passed to a descriptor invoker. -fn emit_descriptor_invoker_arg_array_for_call_user_func_array( - arg_array: &Expr, - sig: Option<&FunctionSig>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> (PhpType, bool) { - if let ExprKind::ArrayLiteral(elems) = &arg_array.kind { - if should_encode_invoker_array_literal_refs(sig, elems) { - let arr_ty = emit_descriptor_invoker_indexed_arg_array_for_call_user_func_array( - elems, - sig, - emitter, - ctx, - data, - ); - return (arr_ty, true); - } - } - - let arr_ty = emit_expr(arg_array, emitter, ctx, data); - let release_arg_array = expr_result_heap_ownership(arg_array) == HeapOwnership::Owned; - (arr_ty, release_arg_array) -} - -/// Returns true when a literal indexed argument array needs ref-cell markers for the invoker. -fn should_encode_invoker_array_literal_refs(sig: Option<&FunctionSig>, elems: &[Expr]) -> bool { - elems.iter().enumerate().any(|(index, elem)| { - matches!(elem.kind, ExprKind::Variable(_)) - && sig.is_none_or(|sig| sig.ref_params.get(index).copied().unwrap_or(false)) - }) -} - -/// Builds a Mixed indexed argument array with invoker-only reference-cell markers. -fn emit_descriptor_invoker_indexed_arg_array_for_call_user_func_array( - elems: &[Expr], - sig: Option<&FunctionSig>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("call_user_func_array() descriptor literal argument array"); - let capacity = elems.len().max(4); - let capacity_reg = abi::int_arg_reg_name(emitter.target, 0); - let elem_size_reg = abi::int_arg_reg_name(emitter.target, 1); - abi::emit_load_int_immediate(emitter, capacity_reg, capacity as i64); - abi::emit_load_int_immediate(emitter, elem_size_reg, 8); - abi::emit_call_label(emitter, "__rt_array_new"); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the descriptor argument array alive while filling Mixed slots - abi::emit_load_temporary_stack_slot(emitter, abi::symbol_scratch_reg(emitter), 0); - crate::codegen::expr::arrays::emit_array_value_type_stamp( - emitter, - abi::symbol_scratch_reg(emitter), - &PhpType::Mixed, - ); - - for (index, elem) in elems.iter().enumerate() { - if should_encode_invoker_literal_ref_arg(sig, index, elem) { - if let ExprKind::Variable(var_name) = &elem.kind { - if !args::emit_ref_arg_variable_address( - var_name, - "call_user_func_array descriptor arg", - emitter, - ctx, - ) { - panic!("call_user_func_array() descriptor argument variable not found"); - } - emit_box_current_ref_arg_address_for_invoker(var_name, emitter, ctx); - emit_store_descriptor_invoker_arg_array_slot(index, emitter); - continue; - } - } - - let mut ty = emit_expr(elem, emitter, ctx, data); - let boxed_iterable = crate::codegen::emit_box_iterable_value_for_mixed_container( - emitter, - &mut ty, - ); - if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - crate::codegen::emit_box_current_expr_value_as_mixed_for_container(emitter, elem, &ty); - } else if !boxed_iterable { - retain_borrowed_mixed_arg_for_invoker(emitter, elem, &ty); - } - emit_store_descriptor_invoker_arg_array_slot(index, emitter); - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the filled descriptor argument array - PhpType::Array(Box::new(PhpType::Mixed)) -} - -/// Returns true when this literal element should carry a source-variable ref marker. -fn should_encode_invoker_literal_ref_arg( - sig: Option<&FunctionSig>, - index: usize, - elem: &Expr, -) -> bool { - matches!(elem.kind, ExprKind::Variable(_)) - && sig.is_none_or(|sig| sig.ref_params.get(index).copied().unwrap_or(false)) -} - -/// Retains a borrowed boxed Mixed argument before storing it in the invoker array. -fn retain_borrowed_mixed_arg_for_invoker(emitter: &mut Emitter, arg: &Expr, ty: &PhpType) { - if ty.codegen_repr().is_refcounted() && expr_result_heap_ownership(arg) != HeapOwnership::Owned { - abi::emit_incref_if_refcounted(emitter, &ty.codegen_repr()); - } -} - -/// Boxes the current variable storage address as an invoker-only Mixed marker. -fn emit_box_current_ref_arg_address_for_invoker( - var_name: &str, - emitter: &mut Emitter, - ctx: &Context, -) { - let ref_cell_reg = abi::secondary_scratch_reg(emitter); - let marker_tag_reg = abi::tertiary_scratch_reg(emitter); - let source_tag_reg = abi::symbol_scratch_reg(emitter); - emitter.instruction(&format!("mov {}, {}", ref_cell_reg, abi::int_result_reg(emitter))); // preserve the source variable storage address before Mixed marker boxing - abi::emit_load_int_immediate(emitter, marker_tag_reg, INVOKER_ARG_REF_CELL_TAG); - abi::emit_load_int_immediate( - emitter, - source_tag_reg, - variable_runtime_value_tag(var_name, ctx) as i64, - ); - crate::codegen::emit_box_runtime_payload_as_mixed( - emitter, - marker_tag_reg, - ref_cell_reg, - source_tag_reg, - ); -} - -/// Returns the runtime tag for a variable's current codegen type. -fn variable_runtime_value_tag(var_name: &str, ctx: &Context) -> u8 { - ctx.variables - .get(var_name) - .map(|var| crate::codegen::runtime_value_tag(&var.ty.codegen_repr())) - .unwrap_or_else(|| crate::codegen::runtime_value_tag(&PhpType::Int)) -} - -/// Stores the current boxed Mixed argument into the synthetic invoker array. -fn emit_store_descriptor_invoker_arg_array_slot(index: usize, emitter: &mut Emitter) { - let array_reg = abi::symbol_scratch_reg(emitter); - let len_reg = abi::secondary_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, array_reg, 0); - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), array_reg, 24 + index * 8); - abi::emit_load_int_immediate(emitter, len_reg, (index + 1) as i64); - abi::emit_store_to_address(emitter, len_reg, array_reg, 0); -} - -/// Releases the preserved descriptor-invoker argument array while keeping the call result live. -fn release_preserved_descriptor_invoker_arg_array_after_mixed_result( - arr_ty: &PhpType, - should_release: bool, - emitter: &mut Emitter, -) { - if should_release { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed call result while releasing the invoker argument array - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, arr_ty); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the boxed call result after argument-array cleanup - } - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved descriptor-invoker argument-array slot -} - -/// Releases the preserved callable descriptor while keeping the boxed Mixed call result live. -fn release_preserved_descriptor_after_mixed_result(emitter: &mut Emitter) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor-invoker result while releasing the selected descriptor - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - callable_descriptor::emit_release_current_descriptor(emitter); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the boxed Mixed descriptor-invoker result - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved selected descriptor slot -} - -/// Returns true when `call_user_func_array()` must invoke a descriptor-owned environment. -fn expr_call_needs_descriptor_invoker(callback: &Expr, ctx: &Context) -> bool { - if runtime_callable_expr_result_needs_descriptor_invoker(callback, ctx) { - return true; - } - - match &callback.kind { - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) | ExprKind::Variable(_) => false, - ExprKind::Assignment { value, .. } => expr_produces_captured_callable(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => { - expr_produces_captured_callable(then_expr, ctx) - || expr_produces_captured_callable(else_expr, ctx) - } - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - expr_produces_captured_callable(value, ctx) - || expr_produces_captured_callable(default, ctx) - } - _ => false, - } -} - -/// Returns true when a runtime callable expression must use descriptor-owned metadata. -fn runtime_callable_expr_result_needs_descriptor_invoker(callback: &Expr, ctx: &Context) -> bool { - if !matches!( - crate::codegen::functions::infer_contextual_type(callback, ctx).codegen_repr(), - PhpType::Callable - ) { - return false; - } - match &callback.kind { - ExprKind::Variable(name) => ctx.runtime_callable_vars.contains(name), - ExprKind::ArrayAccess { .. } - | ExprKind::PropertyAccess { .. } - | ExprKind::DynamicPropertyAccess { .. } - | ExprKind::StaticPropertyAccess { .. } - | ExprKind::Assignment { .. } - | ExprKind::Ternary { .. } - | ExprKind::ShortTernary { .. } - | ExprKind::NullCoalesce { .. } - | ExprKind::FunctionCall { .. } - | ExprKind::MethodCall { .. } - | ExprKind::StaticMethodCall { .. } - | ExprKind::ExprCall { .. } => true, - _ => false, - } -} - -/// Returns true if an expression produces a callable with descriptor-owned environment. -fn expr_produces_captured_callable(expr: &Expr, ctx: &Context) -> bool { - match &expr.kind { - ExprKind::Closure { captures, .. } => !captures.is_empty(), - ExprKind::FirstClassCallable(target) => first_class_target_needs_runtime_capture(target), - ExprKind::Variable(name) => { - ctx.closure_captures - .get(name) - .is_some_and(|captures| !captures.is_empty()) - || ctx - .first_class_callable_targets - .get(name) - .is_some_and(first_class_target_needs_runtime_capture) - } - ExprKind::Assignment { value, .. } => expr_produces_captured_callable(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => { - expr_produces_captured_callable(then_expr, ctx) - || expr_produces_captured_callable(else_expr, ctx) - } - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - expr_produces_captured_callable(value, ctx) - || expr_produces_captured_callable(default, ctx) - } - _ => false, - } -} - -/// Returns true when a first-class callable target carries receiver environment. -fn first_class_target_needs_runtime_capture(target: &CallableTarget) -> bool { - matches!( - target, - CallableTarget::Method { .. } - | CallableTarget::StaticMethod { - receiver: StaticReceiver::Static, - .. - } - ) -} - -/// Emits assembly for dynamic string callback with array expr. -pub(crate) fn emit_dynamic_string_callback_with_array_expr( - callback: &Expr, - arg_array: &Expr, - call_reg: &str, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let callback_ty = emit_expr(callback, emitter, ctx, data); - debug_assert!(matches!(callback_ty.codegen_repr(), PhpType::Str)); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime string callback name while evaluating argument array - let arr_ty = emit_expr(arg_array, emitter, ctx, data); - let ret_ty = emit_loaded_array_string_callback_call( - LoadedArraySource::Result, - &arr_ty, - 0, - 8, - call_reg, - concat_saved_before_args, - emitter, - ctx, - data, - ); - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved runtime string callback name - ret_ty -} - -/// Emits assembly for spread callback call from array expr. -fn emit_spread_callback_call_from_array_expr( - arg_array: &Expr, - call_reg: &str, - captures: &[(String, PhpType, bool)], - sig: &FunctionSig, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - abi::emit_push_reg(emitter, call_reg); // preserve the callback address while unpacking call_user_func_array() args - let spread_arg = Expr::new(ExprKind::Spread(Box::new(arg_array.clone())), arg_array.span); - let visible_param_count = sig.params.len(); - let regular_param_count = if sig.variadic.is_some() { - visible_param_count.saturating_sub(1) - } else { - visible_param_count - }; - let emitted_args = args::emit_pushed_call_args( - &[spread_arg], - Some(sig), - regular_param_count, - "call_user_func_array ref arg", - true, - true, - emitter, - ctx, - data, - ); - let mut arg_types = emitted_args.arg_types; - callback_env::push_captures_as_hidden_args(captures, emitter, ctx, &mut arg_types); - - let callback_offset = args::pushed_temp_bytes(&arg_types) + emitted_args.source_temp_bytes; - abi::emit_load_temporary_stack_slot(emitter, call_reg, callback_offset); - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); - let ret_ty = sig.return_type.clone(); - - if !concat_saved_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - abi::emit_call_reg(emitter, call_reg); - if concat_saved_before_args { - abi::emit_release_temporary_stack(emitter, overflow_bytes); - abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - abi::emit_release_temporary_stack(emitter, 16); - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - abi::emit_release_temporary_stack(emitter, overflow_bytes); - abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - abi::emit_release_temporary_stack(emitter, 16); - } - - ret_ty -} - -/// Emits assembly for loaded array callback call. -pub(crate) fn emit_loaded_array_callback_call( - array_source: LoadedArraySource, - arr_ty: &PhpType, - literal_arg_elems: Option<&[Expr]>, - call_reg: &str, - captures: &[(String, PhpType, bool)], - sig: &FunctionSig, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if matches!(arr_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - return emit_loaded_mixed_array_callback_call( - array_source, - call_reg, - captures, - sig, - concat_saved_before_args, - emitter, - ctx, - data, - ); - } - if matches!(arr_ty, PhpType::AssocArray { .. }) { - return emit_loaded_assoc_array_callback_call( - array_source, - arr_ty, - call_reg, - captures, - sig, - concat_saved_before_args, - emitter, - ctx, - data, - ); - } - - let (array_reg, len_reg, tail_count_reg, tail_index_reg, index_reg, offset_reg, data_reg, peek_reg, array_new_capacity_reg, array_new_elem_size_reg, len_store_reg) = - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => ( - "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x9", "x0", "x1", "x10" - ), - crate::codegen::platform::Arch::X86_64 => ( - "r13", "r14", "r15", "rbx", "rcx", "r8", "r9", "r11", "rdi", "rsi", "r10" - ), - }; - - // Determine element type and size from the array type - let elem_ty = match arr_ty { - PhpType::Array(t) => *t.clone(), - PhpType::AssocArray { value, .. } => *value.clone(), - _ => PhpType::Int, - }; - let elem_size = args::array_element_stride(&elem_ty); - let visible_param_count = sig.params.len(); - let regular_param_count = if sig.variadic.is_some() { - visible_param_count.saturating_sub(1) - } else { - visible_param_count - }; - - emit_loaded_array_source_to_reg(array_source, array_reg, emitter); - abi::emit_load_from_address(emitter, len_reg, array_reg, 0); // load callback-argument array length - emit_indexed_required_arg_count_check( - sig, - regular_param_count, - len_reg, - emitter, - ctx, - data, - ); - - // -- extract elements from array and push them as regular call arguments -- - let mut arg_types = Vec::new(); - for i in 0..regular_param_count { - let is_ref = sig.ref_params.get(i).copied().unwrap_or(false); - if is_ref { - if let Some(Expr { - kind: ExprKind::Variable(var_name), - .. - }) = literal_arg_elems.and_then(|elems| elems.get(i)) - { - if !args::emit_ref_arg_variable_address( - var_name, - "call_user_func_array ref arg", - emitter, - ctx, - ) { - panic!("call_user_func_array() by-reference callback argument variable not found"); - } - args::push_arg_value(emitter, &PhpType::Int); - arg_types.push(PhpType::Int); - continue; - } - let has_default = sig.defaults.get(i).and_then(|d| d.as_ref()).is_some(); - let target_ty = callback_arg_target_ty(sig, i, has_default, &elem_ty); - if let Some(default_expr) = sig.defaults.get(i).and_then(|d| d.as_ref()) { - let load_label = ctx.next_label("cufa_ref_load_arg"); - let done_label = ctx.next_label("cufa_ref_arg_done"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", len_reg, i + 1)); // compare provided array length before binding a by-reference callback argument - emitter.instruction(&format!("b.ge {}", load_label)); // bind the provided element when this by-reference slot exists - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", len_reg, i + 1)); // compare provided array length before binding a by-reference callback argument - emitter.instruction(&format!("jge {}", load_label)); // bind the provided element when this by-reference slot exists - } - } - args::push_non_variable_ref_arg_address( - default_expr, - target_ty, - emitter, - ctx, - data, - ); - abi::emit_jump(emitter, &done_label); - emitter.label(&load_label); - args::load_array_element_to_result(emitter, &elem_ty, array_reg, 24 + i * elem_size); - push_loaded_indexed_array_ref_arg( - &elem_ty, - target_ty, - emitter, - ctx, - data, - ); - emitter.label(&done_label); - } else { - args::load_array_element_to_result(emitter, &elem_ty, array_reg, 24 + i * elem_size); - push_loaded_indexed_array_ref_arg( - &elem_ty, - target_ty, - emitter, - ctx, - data, - ); - } - arg_types.push(PhpType::Int); - continue; - } - let has_default = sig.defaults.get(i).and_then(|d| d.as_ref()).is_some(); - let target_ty = callback_arg_target_ty(sig, i, has_default, &elem_ty); - let pushed_ty = target_ty - .map(PhpType::codegen_repr) - .unwrap_or_else(|| elem_ty.codegen_repr()); - - if let Some(default_expr) = sig.defaults.get(i).and_then(|d| d.as_ref()) { - let load_label = ctx.next_label("cufa_load_arg"); - let done_label = ctx.next_label("cufa_arg_done"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", len_reg, i + 1)); // compare provided array length against required positional index - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", len_reg, i + 1)); // compare provided array length against required positional index - } - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("b.ge {}", load_label)); // load an explicit array element when present - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("jge {}", load_label)); // load an explicit array element when present - } - } - let _ = args::push_expr_arg(default_expr, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - emitter.label(&load_label); - args::load_array_element_to_result(emitter, &elem_ty, array_reg, 24 + i * elem_size); - let _ = push_loaded_indexed_array_value_arg(&elem_ty, target_ty, emitter, ctx, data); - emitter.label(&done_label); - } else { - args::load_array_element_to_result(emitter, &elem_ty, array_reg, 24 + i * elem_size); - let _ = push_loaded_indexed_array_value_arg(&elem_ty, target_ty, emitter, ctx, data); - } - arg_types.push(pushed_ty); - } - - if sig.variadic.is_some() { - let variadic_elem_ty = sig - .params - .get(visible_param_count.saturating_sub(1)) - .and_then(|(_, ty)| match ty { - PhpType::Array(elem) => Some((**elem).clone()), - _ => None, - }) - .unwrap_or_else(|| elem_ty.clone()); - let build_label = ctx.next_label("cufa_build_variadic"); - let done_label = ctx.next_label("cufa_variadic_done"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", len_reg, regular_param_count)); // compare provided array length against the fixed arity prefix - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", len_reg, regular_param_count)); // compare provided array length against the fixed arity prefix - } - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("b.gt {}", build_label)); // build a tail array only when extra positional elements exist - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("jg {}", build_label)); // build a tail array only when extra positional elements exist - } - } - emitter.comment("empty variadic array for call_user_func_array()"); - abi::emit_load_int_immediate(emitter, array_new_capacity_reg, 4); - abi::emit_load_int_immediate(emitter, array_new_elem_size_reg, variadic_elem_ty.stack_size() as i64); - abi::emit_call_label(emitter, "__rt_array_new"); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // push the empty variadic array onto the temporary arg stack - abi::emit_jump(emitter, &done_label); - - emitter.label(&build_label); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("sub {}, {}, #{}", tail_count_reg, len_reg, regular_param_count)); // compute the count of variadic tail arguments - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", tail_count_reg, len_reg)); // seed the tail count from the provided array length - emitter.instruction(&format!("sub {}, {}", tail_count_reg, regular_param_count)); // compute the count of variadic tail arguments - } - } - emitter.instruction(&format!("mov {}, {}", array_new_capacity_reg, tail_count_reg)); // pass the exact tail argument count as the initial capacity - abi::emit_load_int_immediate(emitter, array_new_elem_size_reg, variadic_elem_ty.stack_size() as i64); - abi::emit_call_label(emitter, "__rt_array_new"); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the variadic array pointer on the stack while filling it - emitter.instruction(&format!("mov {}, {}", peek_reg, abi::int_result_reg(emitter))); // copy the variadic array pointer into a scratch register for metadata stamping - emit_array_value_type_stamp(emitter, peek_reg, &variadic_elem_ty); // stamp the array header with the variadic element runtime tag - abi::emit_load_int_immediate(emitter, tail_index_reg, 0); - let loop_label = ctx.next_label("cufa_variadic_loop"); - let loop_done_label = ctx.next_label("cufa_variadic_loop_done"); - emitter.label(&loop_label); - emitter.instruction(&format!("cmp {}, {}", tail_index_reg, tail_count_reg)); // stop once every tail element has been copied - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("b.ge {}", loop_done_label)); // exit the fill loop when the tail array is complete - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("jge {}", loop_done_label)); // exit the fill loop when the tail array is complete - } - } - emitter.instruction(&format!("mov {}, {}", index_reg, tail_index_reg)); // copy the tail index into a scratch register - if regular_param_count > 0 { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #{}", index_reg, index_reg, regular_param_count)); // offset the tail index by the fixed-arity prefix length - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("add {}, {}", index_reg, regular_param_count)); // offset the tail index by the fixed-arity prefix length - } - } - } - emitter.instruction(&format!("mov {}, {}", data_reg, array_reg)); // start from the callback-argument array pointer before indexing into payload data - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", data_reg, data_reg)); // skip the fixed array header before indexing variadic source elements - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("add {}, 24", data_reg)); // skip the fixed array header before indexing variadic source elements - } - } - match elem_ty.codegen_repr() { - PhpType::Str => { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("lsl {}, {}, #4", offset_reg, index_reg)); // compute the 16-byte source slot offset for a string element - emitter.instruction(&format!("add {}, {}, {}", data_reg, data_reg, offset_reg)); // advance to the selected source string element - let (ptr_reg, len_reg_out) = abi::string_result_regs(emitter); - abi::emit_load_from_address(emitter, ptr_reg, data_reg, 0); - abi::emit_load_from_address(emitter, len_reg_out, data_reg, 8); - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", offset_reg, index_reg)); // copy the element index before scaling to bytes - emitter.instruction(&format!("shl {}, 4", offset_reg)); // compute the 16-byte source slot offset for a string element - emitter.instruction(&format!("add {}, {}", data_reg, offset_reg)); // advance to the selected source string element - let (ptr_reg, len_reg_out) = abi::string_result_regs(emitter); - abi::emit_load_from_address(emitter, ptr_reg, data_reg, 0); - abi::emit_load_from_address(emitter, len_reg_out, data_reg, 8); - } - } - } - PhpType::Float => { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("lsl {}, {}, #3", offset_reg, index_reg)); // compute the 8-byte source slot offset for a float element - emitter.instruction(&format!("add {}, {}, {}", data_reg, data_reg, offset_reg)); // advance to the selected source float element - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", offset_reg, index_reg)); // copy the element index before scaling to bytes - emitter.instruction(&format!("shl {}, 3", offset_reg)); // compute the 8-byte source slot offset for a float element - emitter.instruction(&format!("add {}, {}", data_reg, offset_reg)); // advance to the selected source float element - } - } - abi::emit_load_from_address(emitter, abi::float_result_reg(emitter), data_reg, 0); - } - PhpType::Void => {} - _ => { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("lsl {}, {}, #3", offset_reg, index_reg)); // compute the 8-byte source slot offset for a scalar or boxed element - emitter.instruction(&format!("add {}, {}, {}", data_reg, data_reg, offset_reg)); // advance to the selected source scalar element - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", offset_reg, index_reg)); // copy the element index before scaling to bytes - emitter.instruction(&format!("shl {}, 3", offset_reg)); // compute the 8-byte source slot offset for a scalar or boxed element - emitter.instruction(&format!("add {}, {}", data_reg, offset_reg)); // advance to the selected source scalar element - } - } - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), data_reg, 0); - } - } - let (stored_ty, boxed_to_mixed) = args::coerce_current_value_to_target( - emitter, - ctx, - data, - &elem_ty, - Some(&variadic_elem_ty), - ); - if !boxed_to_mixed { - abi::emit_incref_if_refcounted(emitter, &elem_ty.codegen_repr()); // retain refcounted tail elements copied into the new variadic array - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("ldr {}, [sp]", peek_reg)); // reload the variadic array pointer from the stack - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, QWORD PTR [rsp]", peek_reg)); // reload the variadic array pointer from the stack - } - } - match stored_ty { - PhpType::TaggedScalar => { - unreachable!("TaggedScalar must be narrowed or boxed before variadic array storage") - } - PhpType::Int - | PhpType::Bool - | PhpType::Resource(_) - | PhpType::Callable - | PhpType::Iterable - | PhpType::Mixed - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) - | PhpType::Union(_) | PhpType::Never => { - let dest_reg = len_store_reg; - emitter.instruction(&format!("mov {}, {}", dest_reg, peek_reg)); // point at the variadic array before skipping the header - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", dest_reg, dest_reg)); // point at the variadic array payload - emitter.instruction(&format!("lsl {}, {}, #3", offset_reg, tail_index_reg)); // compute the 8-byte destination slot offset - emitter.instruction(&format!("add {}, {}, {}", dest_reg, dest_reg, offset_reg)); // advance to the selected variadic destination slot - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("add {}, 24", dest_reg)); // point at the variadic array payload - emitter.instruction(&format!("mov {}, {}", offset_reg, tail_index_reg)); // copy the destination index before scaling - emitter.instruction(&format!("shl {}, 3", offset_reg)); // compute the 8-byte destination slot offset - emitter.instruction(&format!("add {}, {}", dest_reg, offset_reg)); // advance to the selected variadic destination slot - } - } - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), dest_reg, 0); - } - PhpType::Float => { - let dest_reg = len_store_reg; - emitter.instruction(&format!("mov {}, {}", dest_reg, peek_reg)); // point at the variadic array before skipping the header - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", dest_reg, dest_reg)); // point at the variadic array payload - emitter.instruction(&format!("lsl {}, {}, #3", offset_reg, tail_index_reg)); // compute the 8-byte destination slot offset - emitter.instruction(&format!("add {}, {}, {}", dest_reg, dest_reg, offset_reg)); // advance to the selected variadic destination slot - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("add {}, 24", dest_reg)); // point at the variadic array payload - emitter.instruction(&format!("mov {}, {}", offset_reg, tail_index_reg)); // copy the destination index before scaling - emitter.instruction(&format!("shl {}, 3", offset_reg)); // compute the 8-byte destination slot offset - emitter.instruction(&format!("add {}, {}", dest_reg, offset_reg)); // advance to the selected variadic destination slot - } - } - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), dest_reg, 0); - } - PhpType::Str => { - let dest_reg = len_store_reg; - let (ptr_reg, len_reg_out) = abi::string_result_regs(emitter); - emitter.instruction(&format!("mov {}, {}", dest_reg, peek_reg)); // point at the variadic array before skipping the header - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", dest_reg, dest_reg)); // point at the variadic array payload - emitter.instruction(&format!("lsl {}, {}, #4", offset_reg, tail_index_reg)); // compute the 16-byte destination slot offset - emitter.instruction(&format!("add {}, {}, {}", dest_reg, dest_reg, offset_reg)); // advance to the selected variadic destination slot - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("add {}, 24", dest_reg)); // point at the variadic array payload - emitter.instruction(&format!("mov {}, {}", offset_reg, tail_index_reg)); // copy the destination index before scaling - emitter.instruction(&format!("shl {}, 4", offset_reg)); // compute the 16-byte destination slot offset - emitter.instruction(&format!("add {}, {}", dest_reg, offset_reg)); // advance to the selected variadic destination slot - } - } - abi::emit_store_to_address(emitter, ptr_reg, dest_reg, 0); - abi::emit_store_to_address(emitter, len_reg_out, dest_reg, 8); - } - PhpType::Void => {} - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #1", tail_index_reg, tail_index_reg)); // advance to the next tail element - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("add {}, 1", tail_index_reg)); // advance to the next tail element - } - } - abi::emit_store_to_address(emitter, tail_index_reg, peek_reg, 0); // persist the updated variadic array length - 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))); - } - callback_env::push_captures_as_hidden_args(&captures, emitter, ctx, &mut arg_types); - - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); - - let ret_ty = sig.return_type.clone(); - - // -- call callback via the resolved address in x19 -- - if !concat_saved_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - abi::emit_call_reg(emitter, call_reg); - if concat_saved_before_args { - abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - abi::emit_release_temporary_stack(emitter, overflow_bytes); - } - - ret_ty -} - -/// Pushes a loaded indexed-array element as a by-reference callback argument. -fn push_loaded_indexed_array_ref_arg( - source_elem_ty: &PhpType, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if !matches!(source_elem_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - return args::push_current_result_ref_arg_address( - source_elem_ty, - target_ty, - emitter, - ctx, - data, - ); - } - - let special_label = ctx.next_label("cufa_invoker_ref_cell"); - let temp_label = ctx.next_label("cufa_invoker_ref_temp"); - let done_label = ctx.next_label("cufa_invoker_ref_done"); - let result_reg = abi::int_result_reg(emitter); - let tag_reg = abi::secondary_scratch_reg(emitter); - - abi::emit_load_from_address(emitter, tag_reg, result_reg, 0); - emit_branch_if_invoker_ref_cell_tag(tag_reg, &special_label, emitter); - abi::emit_jump(emitter, &temp_label); - - emitter.label(&special_label); - abi::emit_load_from_address(emitter, result_reg, result_reg, 8); - args::push_arg_value(emitter, &PhpType::Int); - abi::emit_jump(emitter, &done_label); - - emitter.label(&temp_label); - args::push_current_result_ref_arg_address(source_elem_ty, target_ty, emitter, ctx, data); - - emitter.label(&done_label); - PhpType::Int -} - -/// Pushes a loaded indexed-array element as a normal callback argument. -fn push_loaded_indexed_array_value_arg( - source_elem_ty: &PhpType, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if !matches!(source_elem_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - return args::push_loaded_array_element_arg(source_elem_ty, target_ty, emitter, ctx, data); - } - - let special_label = ctx.next_label("cufa_invoker_ref_value"); - let done_label = ctx.next_label("cufa_invoker_value_done"); - let result_reg = abi::int_result_reg(emitter); - let tag_reg = abi::secondary_scratch_reg(emitter); - - abi::emit_load_from_address(emitter, tag_reg, result_reg, 0); - emit_branch_if_invoker_ref_cell_tag(tag_reg, &special_label, emitter); - let ordinary_ty = args::push_loaded_array_element_arg(source_elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - - emitter.label(&special_label); - let ref_cell_ty = push_loaded_invoker_ref_cell_value_arg(target_ty, emitter, ctx, data); - - emitter.label(&done_label); - widen_callback_arg_type(&ordinary_ty, &ref_cell_ty) -} - -/// Pushes the value inside an invoker reference-cell marker for a non-ref parameter. -fn push_loaded_invoker_ref_cell_value_arg( - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_box_loaded_invoker_ref_cell_value_as_mixed(emitter, ctx); - let release_mixed_after_coerce = target_ty.is_some_and(|target_ty| { - !matches!(target_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) - && crate::codegen::expr::can_coerce_result_to_type(&PhpType::Mixed, target_ty) - }); - if release_mixed_after_coerce { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed ref-cell value while coercing it for a by-value parameter - } - let (pushed_ty, _boxed_to_mixed) = - args::coerce_current_value_to_target(emitter, ctx, data, &PhpType::Mixed, target_ty); - if release_mixed_after_coerce { - args::release_preserved_mixed_after_arg_coercion(emitter, &pushed_ty); - } - args::push_arg_value(emitter, &pushed_ty); - pushed_ty -} - -/// Boxes the value referenced by an invoker marker into an owned Mixed cell. -fn emit_box_loaded_invoker_ref_cell_value_as_mixed(emitter: &mut Emitter, ctx: &mut Context) { - let result_reg = abi::int_result_reg(emitter); - let ref_cell_reg = abi::symbol_scratch_reg(emitter); - let tag_reg = abi::secondary_scratch_reg(emitter); - let lo_reg = abi::tertiary_scratch_reg(emitter); - let hi_reg = match emitter.target.arch { - Arch::AArch64 => "x12", - Arch::X86_64 => "rdx", - }; - let string_hi_label = ctx.next_label("cufa_invoker_ref_string_hi"); - let box_label = ctx.next_label("cufa_invoker_ref_box"); - - abi::emit_load_from_address(emitter, ref_cell_reg, result_reg, 8); - abi::emit_load_from_address(emitter, tag_reg, result_reg, 16); - abi::emit_load_from_address(emitter, lo_reg, ref_cell_reg, 0); - abi::emit_load_int_immediate(emitter, hi_reg, 0); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #1", tag_reg)); // does the referenced value use a two-word string slot? - emitter.instruction(&format!("b.eq {}", string_hi_label)); // load the string length only for string reference cells - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, 1", tag_reg)); // does the referenced value use a two-word string slot? - emitter.instruction(&format!("je {}", string_hi_label)); // load the string length only for string reference cells - } - } - abi::emit_jump(emitter, &box_label); - - emitter.label(&string_hi_label); - abi::emit_load_from_address(emitter, hi_reg, ref_cell_reg, 8); - - emitter.label(&box_label); - crate::codegen::emit_box_runtime_payload_as_mixed(emitter, tag_reg, lo_reg, hi_reg); -} - -/// Branches when a boxed Mixed element represents an invoker reference-cell marker. -fn emit_branch_if_invoker_ref_cell_tag(tag_reg: &str, label: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", tag_reg, INVOKER_ARG_REF_CELL_TAG)); // check for an invoker-only by-reference argument marker - emitter.instruction(&format!("b.eq {}", label)); // use the original caller storage when this slot is a marker - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", tag_reg, INVOKER_ARG_REF_CELL_TAG)); // check for an invoker-only by-reference argument marker - emitter.instruction(&format!("je {}", label)); // use the original caller storage when this slot is a marker - } - } -} - -/// Emits assembly for a callback call whose argument container is boxed as `Mixed`. -#[allow(clippy::too_many_arguments)] -fn emit_loaded_mixed_array_callback_call( - array_source: LoadedArraySource, - call_reg: &str, - captures: &[(String, PhpType, bool)], - sig: &FunctionSig, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let (mixed_reg, tag_reg, payload_reg) = match emitter.target.arch { - Arch::AArch64 => ("x20", "x21", "x22"), - Arch::X86_64 => ("r13", "r14", "r15"), - }; - let indexed_label = ctx.next_label("cufa_mixed_indexed"); - let assoc_label = ctx.next_label("cufa_mixed_assoc"); - let done_label = ctx.next_label("cufa_mixed_done"); - let indexed_ty = PhpType::Array(Box::new(PhpType::Mixed)); - let assoc_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; - - emit_loaded_array_source_to_reg(array_source, mixed_reg, emitter); - abi::emit_load_from_address(emitter, tag_reg, mixed_reg, 0); - abi::emit_load_from_address(emitter, payload_reg, mixed_reg, 8); - abi::emit_push_reg(emitter, payload_reg); // preserve the unboxed argument container while branching by Mixed tag - emit_branch_if_mixed_arg_tag( - tag_reg, - crate::codegen::runtime_value_tag(&indexed_ty), - &indexed_label, - emitter, - ); - emit_branch_if_mixed_arg_tag( - tag_reg, - crate::codegen::runtime_value_tag(&assoc_ty), - &assoc_label, - emitter, - ); - emit_call_user_func_array_invalid_mixed_args_abort(emitter, data); - - emitter.label(&indexed_label); - emit_loaded_array_callback_call( - LoadedArraySource::TemporaryStackSlot(0), - &indexed_ty, - None, - call_reg, - captures, - sig, - concat_saved_before_args, - emitter, - ctx, - data, - ); - abi::emit_jump(emitter, &done_label); - - emitter.label(&assoc_label); - emit_loaded_assoc_array_callback_call( - LoadedArraySource::TemporaryStackSlot(0), - &assoc_ty, - call_reg, - captures, - sig, - concat_saved_before_args, - emitter, - ctx, - data, - ); - abi::emit_jump(emitter, &done_label); - - emitter.label(&done_label); - abi::emit_release_temporary_stack(emitter, 16); // drop the borrowed unboxed argument-container pointer - sig.return_type.clone() -} - -/// Branches to `label` when a boxed invoker argument carries `expected_tag`. -pub(crate) fn emit_branch_if_mixed_arg_tag( - tag_reg: &str, - expected_tag: u8, - label: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", tag_reg, expected_tag)); // check the runtime tag of the boxed invoker argument container - emitter.instruction(&format!("b.eq {}", label)); // dispatch to the handler for this argument-container shape - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", tag_reg, expected_tag)); // check the runtime tag of the boxed invoker argument container - emitter.instruction(&format!("je {}", label)); // dispatch to the handler for this argument-container shape - } - } -} - -/// Provides the Callback arg target ty helper used by the call user func array module. -fn callback_arg_target_ty<'a>( - sig: &'a FunctionSig, - index: usize, - has_default: bool, - source_elem_ty: &PhpType, -) -> Option<&'a PhpType> { - if args::declared_target_ty(Some(sig), index).is_some() - || has_default - || matches!(source_elem_ty.codegen_repr(), PhpType::Mixed) - { - sig.params.get(index).map(|(_, ty)| ty) - } else { - None - } -} - -/// Emits assembly for indexed required arg count check. -fn emit_indexed_required_arg_count_check( - sig: &FunctionSig, - regular_param_count: usize, - len_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let required_count = (0..regular_param_count) - .filter(|idx| sig.defaults.get(*idx).and_then(|default| default.as_ref()).is_none()) - .map(|idx| idx + 1) - .max() - .unwrap_or(0); - if required_count == 0 { - return; - } - let ok_label = ctx.next_label("cufa_indexed_required_ok"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", len_reg, required_count)); // check that the dynamic indexed arg array contains all required callback parameters - emitter.instruction(&format!("b.ge {}", ok_label)); // continue when every required callback parameter is present - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", len_reg, required_count)); // check that the dynamic indexed arg array contains all required callback parameters - emitter.instruction(&format!("jge {}", ok_label)); // continue when every required callback parameter is present - } - } - emit_call_user_func_array_missing_arg_abort(emitter, data); - emitter.label(&ok_label); -} - -/// Emits assembly for loaded assoc array callback call. -fn emit_loaded_assoc_array_callback_call( - array_source: LoadedArraySource, - arr_ty: &PhpType, - call_reg: &str, - captures: &[(String, PhpType, bool)], - sig: &FunctionSig, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let hash_reg = match emitter.target.arch { - Arch::AArch64 => "x20", - Arch::X86_64 => "r13", - }; - let elem_ty = match arr_ty { - PhpType::AssocArray { value, .. } => *value.clone(), - _ => PhpType::Int, - }; - - emit_loaded_array_source_to_reg(array_source, hash_reg, emitter); - - let visible_param_count = sig.params.len(); - let regular_param_count = if sig.variadic.is_some() { - visible_param_count.saturating_sub(1) - } else { - visible_param_count - }; - let mut arg_types = Vec::new(); - - for i in 0..regular_param_count { - let has_default = sig.defaults.get(i).and_then(|d| d.as_ref()).is_some(); - let target_ty = callback_arg_target_ty(sig, i, has_default, &elem_ty); - let param_name = sig.params.get(i).map(|(name, _)| name.as_str()); - emitter.comment("lookup call_user_func_array() named argument"); - args::emit_hash_lookup_for_param_or_index( - hash_reg, - param_name, - i, - emitter, - ctx, - data, - ); - - let is_ref = sig.ref_params.get(i).copied().unwrap_or(false); - if is_ref { - if let Some(default_expr) = sig.defaults.get(i).and_then(|d| d.as_ref()) { - let use_default = ctx.next_label("cufa_assoc_ref_default"); - let done = ctx.next_label("cufa_assoc_ref_done"); - abi::emit_branch_if_int_result_zero(emitter, &use_default); - args::push_loaded_hash_value_ref_arg(&elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&use_default); - args::push_non_variable_ref_arg_address( - default_expr, - target_ty, - emitter, - ctx, - data, - ); - emitter.label(&done); - } else { - let missing = ctx.next_label("cufa_assoc_ref_missing"); - let done = ctx.next_label("cufa_assoc_ref_done"); - abi::emit_branch_if_int_result_zero(emitter, &missing); - args::push_loaded_hash_value_ref_arg(&elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&missing); - emit_call_user_func_array_missing_arg_abort(emitter, data); - emitter.label(&done); - } - arg_types.push(PhpType::Int); - continue; - } - - let pushed_ty = if let Some(default_expr) = sig.defaults.get(i).and_then(|d| d.as_ref()) { - let use_default = ctx.next_label("cufa_assoc_default"); - let done = ctx.next_label("cufa_assoc_done"); - abi::emit_branch_if_int_result_zero(emitter, &use_default); - let loaded_ty = args::push_loaded_hash_value_arg(&elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&use_default); - let default_ty = args::push_expr_arg(default_expr, target_ty, emitter, ctx, data); - emitter.label(&done); - widen_callback_arg_type(&loaded_ty, &default_ty) - } else { - let missing = ctx.next_label("cufa_assoc_missing"); - let done = ctx.next_label("cufa_assoc_done"); - abi::emit_branch_if_int_result_zero(emitter, &missing); - let loaded_ty = args::push_loaded_hash_value_arg(&elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&missing); - emit_call_user_func_array_missing_arg_abort(emitter, data); - emitter.label(&done); - loaded_ty - }; - arg_types.push(pushed_ty); - } - - if sig.variadic.is_some() { - let variadic_ty = args::emit_loaded_assoc_variadic_array_arg( - hash_reg, - &elem_ty, - sig, - regular_param_count, - regular_param_count, - "build associative variadic array for callback", - emitter, - ctx, - data, - ); - arg_types.push(variadic_ty); - } - - callback_env::push_captures_as_hidden_args(captures, emitter, ctx, &mut arg_types); - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); - let ret_ty = sig.return_type.clone(); - - if !concat_saved_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - abi::emit_call_reg(emitter, call_reg); - if concat_saved_before_args { - abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - abi::emit_release_temporary_stack(emitter, overflow_bytes); - } - - ret_ty -} - -/// Emits assembly for loaded array unknown callback call. -pub(crate) fn emit_loaded_array_unknown_callback_call( - array_source: LoadedArraySource, - arr_ty: &PhpType, - call_reg: &str, - captures: &[(String, PhpType, bool)], - descriptor_source: Option, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if matches!(arr_ty, PhpType::AssocArray { .. }) { - return emit_loaded_assoc_array_unknown_callback_call( - array_source, - arr_ty, - call_reg, - captures, - descriptor_source, - concat_saved_before_args, - emitter, - ctx, - data, - ); - } - - let cases = callable_dispatch::runtime_callable_cases(ctx, data, captures, Some(arr_ty)); - if !cases.is_empty() { - return emit_loaded_indexed_array_unknown_callback_call( - array_source, - arr_ty, - call_reg, - captures, - &cases, - descriptor_source, - concat_saved_before_args, - emitter, - ctx, - data, - ); - } - - emit_loaded_array_unknown_callback_call_by_arity( - array_source, - arr_ty, - call_reg, - captures, - concat_saved_before_args, - emitter, - ctx, - data, - ) -} - -/// Emits assembly for loaded array string callback call. -#[allow(clippy::too_many_arguments)] -pub(crate) fn emit_loaded_array_string_callback_call( - array_source: LoadedArraySource, - arr_ty: &PhpType, - string_ptr_offset: usize, - string_len_offset: usize, - call_reg: &str, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let cases = callable_dispatch::runtime_callable_cases(ctx, data, &[], Some(arr_ty)); - let done_label = ctx.next_label("cufa_string_done"); - let pushed_array = matches!(array_source, LoadedArraySource::Result); - let (array_source, string_ptr_offset, string_len_offset) = match array_source { - LoadedArraySource::Result => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the callback-argument array for runtime string-name dispatch - ( - LoadedArraySource::TemporaryStackSlot(0), - string_ptr_offset + 16, - string_len_offset + 16, - ) - } - LoadedArraySource::TemporaryStackSlot(offset) => ( - LoadedArraySource::TemporaryStackSlot(offset), - string_ptr_offset, - string_len_offset, - ), - LoadedArraySource::ArgumentRegister(index) => ( - LoadedArraySource::ArgumentRegister(index), - string_ptr_offset, - string_len_offset, - ), - }; - let selector = RuntimeCallableSelector::StringNameStack { - ptr_offset: string_ptr_offset, - len_offset: string_len_offset, - call_reg, - }; - - for case in &cases { - let next_case = ctx.next_label("cufa_string_next"); - callable_dispatch::emit_branch_if_callable_case_mismatch( - &selector, - case, - &next_case, - emitter, - ctx, - data, - ); - emit_call_descriptor_array_invoker( - array_source, - arr_ty, - call_reg, - concat_saved_before_args, - emitter, - ctx, - data, - ); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - - emit_dynamic_string_callback_abort(emitter, data); - emitter.label(&done_label); - if pushed_array { - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callback-argument array - } - PhpType::Mixed -} - -/// Calls the uniform invoker stored in the matched callable descriptor. -pub(crate) fn emit_call_descriptor_array_invoker( - array_source: LoadedArraySource, - arr_ty: &PhpType, - descriptor_reg: &str, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_call_descriptor_array_invoker_with_label( - array_source, - arr_ty, - descriptor_reg, - None, - concat_saved_before_args, - emitter, - ctx, - data, - ); -} - -/// Calls a descriptor invoker, optionally overriding the invoker slot with a case label. -#[allow(clippy::too_many_arguments)] -fn emit_call_descriptor_array_invoker_with_label( - array_source: LoadedArraySource, - arr_ty: &PhpType, - descriptor_reg: &str, - invoker_label: Option<&str>, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let descriptor_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let invoker_reg = abi::symbol_scratch_reg(emitter); - let missing_label = ctx.next_label("cufa_descriptor_invoker_missing"); - let ready_label = ctx.next_label("cufa_descriptor_invoker_ready"); - - abi::emit_push_reg(emitter, descriptor_reg); // preserve the callable descriptor while normalizing the invoker argument container - let array_source = array_source_after_descriptor_push(array_source); - let normalized_arg_ty = - emit_normalized_invoker_arg_mixed(array_source, arr_ty, array_arg_reg, emitter, ctx, data); - abi::emit_push_reg(emitter, array_arg_reg); // preserve the temporary boxed Mixed argument container for release after invocation - abi::emit_load_temporary_stack_slot(emitter, descriptor_arg_reg, 16); - if !concat_saved_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - if let Some(invoker_label) = invoker_label { - abi::emit_symbol_address(emitter, invoker_reg, invoker_label); - } else { - callable_descriptor::emit_load_invoker_from_descriptor(emitter, invoker_reg, descriptor_arg_reg); - } - emit_branch_if_descriptor_invoker_missing(invoker_reg, &missing_label, &ready_label, emitter); - - emitter.label(&missing_label); - emit_descriptor_invoker_missing_abort(emitter, data); - - emitter.label(&ready_label); - abi::emit_call_reg(emitter, invoker_reg); - if concat_saved_before_args { - emit_release_normalized_invoker_arg_mixed(&normalized_arg_ty, emitter); - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callable descriptor after invocation - crate::codegen::expr::restore_concat_offset_after_nested_call( - emitter, - ctx, - &PhpType::Mixed, - ); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call( - emitter, - ctx, - &PhpType::Mixed, - ); - emit_release_normalized_invoker_arg_mixed(&normalized_arg_ty, emitter); - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callable descriptor after invocation - } -} - -/// Loads the descriptor that should feed a matched case invoker. -fn emit_case_descriptor_for_invoker<'a>( - case: &'a RuntimeCallableCase, - descriptor_source: Option, - descriptor_reg: &str, - emitter: &mut Emitter, -) -> Option> { - if !case.has_invoker { - return None; - } - if let Some(source) = descriptor_source { - emit_loaded_descriptor_source_to_reg(source, descriptor_reg, emitter); - return Some(case.invoker_label.as_deref()); - } - if case.captures.is_empty() { - abi::emit_symbol_address(emitter, descriptor_reg, &case.descriptor_label); - return Some(None); - } - None -} - -/// Materializes the descriptor invoker argument as a boxed Mixed container. -fn emit_normalized_invoker_arg_mixed( - array_source: LoadedArraySource, - arr_ty: &PhpType, - dest_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_loaded_array_source_to_reg(array_source, dest_reg, emitter); - match arr_ty { - PhpType::Array(elem_ty) => { - emit_clone_indexed_array_for_invoker(dest_reg, elem_ty, emitter); - let normalized_ty = PhpType::Array(Box::new(PhpType::Mixed)); - emit_box_invoker_arg_clone_as_mixed(dest_reg, &normalized_ty, emitter); - PhpType::Mixed - } - PhpType::AssocArray { value, .. } => { - emit_clone_assoc_array_for_invoker_with_value_type(dest_reg, value, emitter); - let normalized_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; - emit_box_invoker_arg_clone_as_mixed(dest_reg, &normalized_ty, emitter); - PhpType::Mixed - } - PhpType::Mixed | PhpType::Union(_) => { - emit_clone_runtime_mixed_invoker_arg_as_mixed(dest_reg, emitter, ctx, data); - PhpType::Mixed - } - _ => arr_ty.codegen_repr(), - } -} - -/// Clones a boxed runtime Mixed argument container into a normalized boxed Mixed container. -pub(crate) fn emit_clone_runtime_mixed_invoker_arg_as_mixed( - dest_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let tag_reg = abi::secondary_scratch_reg(emitter); - let payload_reg = abi::tertiary_scratch_reg(emitter); - let indexed_label = ctx.next_label("cufa_normalize_mixed_indexed"); - let assoc_label = ctx.next_label("cufa_normalize_mixed_assoc"); - let done_label = ctx.next_label("cufa_normalize_mixed_done"); - let indexed_ty = PhpType::Array(Box::new(PhpType::Mixed)); - let assoc_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; - - abi::emit_load_from_address(emitter, tag_reg, dest_reg, 0); - abi::emit_load_from_address(emitter, payload_reg, dest_reg, 8); - abi::emit_push_reg(emitter, payload_reg); // preserve the unboxed runtime argument container while normalizing by Mixed tag - emit_branch_if_mixed_arg_tag( - tag_reg, - crate::codegen::runtime_value_tag(&indexed_ty), - &indexed_label, - emitter, - ); - emit_branch_if_mixed_arg_tag( - tag_reg, - crate::codegen::runtime_value_tag(&assoc_ty), - &assoc_label, - emitter, - ); - emit_call_user_func_array_invalid_mixed_args_abort(emitter, data); - - emitter.label(&indexed_label); - abi::emit_load_temporary_stack_slot(emitter, dest_reg, 0); - abi::emit_release_temporary_stack(emitter, 16); // discard the borrowed indexed-array pointer after loading it for cloning - emit_clone_indexed_array_for_invoker_with_runtime_tag(dest_reg, emitter); - emit_box_invoker_arg_clone_as_mixed(dest_reg, &indexed_ty, emitter); - abi::emit_jump(emitter, &done_label); - - emitter.label(&assoc_label); - abi::emit_load_temporary_stack_slot(emitter, dest_reg, 0); - abi::emit_release_temporary_stack(emitter, 16); // discard the borrowed hash pointer after loading it for cloning - emit_clone_assoc_array_for_invoker(dest_reg, emitter); - emit_box_invoker_arg_clone_as_mixed(dest_reg, &assoc_ty, emitter); - - emitter.label(&done_label); -} - -/// Clones and converts an indexed callback argument array to boxed Mixed slots. -pub(crate) fn emit_clone_indexed_array_for_invoker( - dest_reg: &str, - elem_ty: &PhpType, - emitter: &mut Emitter, -) { - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let tag_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let result_reg = abi::int_result_reg(emitter); - if array_arg_reg != dest_reg { - emitter.instruction(&format!("mov {}, {}", array_arg_reg, dest_reg)); // pass the callback-argument array to the clone helper without mutating caller storage - } - abi::emit_call_label(emitter, "__rt_array_clone_shallow"); - if array_arg_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", array_arg_reg, result_reg)); // pass the cloned argument array to the Mixed-slot conversion helper - } - abi::emit_load_int_immediate( - emitter, - tag_arg_reg, - crate::codegen::runtime_value_tag(&elem_ty.codegen_repr()) as i64, - ); - abi::emit_call_label(emitter, "__rt_array_to_mixed"); - if dest_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", dest_reg, result_reg)); // keep the normalized Mixed argument array in the invoker ABI register - } -} - -/// Clones and converts a runtime-typed indexed callback argument array to boxed Mixed slots. -pub(crate) fn emit_clone_indexed_array_for_invoker_with_runtime_tag( - dest_reg: &str, - emitter: &mut Emitter, -) { - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let tag_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let result_reg = abi::int_result_reg(emitter); - if array_arg_reg != dest_reg { - emitter.instruction(&format!("mov {}, {}", array_arg_reg, dest_reg)); // pass the runtime-typed callback array to the clone helper without mutating caller storage - } - abi::emit_call_label(emitter, "__rt_array_clone_shallow"); - if array_arg_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", array_arg_reg, result_reg)); // pass the cloned runtime-typed array to the Mixed-slot conversion helper - } - emit_load_indexed_array_runtime_value_type_tag(array_arg_reg, tag_arg_reg, emitter); - abi::emit_call_label(emitter, "__rt_array_to_mixed"); - if dest_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", dest_reg, result_reg)); // keep the normalized Mixed argument array in the invoker ABI register - } -} - -/// Loads an indexed array's runtime value-type tag from its packed heap header. -fn emit_load_indexed_array_runtime_value_type_tag( - array_reg: &str, - tag_reg: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr {}, [{}, #-8]", tag_reg, array_reg)); // load the packed indexed-array metadata before Mixed-slot conversion - emitter.instruction(&format!("lsr {}, {}, #8", tag_reg, tag_reg)); // move the indexed-array value_type tag into the low bits - emitter.instruction(&format!("and {}, {}, #0x7f", tag_reg, tag_reg)); // isolate the runtime indexed-array value_type tag - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, QWORD PTR [{} - 8]", tag_reg, array_reg)); // load the packed indexed-array metadata before Mixed-slot conversion - emitter.instruction(&format!("shr {}, 8", tag_reg)); // move the indexed-array value_type tag into the low bits - emitter.instruction(&format!("and {}, 0x7f", tag_reg)); // isolate the runtime indexed-array value_type tag - } - } -} - -/// Clones and converts an associative callback argument array to boxed Mixed entries. -pub(crate) fn emit_clone_assoc_array_for_invoker(dest_reg: &str, emitter: &mut Emitter) { - emit_clone_assoc_array_for_invoker_with_value_type(dest_reg, &PhpType::Int, emitter); -} - -/// Clones an associative callback argument array and boxes entries when needed. -pub(crate) fn emit_clone_assoc_array_for_invoker_with_value_type( - dest_reg: &str, - value_ty: &PhpType, - emitter: &mut Emitter, -) { - let hash_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let result_reg = abi::int_result_reg(emitter); - if hash_arg_reg != dest_reg { - emitter.instruction(&format!("mov {}, {}", hash_arg_reg, dest_reg)); // pass the callback-argument hash to the clone helper without mutating caller storage - } - abi::emit_call_label(emitter, "__rt_hash_clone_shallow"); - if hash_arg_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", hash_arg_reg, result_reg)); // pass the cloned argument hash to the Mixed-entry conversion helper - } - if !matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_hash_to_mixed"); - } - if dest_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", dest_reg, result_reg)); // keep the normalized Mixed argument hash in the invoker ABI register - } -} - -/// Boxes the normalized argument clone as `Mixed` and releases the caller-side clone owner. -pub(crate) fn emit_box_invoker_arg_clone_as_mixed(dest_reg: &str, container_ty: &PhpType, emitter: &mut Emitter) { - let tag_reg = abi::secondary_scratch_reg(emitter); - let zero_reg = abi::tertiary_scratch_reg(emitter); - - abi::emit_push_reg(emitter, dest_reg); // preserve the cloned argument container while Mixed boxing retains it - abi::emit_load_int_immediate( - emitter, - tag_reg, - crate::codegen::runtime_value_tag(container_ty) as i64, - ); - abi::emit_load_int_immediate(emitter, zero_reg, 0); - crate::codegen::emit_box_runtime_payload_as_mixed(emitter, tag_reg, dest_reg, zero_reg); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed Mixed argument while dropping the clone owner - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, container_ty); - abi::emit_pop_reg(emitter, dest_reg); // move the boxed Mixed argument into the invoker ABI register - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved clone slot after ownership transfer -} - -/// Releases the temporary boxed Mixed argument container while preserving the Mixed call result. -fn emit_release_normalized_invoker_arg_mixed(array_ty: &PhpType, emitter: &mut Emitter) { - abi::emit_push_result_value(emitter, &PhpType::Mixed); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, array_ty); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_release_temporary_stack(emitter, 16); -} - -/// Branches to the abort path when a descriptor lacks an invoker pointer. -fn emit_branch_if_descriptor_invoker_missing( - invoker_reg: &str, - missing_label: &str, - ready_label: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz {}, {}", invoker_reg, missing_label)); // abort when the descriptor has no uniform invoker - emitter.instruction(&format!("b {}", ready_label)); // continue when a descriptor invoker is available - } - Arch::X86_64 => { - emitter.instruction(&format!("test {}, {}", invoker_reg, invoker_reg)); // abort when the descriptor has no uniform invoker - emitter.instruction(&format!("je {}", missing_label)); // branch to the fatal descriptor-invoker diagnostic - emitter.instruction(&format!("jmp {}", ready_label)); // continue when a descriptor invoker is available - } - } -} - -/// Emits the fatal diagnostic for descriptors without a generated invoker. -fn emit_descriptor_invoker_missing_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: callable descriptor does not provide a runtime invoker\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the descriptor-invoker diagnostic to stderr - abi::emit_symbol_address(emitter, "x1", &message_label); - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the descriptor-invoker diagnostic byte length to write() - emitter.syscall(4); - abi::emit_exit(emitter, 1); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the descriptor-invoker diagnostic to stderr - abi::emit_symbol_address(emitter, "rsi", &message_label); - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the descriptor-invoker diagnostic byte length to write() - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal descriptor-invoker diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Emits assembly for loaded indexed array unknown callback call. -#[allow(clippy::too_many_arguments)] -fn emit_loaded_indexed_array_unknown_callback_call( - array_source: LoadedArraySource, - arr_ty: &PhpType, - call_reg: &str, - captures: &[(String, PhpType, bool)], - cases: &[RuntimeCallableCase], - descriptor_source: Option, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let done_label = ctx.next_label("cufa_unknown_indexed_done"); - let pushed_array = matches!(array_source, LoadedArraySource::Result); - let array_source = match array_source { - LoadedArraySource::Result => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the indexed callback-argument array for runtime signature dispatch - LoadedArraySource::TemporaryStackSlot(0) - } - LoadedArraySource::TemporaryStackSlot(offset) => LoadedArraySource::TemporaryStackSlot(offset), - LoadedArraySource::ArgumentRegister(index) => LoadedArraySource::ArgumentRegister(index), - }; - let descriptor_source = descriptor_source_after_array_push(descriptor_source, pushed_array); - - let selector = RuntimeCallableSelector::Address(call_reg); - for case in cases { - let next_case = ctx.next_label("cufa_unknown_indexed_next"); - callable_dispatch::emit_branch_if_callable_case_mismatch( - &selector, - case, - &next_case, - emitter, - ctx, - data, - ); - if let Some(invoker_label) = - emit_case_descriptor_for_invoker(case, descriptor_source, call_reg, emitter) - { - emit_call_descriptor_array_invoker_with_label( - array_source, - arr_ty, - call_reg, - invoker_label, - concat_saved_before_args, - emitter, - ctx, - data, - ); - } else { - let case_ret_ty = emit_loaded_array_callback_call( - array_source, - arr_ty, - None, - call_reg, - &case.captures, - &case.sig, - concat_saved_before_args, - emitter, - ctx, - data, - ); - crate::codegen::emit_box_current_value_as_mixed(emitter, &case_ret_ty.codegen_repr()); - } - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - - let fallback_ret_ty = emit_loaded_array_unknown_callback_call_by_arity( - array_source, - arr_ty, - call_reg, - captures, - concat_saved_before_args, - emitter, - ctx, - data, - ); - crate::codegen::emit_box_current_value_as_mixed(emitter, &fallback_ret_ty.codegen_repr()); - - emitter.label(&done_label); - if pushed_array { - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved indexed callback-argument array - } - PhpType::Mixed -} - -/// Emits assembly for loaded array unknown callback call by arity. -#[allow(clippy::too_many_arguments)] -fn emit_loaded_array_unknown_callback_call_by_arity( - array_source: LoadedArraySource, - arr_ty: &PhpType, - call_reg: &str, - captures: &[(String, PhpType, bool)], - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if captures.is_empty() { - return emit_loaded_array_unknown_callback_call_dynamic( - array_source, - arr_ty, - call_reg, - concat_saved_before_args, - emitter, - ctx, - ); - } - - let (array_reg, len_reg) = match emitter.target.arch { - Arch::AArch64 => ("x20", "x21"), - Arch::X86_64 => ("r13", "r14"), - }; - let elem_ty = match arr_ty { - PhpType::Array(elem_ty) => *elem_ty.clone(), - _ => PhpType::Int, - }; - let elem_size = args::array_element_stride(&elem_ty); - - emit_loaded_array_source_to_reg(array_source, array_reg, emitter); - abi::emit_load_from_address(emitter, len_reg, array_reg, 0); // load callback-argument array length for unknown signature dispatch - - let done_label = ctx.next_label("cufa_unknown_done"); - let register_arg_capacity = unknown_callback_register_arg_capacity(emitter.target, &elem_ty); - let case_labels: Vec = (0..=register_arg_capacity) - .map(|_| ctx.next_label("cufa_unknown_arity")) - .collect(); - for (arg_count, label) in case_labels.iter().enumerate() { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", len_reg, arg_count)); // compare runtime callback-argument count against this unknown-signature case - emitter.instruction(&format!("b.eq {}", label)); // dispatch to the call shape matching the runtime argument count - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", len_reg, arg_count)); // compare runtime callback-argument count against this unknown-signature case - emitter.instruction(&format!("je {}", label)); // dispatch to the call shape matching the runtime argument count - } - } - } - emit_unknown_captured_callback_overflow_dynamic( - array_reg, - len_reg, - &elem_ty, - register_arg_capacity, - call_reg, - captures, - concat_saved_before_args, - &done_label, - emitter, - ctx, - ); - - for (arg_count, label) in case_labels.iter().enumerate() { - emitter.label(label); - emit_unknown_callback_case( - arg_count, - &elem_ty, - elem_size, - array_reg, - call_reg, - captures, - concat_saved_before_args, - &done_label, - emitter, - ctx, - data, - ); - } - - emitter.label(&done_label); - PhpType::Int -} - -/// Emits assembly for loaded assoc array unknown callback call. -fn emit_loaded_assoc_array_unknown_callback_call( - array_source: LoadedArraySource, - arr_ty: &PhpType, - call_reg: &str, - captures: &[(String, PhpType, bool)], - descriptor_source: Option, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let done_label = ctx.next_label("cufa_unknown_assoc_done"); - let cases = callable_dispatch::runtime_callable_cases(ctx, data, captures, Some(arr_ty)); - let pushed_array = matches!(array_source, LoadedArraySource::Result); - let array_source = match array_source { - LoadedArraySource::Result => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the associative callback-argument hash for runtime signature dispatch - LoadedArraySource::TemporaryStackSlot(0) - } - LoadedArraySource::TemporaryStackSlot(offset) => LoadedArraySource::TemporaryStackSlot(offset), - LoadedArraySource::ArgumentRegister(index) => LoadedArraySource::ArgumentRegister(index), - }; - let descriptor_source = descriptor_source_after_array_push(descriptor_source, pushed_array); - - let selector = RuntimeCallableSelector::Address(call_reg); - for case in &cases { - let next_case = ctx.next_label("cufa_unknown_assoc_next"); - callable_dispatch::emit_branch_if_callable_case_mismatch( - &selector, - case, - &next_case, - emitter, - ctx, - data, - ); - if let Some(invoker_label) = - emit_case_descriptor_for_invoker(case, descriptor_source, call_reg, emitter) - { - emit_call_descriptor_array_invoker_with_label( - array_source, - arr_ty, - call_reg, - invoker_label, - concat_saved_before_args, - emitter, - ctx, - data, - ); - } else { - let case_ret_ty = emit_loaded_assoc_array_callback_call( - array_source, - arr_ty, - call_reg, - &case.captures, - &case.sig, - concat_saved_before_args, - emitter, - ctx, - data, - ); - crate::codegen::emit_box_current_value_as_mixed(emitter, &case_ret_ty.codegen_repr()); - } - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - - emit_call_user_func_array_unknown_assoc_abort(emitter, data); - emitter.label(&done_label); - if pushed_array { - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved associative callback-argument hash - } - PhpType::Mixed -} - -/// Provides the Unknown callback register arg capacity helper used by the call user func array module. -fn unknown_callback_register_arg_capacity(target: crate::codegen::platform::Target, elem_ty: &PhpType) -> usize { - match elem_ty.codegen_repr() { - PhpType::Float => 8, - PhpType::Str => match target.arch { - Arch::AArch64 => 4, - Arch::X86_64 => 3, - }, - PhpType::Void | PhpType::Never => 0, - _ => match target.arch { - Arch::AArch64 => 8, - Arch::X86_64 => 6, - }, - } -} - -/// Emits assembly for unknown captured callback overflow dynamic. -#[allow(clippy::too_many_arguments)] -fn emit_unknown_captured_callback_overflow_dynamic( - array_reg: &str, - len_reg: &str, - elem_ty: &PhpType, - register_arg_capacity: usize, - call_reg: &str, - captures: &[(String, PhpType, bool)], - concat_saved_before_args: bool, - done_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let (overflow_count_reg, overflow_bytes_reg) = match emitter.target.arch { - Arch::AArch64 => ("x22", "x23"), - Arch::X86_64 => ("r15", "rbx"), - }; - let capture_assignments = unknown_dynamic_capture_assignments( - emitter.target, - elem_ty, - register_arg_capacity, - captures, - ); - let capture_stack_bytes = capture_assignments - .iter() - .filter(|(_, assignment)| !assignment.in_register()) - .count() - * 16; - let visible_register_temp_bytes = register_arg_capacity * 16; - let capture_register_temp_bytes = capture_assignments - .iter() - .filter(|(_, assignment)| assignment.in_register()) - .count() - * 16; - let register_temp_bytes = visible_register_temp_bytes + capture_register_temp_bytes; - - emit_unknown_dynamic_overflow_size( - len_reg, - overflow_count_reg, - overflow_bytes_reg, - register_arg_capacity, - emitter, - ctx, - ); - if capture_stack_bytes > 0 { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #{}", overflow_bytes_reg, overflow_bytes_reg, capture_stack_bytes)); // reserve trailing stack slots for captured callback arguments - } - Arch::X86_64 => { - emitter.instruction(&format!("add {}, {}", overflow_bytes_reg, capture_stack_bytes)); // reserve trailing stack slots for captured callback arguments - } - } - } - emit_dynamic_stack_adjust(emitter, overflow_bytes_reg, true); - abi::emit_reserve_temporary_stack(emitter, register_temp_bytes); - - emit_unknown_dynamic_register_arg_temps( - array_reg, - len_reg, - elem_ty, - register_arg_capacity, - emitter, - ctx, - ); - emit_unknown_dynamic_stack_args( - array_reg, - overflow_count_reg, - elem_ty, - register_arg_capacity, - register_temp_bytes, - emitter, - ctx, - ); - let capture_register_temps = emit_unknown_dynamic_capture_args( - captures, - &capture_assignments, - overflow_count_reg, - register_temp_bytes, - visible_register_temp_bytes, - emitter, - ctx, - ); - emit_unknown_dynamic_load_register_args( - len_reg, - elem_ty, - register_arg_capacity, - emitter, - ctx, - ); - emit_unknown_dynamic_load_capture_register_args(&capture_register_temps, emitter); - - abi::emit_release_temporary_stack(emitter, register_temp_bytes); - let ret_ty = PhpType::Int; - if !concat_saved_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - abi::emit_call_reg(emitter, call_reg); - if concat_saved_before_args { - emit_dynamic_stack_adjust(emitter, overflow_bytes_reg, false); - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - emit_dynamic_stack_adjust(emitter, overflow_bytes_reg, false); - } - abi::emit_jump(emitter, done_label); -} - -/// Provides the Unknown dynamic capture assignments helper used by the call user func array module. -fn unknown_dynamic_capture_assignments( - target: crate::codegen::platform::Target, - elem_ty: &PhpType, - register_arg_capacity: usize, - captures: &[(String, PhpType, bool)], -) -> Vec<(PhpType, abi::OutgoingArgAssignment)> { - let mut arg_types = vec![elem_ty.codegen_repr(); register_arg_capacity]; - let capture_types: Vec = captures - .iter() - .map(|(_, ty, by_ref)| if *by_ref { PhpType::Int } else { ty.codegen_repr() }) - .collect(); - arg_types.extend(capture_types.iter().cloned()); - abi::build_outgoing_arg_assignments_for_target(target, &arg_types, 0) - .into_iter() - .skip(register_arg_capacity) - .zip(capture_types) - .map(|(assignment, ty)| (ty, assignment)) - .collect() -} - -/// Emits assembly for loaded array unknown callback call dynamic. -fn emit_loaded_array_unknown_callback_call_dynamic( - array_source: LoadedArraySource, - arr_ty: &PhpType, - call_reg: &str, - concat_saved_before_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let (array_reg, len_reg, overflow_count_reg, overflow_bytes_reg) = match emitter.target.arch { - Arch::AArch64 => ("x20", "x21", "x22", "x23"), - Arch::X86_64 => ("r13", "r14", "r15", "rbx"), - }; - let elem_ty = match arr_ty { - PhpType::Array(elem_ty) => *elem_ty.clone(), - _ => PhpType::Int, - }; - let register_arg_capacity = unknown_callback_register_arg_capacity(emitter.target, &elem_ty); - let register_temp_bytes = register_arg_capacity * 16; - - emit_loaded_array_source_to_reg(array_source, array_reg, emitter); - abi::emit_load_from_address(emitter, len_reg, array_reg, 0); // load the dynamic callback-argument count - emit_unknown_dynamic_overflow_size( - len_reg, - overflow_count_reg, - overflow_bytes_reg, - register_arg_capacity, - emitter, - ctx, - ); - emit_dynamic_stack_adjust(emitter, overflow_bytes_reg, true); - abi::emit_reserve_temporary_stack(emitter, register_temp_bytes); - - emit_unknown_dynamic_register_arg_temps( - array_reg, - len_reg, - &elem_ty, - register_arg_capacity, - emitter, - ctx, - ); - emit_unknown_dynamic_stack_args( - array_reg, - overflow_count_reg, - &elem_ty, - register_arg_capacity, - register_temp_bytes, - emitter, - ctx, - ); - emit_unknown_dynamic_load_register_args( - len_reg, - &elem_ty, - register_arg_capacity, - emitter, - ctx, - ); - - abi::emit_release_temporary_stack(emitter, register_temp_bytes); - let ret_ty = PhpType::Int; - if !concat_saved_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - abi::emit_call_reg(emitter, call_reg); - if concat_saved_before_args { - emit_dynamic_stack_adjust(emitter, overflow_bytes_reg, false); - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - emit_dynamic_stack_adjust(emitter, overflow_bytes_reg, false); - } - - ret_ty -} - -/// Emits assembly for unknown dynamic overflow size. -fn emit_unknown_dynamic_overflow_size( - len_reg: &str, - overflow_count_reg: &str, - overflow_bytes_reg: &str, - register_arg_capacity: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let has_overflow = ctx.next_label("cufa_unknown_dynamic_overflow"); - let done = ctx.next_label("cufa_unknown_dynamic_overflow_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #0", overflow_count_reg)); // default to no stack-passed unknown callback arguments - emitter.instruction(&format!("cmp {}, #{}", len_reg, register_arg_capacity)); // compare runtime arity with the register argument capacity - emitter.instruction(&format!("b.gt {}", has_overflow)); // compute stack spill bytes only when runtime arity exceeds registers - emitter.instruction(&format!("b {}", done)); // skip overflow sizing for register-only calls - emitter.label(&has_overflow); - emitter.instruction(&format!("sub {}, {}, #{}", overflow_count_reg, len_reg, register_arg_capacity)); // count callback arguments that must be stack-passed - emitter.label(&done); - emitter.instruction(&format!("lsl {}, {}, #4", overflow_bytes_reg, overflow_count_reg)); // convert stack-passed argument count to 16-byte ABI slots - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, 0", overflow_count_reg)); // default to no stack-passed unknown callback arguments - emitter.instruction(&format!("cmp {}, {}", len_reg, register_arg_capacity)); // compare runtime arity with the register argument capacity - emitter.instruction(&format!("jg {}", has_overflow)); // compute stack spill bytes only when runtime arity exceeds registers - emitter.instruction(&format!("jmp {}", done)); // skip overflow sizing for register-only calls - emitter.label(&has_overflow); - emitter.instruction(&format!("mov {}, {}", overflow_count_reg, len_reg)); // seed overflow count from the runtime callback arity - emitter.instruction(&format!("sub {}, {}", overflow_count_reg, register_arg_capacity)); // count callback arguments that must be stack-passed - emitter.label(&done); - emitter.instruction(&format!("mov {}, {}", overflow_bytes_reg, overflow_count_reg)); // copy overflow count before scaling to bytes - emitter.instruction(&format!("shl {}, 4", overflow_bytes_reg)); // convert stack-passed argument count to 16-byte ABI slots - } - } -} - -/// Emits assembly for dynamic stack adjust. -fn emit_dynamic_stack_adjust(emitter: &mut Emitter, bytes_reg: &str, subtract: bool) { - match (emitter.target.arch, subtract) { - (Arch::AArch64, true) => { - emitter.instruction(&format!("sub sp, sp, {}", bytes_reg)); // reserve dynamic stack space for unknown callback overflow arguments - } - (Arch::AArch64, false) => { - emitter.instruction(&format!("add sp, sp, {}", bytes_reg)); // release dynamic stack space for unknown callback overflow arguments - } - (Arch::X86_64, true) => { - emitter.instruction(&format!("sub rsp, {}", bytes_reg)); // reserve dynamic stack space for unknown callback overflow arguments - } - (Arch::X86_64, false) => { - emitter.instruction(&format!("add rsp, {}", bytes_reg)); // release dynamic stack space for unknown callback overflow arguments - } - } -} - -/// Emits assembly for unknown dynamic register arg temps. -fn emit_unknown_dynamic_register_arg_temps( - array_reg: &str, - len_reg: &str, - elem_ty: &PhpType, - register_arg_capacity: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) { - for arg_idx in 0..register_arg_capacity { - let load_label = ctx.next_label("cufa_unknown_dynamic_reg_load"); - let done_label = ctx.next_label("cufa_unknown_dynamic_reg_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", len_reg, arg_idx + 1)); // check whether this register-passed callback argument exists - emitter.instruction(&format!("b.ge {}", load_label)); // materialize the register argument when present - emitter.instruction(&format!("b {}", done_label)); // leave absent optional unknown callback argument registers untouched - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", len_reg, arg_idx + 1)); // check whether this register-passed callback argument exists - emitter.instruction(&format!("jge {}", load_label)); // materialize the register argument when present - emitter.instruction(&format!("jmp {}", done_label)); // leave absent optional unknown callback argument registers untouched - } - } - emitter.label(&load_label); - args::load_array_element_to_result( - emitter, - elem_ty, - array_reg, - 24 + arg_idx * args::array_element_stride(elem_ty), - ); - abi::emit_incref_if_refcounted(emitter, &elem_ty.codegen_repr()); // retain borrowed heap arguments before passing them to the unknown callback - emit_store_current_result_to_sp_offset(emitter, elem_ty, arg_idx * 16); - emitter.label(&done_label); - } -} - -/// Emits assembly for unknown dynamic stack args. -fn emit_unknown_dynamic_stack_args( - array_reg: &str, - overflow_count_reg: &str, - elem_ty: &PhpType, - register_arg_capacity: usize, - register_temp_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let loop_label = ctx.next_label("cufa_unknown_dynamic_stack_loop"); - let done_label = ctx.next_label("cufa_unknown_dynamic_stack_done"); - let (idx_reg, source_idx_reg, source_reg, dest_reg, offset_reg) = match emitter.target.arch { - Arch::AArch64 => ("x24", "x25", "x26", "x27", "x28"), - Arch::X86_64 => ("rcx", "r10", "r11", "rsi", "rdx"), - }; - abi::emit_load_int_immediate(emitter, idx_reg, 0); - emitter.label(&loop_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, {}", idx_reg, overflow_count_reg)); // stop after all stack-passed unknown callback args are materialized - emitter.instruction(&format!("b.ge {}", done_label)); // leave the overflow materialization loop - emitter.instruction(&format!("add {}, {}, #{}", source_idx_reg, idx_reg, register_arg_capacity)); // convert overflow index to source array index - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", idx_reg, overflow_count_reg)); // stop after all stack-passed unknown callback args are materialized - emitter.instruction(&format!("jge {}", done_label)); // leave the overflow materialization loop - emitter.instruction(&format!("mov {}, {}", source_idx_reg, idx_reg)); // seed source array index from overflow index - emitter.instruction(&format!("add {}, {}", source_idx_reg, register_arg_capacity)); // convert overflow index to source array index - } - } - emit_dynamic_array_element_to_result( - array_reg, - source_idx_reg, - source_reg, - offset_reg, - elem_ty, - emitter, - ); - abi::emit_incref_if_refcounted(emitter, &elem_ty.codegen_repr()); // retain borrowed heap overflow arguments before passing them to the unknown callback - emit_unknown_dynamic_stack_arg_address( - idx_reg, - dest_reg, - offset_reg, - register_temp_bytes, - emitter, - ); - emit_store_current_result_to_address(emitter, elem_ty, dest_reg); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #1", idx_reg, idx_reg)); // advance to the next stack-passed unknown callback argument - emitter.instruction(&format!("b {}", loop_label)); // continue materializing overflow arguments - } - Arch::X86_64 => { - emitter.instruction(&format!("add {}, 1", idx_reg)); // advance to the next stack-passed unknown callback argument - emitter.instruction(&format!("jmp {}", loop_label)); // continue materializing overflow arguments - } - } - emitter.label(&done_label); -} - -/// Emits assembly for dynamic array element to result. -fn emit_dynamic_array_element_to_result( - array_reg: &str, - index_reg: &str, - source_reg: &str, - offset_reg: &str, - elem_ty: &PhpType, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, {}", source_reg, array_reg)); // seed the dynamic source element pointer from the callback-argument array - emitter.instruction(&format!("add {}, {}, #24", source_reg, source_reg)); // skip the indexed array header before dynamic argument lookup - if args::array_element_stride(elem_ty) == 16 { - emitter.instruction(&format!("lsl {}, {}, #4", offset_reg, index_reg)); // scale dynamic source index by the string element width - } else { - emitter.instruction(&format!("lsl {}, {}, #3", offset_reg, index_reg)); // scale dynamic source index by the scalar element width - } - emitter.instruction(&format!("add {}, {}, {}", source_reg, source_reg, offset_reg)); // address the selected dynamic callback argument slot - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", source_reg, array_reg)); // seed the dynamic source element pointer from the callback-argument array - emitter.instruction(&format!("add {}, 24", source_reg)); // skip the indexed array header before dynamic argument lookup - emitter.instruction(&format!("mov {}, {}", offset_reg, index_reg)); // copy dynamic source index before scaling - emitter.instruction(&format!("imul {}, {}", offset_reg, args::array_element_stride(elem_ty))); // scale dynamic source index by the element width - emitter.instruction(&format!("add {}, {}", source_reg, offset_reg)); // address the selected dynamic callback argument slot - } - } - args::load_array_element_to_result(emitter, elem_ty, source_reg, 0); -} - -/// Emits assembly for unknown dynamic stack arg address. -fn emit_unknown_dynamic_stack_arg_address( - idx_reg: &str, - dest_reg: &str, - offset_reg: &str, - register_temp_bytes: usize, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, sp", dest_reg)); // seed overflow destination from the current stack pointer - emitter.instruction(&format!("add {}, {}, #{}", dest_reg, dest_reg, register_temp_bytes)); // skip register-argument temp slots to reach outgoing stack args - emitter.instruction(&format!("lsl {}, {}, #4", offset_reg, idx_reg)); // scale overflow argument index by the 16-byte ABI slot width - emitter.instruction(&format!("add {}, {}, {}", dest_reg, dest_reg, offset_reg)); // address the outgoing overflow argument slot - } - Arch::X86_64 => { - emitter.instruction(&format!("lea {}, [rsp + {}]", dest_reg, register_temp_bytes)); // address the first outgoing overflow argument slot - emitter.instruction(&format!("mov {}, {}", offset_reg, idx_reg)); // copy overflow argument index before scaling to bytes - emitter.instruction(&format!("shl {}, 4", offset_reg)); // scale overflow argument index by the 16-byte ABI slot width - emitter.instruction(&format!("add {}, {}", dest_reg, offset_reg)); // address the outgoing overflow argument slot - } - } -} - -/// Emits assembly for unknown dynamic load register args. -fn emit_unknown_dynamic_load_register_args( - len_reg: &str, - elem_ty: &PhpType, - register_arg_capacity: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) { - for arg_idx in 0..register_arg_capacity { - let load_label = ctx.next_label("cufa_unknown_dynamic_arg_load"); - let done_label = ctx.next_label("cufa_unknown_dynamic_arg_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", len_reg, arg_idx + 1)); // check whether this final register argument exists - emitter.instruction(&format!("b.ge {}", load_label)); // load the ABI register when the argument was provided - emitter.instruction(&format!("b {}", done_label)); // skip absent unknown callback argument registers - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", len_reg, arg_idx + 1)); // check whether this final register argument exists - emitter.instruction(&format!("jge {}", load_label)); // load the ABI register when the argument was provided - emitter.instruction(&format!("jmp {}", done_label)); // skip absent unknown callback argument registers - } - } - emitter.label(&load_label); - emit_load_sp_offset_to_arg_register(emitter, elem_ty, arg_idx * 16, arg_idx); - emitter.label(&done_label); - } -} - -/// Emits assembly for unknown dynamic capture args. -fn emit_unknown_dynamic_capture_args( - captures: &[(String, PhpType, bool)], - assignments: &[(PhpType, abi::OutgoingArgAssignment)], - overflow_count_reg: &str, - register_temp_bytes: usize, - visible_register_temp_bytes: usize, - emitter: &mut Emitter, - ctx: &Context, -) -> Vec<(PhpType, abi::OutgoingArgAssignment, usize)> { - let mut register_temps = Vec::new(); - let mut register_capture_idx = 0usize; - let mut stack_capture_idx = 0usize; - for ((capture_name, capture_ty, by_ref), (arg_ty, assignment)) in - captures.iter().zip(assignments.iter()) - { - emit_capture_arg_to_result(capture_name, capture_ty, *by_ref, emitter, ctx); - if assignment.in_register() { - let offset = visible_register_temp_bytes + register_capture_idx * 16; - emit_store_current_result_to_sp_offset(emitter, arg_ty, offset); - register_temps.push((arg_ty.clone(), assignment.clone(), offset)); - register_capture_idx += 1; - } else { - emit_store_current_result_to_dynamic_capture_stack( - overflow_count_reg, - register_temp_bytes, - stack_capture_idx * 16, - arg_ty, - emitter, - ); - stack_capture_idx += 1; - } - } - register_temps -} - -/// Emits assembly for capture arg to result. -fn emit_capture_arg_to_result( - capture_name: &str, - capture_ty: &PhpType, - by_ref: bool, - emitter: &mut Emitter, - ctx: &Context, -) { - emitter.comment(&format!("materialize callback capture ${}", capture_name)); - if by_ref { - if !args::emit_ref_arg_variable_address(capture_name, "callback capture ref", emitter, ctx) { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - return; - } - let Some(capture_info) = ctx.variables.get(capture_name) else { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - return; - }; - abi::emit_load(emitter, capture_ty, capture_info.stack_offset); -} - -/// Emits assembly for store current result to dynamic capture stack. -fn emit_store_current_result_to_dynamic_capture_stack( - overflow_count_reg: &str, - register_temp_bytes: usize, - capture_stack_offset: usize, - ty: &PhpType, - emitter: &mut Emitter, -) { - let (dest_reg, offset_reg) = match emitter.target.arch { - Arch::AArch64 => ("x10", "x11"), - Arch::X86_64 => ("r10", "r11"), - }; - emit_dynamic_capture_stack_arg_address( - overflow_count_reg, - dest_reg, - offset_reg, - register_temp_bytes, - capture_stack_offset, - emitter, - ); - emit_store_current_result_to_address(emitter, ty, dest_reg); -} - -/// Emits assembly for dynamic capture stack arg address. -fn emit_dynamic_capture_stack_arg_address( - overflow_count_reg: &str, - dest_reg: &str, - offset_reg: &str, - register_temp_bytes: usize, - capture_stack_offset: usize, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, sp", dest_reg)); // seed the captured-argument stack slot from the current stack pointer - emitter.instruction(&format!("add {}, {}, #{}", dest_reg, dest_reg, register_temp_bytes)); // skip register-argument temps before captured stack args - emitter.instruction(&format!("lsl {}, {}, #4", offset_reg, overflow_count_reg)); // scale visible overflow count to outgoing stack bytes - emitter.instruction(&format!("add {}, {}, {}", dest_reg, dest_reg, offset_reg)); // skip dynamic visible overflow args before captured stack args - if capture_stack_offset > 0 { - emitter.instruction(&format!("add {}, {}, #{}", dest_reg, dest_reg, capture_stack_offset)); // select the current captured stack argument slot - } - } - Arch::X86_64 => { - emitter.instruction(&format!("lea {}, [rsp + {}]", dest_reg, register_temp_bytes)); // skip register-argument temps before captured stack args - emitter.instruction(&format!("mov {}, {}", offset_reg, overflow_count_reg)); // copy visible overflow count before scaling to bytes - emitter.instruction(&format!("shl {}, 4", offset_reg)); // scale visible overflow count to outgoing stack bytes - emitter.instruction(&format!("add {}, {}", dest_reg, offset_reg)); // skip dynamic visible overflow args before captured stack args - if capture_stack_offset > 0 { - emitter.instruction(&format!("add {}, {}", dest_reg, capture_stack_offset)); // select the current captured stack argument slot - } - } - } -} - -/// Emits assembly for unknown dynamic load capture register args. -fn emit_unknown_dynamic_load_capture_register_args( - register_temps: &[(PhpType, abi::OutgoingArgAssignment, usize)], - emitter: &mut Emitter, -) { - for (ty, assignment, offset) in register_temps { - emit_load_sp_offset_to_assignment_register(emitter, ty, *offset, assignment); - } -} - -/// Emits assembly for store current result to sp offset. -fn emit_store_current_result_to_sp_offset(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - let stack_reg = match emitter.target.arch { - Arch::AArch64 => "sp", - Arch::X86_64 => "rsp", - }; - emit_store_current_result_to_address_offset(emitter, ty, stack_reg, offset); -} - -/// Emits assembly for store current result to address. -fn emit_store_current_result_to_address(emitter: &mut Emitter, ty: &PhpType, address_reg: &str) { - emit_store_current_result_to_address_offset(emitter, ty, address_reg, 0); -} - -/// Emits assembly for store current result to address offset. -fn emit_store_current_result_to_address_offset( - emitter: &mut Emitter, - ty: &PhpType, - address_reg: &str, - offset: usize, -) { - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), address_reg, offset); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_store_to_address(emitter, ptr_reg, address_reg, offset); - abi::emit_store_to_address(emitter, len_reg, address_reg, offset + 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), address_reg, offset); - } - } -} - -/// Emits assembly for load sp offset to arg register. -fn emit_load_sp_offset_to_arg_register( - emitter: &mut Emitter, - ty: &PhpType, - offset: usize, - arg_idx: usize, -) { - match ty.codegen_repr() { - PhpType::Float => { - let reg = abi::float_arg_reg_name(emitter.target, arg_idx); - abi::emit_load_temporary_stack_slot(emitter, reg, offset); - } - PhpType::Str => { - let ptr_reg = abi::int_arg_reg_name(emitter.target, arg_idx * 2); - let len_reg = abi::int_arg_reg_name(emitter.target, arg_idx * 2 + 1); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, offset); - abi::emit_load_temporary_stack_slot(emitter, len_reg, offset + 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - let reg = abi::int_arg_reg_name(emitter.target, arg_idx); - abi::emit_load_temporary_stack_slot(emitter, reg, offset); - } - } -} - -/// Emits assembly for load sp offset to assignment register. -fn emit_load_sp_offset_to_assignment_register( - emitter: &mut Emitter, - ty: &PhpType, - offset: usize, - assignment: &abi::OutgoingArgAssignment, -) { - match ty.codegen_repr() { - PhpType::Float => { - let reg = abi::float_arg_reg_name(emitter.target, assignment.start_reg); - abi::emit_load_temporary_stack_slot(emitter, reg, offset); - } - PhpType::Str => { - let ptr_reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg); - let len_reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, offset); - abi::emit_load_temporary_stack_slot(emitter, len_reg, offset + 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - let reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg); - abi::emit_load_temporary_stack_slot(emitter, reg, offset); - } - } -} - -/// Emits assembly for unknown callback case. -#[allow(clippy::too_many_arguments)] -fn emit_unknown_callback_case( - arg_count: usize, - elem_ty: &PhpType, - elem_size: usize, - array_reg: &str, - call_reg: &str, - captures: &[(String, PhpType, bool)], - concat_saved_before_args: bool, - done_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let mut arg_types = Vec::with_capacity(arg_count + captures.len()); - for i in 0..arg_count { - args::load_array_element_to_result(emitter, elem_ty, array_reg, 24 + i * elem_size); - let pushed_ty = args::push_loaded_array_element_arg(elem_ty, None, emitter, ctx, data); - arg_types.push(pushed_ty); - } - callback_env::push_captures_as_hidden_args(captures, emitter, ctx, &mut arg_types); - - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); - let ret_ty = PhpType::Int; - - if !concat_saved_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - abi::emit_call_reg(emitter, call_reg); - if concat_saved_before_args { - abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - abi::emit_release_temporary_stack(emitter, overflow_bytes); - } - abi::emit_jump(emitter, done_label); -} - -/// Emits assembly for call user func array missing arg abort. -fn emit_call_user_func_array_missing_arg_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: call_user_func_array() argument array is missing a required callback parameter\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the callback argument diagnostic to stderr - abi::emit_symbol_address(emitter, "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); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the callback argument 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 callback argument diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Emits assembly for call user func array unknown assoc abort. -fn emit_call_user_func_array_unknown_assoc_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: call_user_func_array() could not resolve named callback arguments for this callable\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the callback metadata diagnostic to stderr - abi::emit_symbol_address(emitter, "x1", &message_label); - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the callback metadata diagnostic byte length to write() - emitter.syscall(4); - abi::emit_exit(emitter, 1); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the callback metadata diagnostic to stderr - abi::emit_symbol_address(emitter, "rsi", &message_label); - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the callback metadata diagnostic byte length to write() - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal callback metadata diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Emits assembly for a descriptor invoker argument-container type mismatch. -pub(crate) fn emit_call_user_func_array_invalid_mixed_args_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: callable descriptor invoker expected an indexed or associative argument array\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the descriptor argument-shape diagnostic to stderr - abi::emit_symbol_address(emitter, "x1", &message_label); - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the descriptor argument-shape diagnostic byte length to write() - emitter.syscall(4); - abi::emit_exit(emitter, 1); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the descriptor argument-shape diagnostic to stderr - abi::emit_symbol_address(emitter, "rsi", &message_label); - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the descriptor argument-shape diagnostic byte length to write() - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the descriptor argument-shape diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Emits assembly for dynamic string callback abort. -pub(crate) fn emit_dynamic_string_callback_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: dynamic string callback could not be resolved\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the dynamic callback diagnostic to stderr - abi::emit_symbol_address(emitter, "x1", &message_label); - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the dynamic callback diagnostic byte length to write() - emitter.syscall(4); - abi::emit_exit(emitter, 1); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the dynamic callback diagnostic to stderr - abi::emit_symbol_address(emitter, "rsi", &message_label); - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the dynamic callback diagnostic byte length to write() - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal dynamic callback diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Computes the type metadata for widen callback arg. -fn widen_callback_arg_type(a: &PhpType, b: &PhpType) -> PhpType { - if a == b { - return a.clone(); - } - if matches!(a, PhpType::Mixed | PhpType::Union(_)) - || matches!(b, PhpType::Mixed | PhpType::Union(_)) - { - return PhpType::Mixed; - } - if *a == PhpType::Str || *b == PhpType::Str { - return PhpType::Str; - } - if *a == PhpType::Float || *b == PhpType::Float { - return PhpType::Float; - } - if *a == PhpType::Void { - return b.clone(); - } - if *b == PhpType::Void { - return a.clone(); - } - a.clone() -} diff --git a/src/codegen/builtins/arrays/callable_forms.rs b/src/codegen/builtins/arrays/callable_forms.rs deleted file mode 100644 index 39cec971c5..0000000000 --- a/src/codegen/builtins/arrays/callable_forms.rs +++ /dev/null @@ -1,699 +0,0 @@ -//! Purpose: -//! Lowers non-scalar PHP callable forms used by dynamic-call builtins. -//! Handles invokable objects and static/literal callable arrays before generic pointer dispatch. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::call_user_func` -//! - `crate::codegen::builtins::arrays::call_user_func_array` -//! -//! Key details: -//! - Descriptor-capable shapes route through the uniform invoker; unsupported -//! shapes preserve PHP evaluation order by delegating to normal method emitters. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::callable_dispatch::RuntimeInstanceCallableShape; -use crate::codegen::functions; -use crate::codegen::{abi, callable_dispatch}; -use crate::names::{php_symbol_key, Name}; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver}; -use crate::types::PhpType; - -use super::call_user_func_array::{self, LoadedArraySource}; -use super::descriptor_arg_builder; -use super::receiver_call_args; - -/// Emits assembly for call user func form. -pub(crate) fn emit_call_user_func_form( - callback: &Expr, - callback_args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match resolve_callable_form(callback, ctx) { - Some(CallableForm::InvokableObject { object, .. }) => { - if callback_args_have_spread(callback_args) { - return emit_instance_method_descriptor_spread_form( - &object, - "__invoke", - RuntimeInstanceCallableShape::ObjectInvoke, - callback_args, - callback.span, - emitter, - ctx, - data, - ) - .or_else(|| { - Some(crate::codegen::expr::objects::emit_method_call( - &object, - "__invoke", - callback_args, - emitter, - ctx, - data, - )) - }); - } - let descriptor_args = - receiver_prefixed_indexed_arg_array(&object, callback_args, callback.span); - emit_instance_method_descriptor_form( - &object, - "__invoke", - RuntimeInstanceCallableShape::ObjectInvoke, - emitter, - ctx, - data, - &descriptor_args, - ) - .or_else(|| { - Some(crate::codegen::expr::objects::emit_method_call( - &object, - "__invoke", - callback_args, - emitter, - ctx, - data, - )) - }) - } - Some(CallableForm::InstanceMethod { object, method, .. }) => { - if callback_args_have_spread(callback_args) { - return emit_instance_method_descriptor_spread_form( - &object, - &method, - RuntimeInstanceCallableShape::InstanceMethod, - callback_args, - callback.span, - emitter, - ctx, - data, - ) - .or_else(|| { - Some(crate::codegen::expr::objects::emit_method_call( - &object, - &method, - callback_args, - emitter, - ctx, - data, - )) - }); - } - let descriptor_args = - receiver_prefixed_indexed_arg_array(&object, callback_args, callback.span); - emit_instance_method_descriptor_form( - &object, - &method, - RuntimeInstanceCallableShape::InstanceMethod, - emitter, - ctx, - data, - &descriptor_args, - ) - .or_else(|| { - Some(crate::codegen::expr::objects::emit_method_call( - &object, - &method, - callback_args, - emitter, - ctx, - data, - )) - }) - } - Some(CallableForm::StaticMethod { receiver, method }) => { - if callback_args_have_spread(callback_args) { - return emit_static_method_descriptor_spread_form( - &receiver, - &method, - callback_args, - emitter, - ctx, - data, - ) - .or_else(|| { - Some(crate::codegen::expr::objects::emit_static_method_call( - &receiver, - &method, - callback_args, - emitter, - ctx, - data, - )) - }); - } - emit_static_method_descriptor_form( - &receiver, - &method, - emitter, - ctx, - data, - &Expr::new(ExprKind::ArrayLiteral(callback_args.to_vec()), callback.span), - ) - .or_else(|| { - Some(crate::codegen::expr::objects::emit_static_method_call( - &receiver, - &method, - callback_args, - emitter, - ctx, - data, - )) - }) - } - None => crate::codegen::expr::calls::emit_runtime_callable_array_call( - callback, - callback_args, - emitter, - ctx, - data, - ), - } -} - -/// Emits assembly for call user func array form. -pub(crate) fn emit_call_user_func_array_form( - callback: &Expr, - arg_array: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - if let Some(form) = resolve_callable_form(callback, ctx) { - match form { - CallableForm::StaticMethod { receiver, method } => { - if let Some(ret_ty) = emit_static_method_descriptor_form( - &receiver, - &method, - emitter, - ctx, - data, - arg_array, - ) { - return Some(ret_ty); - } - } - CallableForm::InvokableObject { object } => { - if let Some(descriptor_args) = - receiver_prefixed_call_user_func_array_args(&object, arg_array) - { - if let Some(ret_ty) = emit_instance_method_descriptor_form( - &object, - "__invoke", - RuntimeInstanceCallableShape::ObjectInvoke, - emitter, - ctx, - data, - &descriptor_args, - ) { - return Some(ret_ty); - } - } - if let Some(ret_ty) = emit_instance_method_descriptor_dynamic_arg_form( - &object, - "__invoke", - RuntimeInstanceCallableShape::ObjectInvoke, - arg_array, - emitter, - ctx, - data, - ) { - return Some(ret_ty); - } - } - CallableForm::InstanceMethod { object, method } => { - if let Some(descriptor_args) = - receiver_prefixed_call_user_func_array_args(&object, arg_array) - { - if let Some(ret_ty) = emit_instance_method_descriptor_form( - &object, - &method, - RuntimeInstanceCallableShape::InstanceMethod, - emitter, - ctx, - data, - &descriptor_args, - ) { - return Some(ret_ty); - } - } - if let Some(ret_ty) = emit_instance_method_descriptor_dynamic_arg_form( - &object, - &method, - RuntimeInstanceCallableShape::InstanceMethod, - arg_array, - emitter, - ctx, - data, - ) { - return Some(ret_ty); - } - } - } - } - - let spread_args = vec![Expr::new( - ExprKind::Spread(Box::new(arg_array.clone())), - arg_array.span, - )]; - emit_call_user_func_form(callback, &spread_args, emitter, ctx, data) -} - -/// Invokes receiver-bound `call_user_func()` spread args through the descriptor invoker. -#[allow(clippy::too_many_arguments)] -fn emit_instance_method_descriptor_spread_form( - object: &Expr, - method: &str, - shape: RuntimeInstanceCallableShape, - callback_args: &[Expr], - _span: crate::span::Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - if let Some(arg_array) = single_spread_inner(callback_args) { - return emit_instance_method_descriptor_dynamic_arg_form( - object, method, shape, arg_array, emitter, ctx, data, - ); - } - - emit_instance_method_descriptor_positional_spread_form( - object, - method, - shape, - callback_args, - emitter, - ctx, - data, - ) -} - -/// Returns the spread source when `call_user_func()` forwards one spread argument segment. -fn single_spread_inner(args: &[Expr]) -> Option<&Expr> { - if let [arg] = args { - if let ExprKind::Spread(inner) = &arg.kind { - return Some(inner); - } - } - None -} - -/// Invokes receiver-bound positional+spread `call_user_func()` args through descriptors. -#[allow(clippy::too_many_arguments)] -fn emit_instance_method_descriptor_positional_spread_form( - object: &Expr, - method: &str, - shape: RuntimeInstanceCallableShape, - callback_args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let receiver_ty = functions::infer_contextual_type(object, ctx); - let class_name = functions::singular_object_class(&receiver_ty)?; - let case = - callable_dispatch::runtime_instance_method_case(ctx, data, class_name, method, shape)?; - if !case.has_invoker { - return None; - } - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - - let leading_args = vec![object.clone()]; - let arg_array_ty = descriptor_arg_builder::emit_positional_spread_invoker_arg_array( - &leading_args, - callback_args, - Some(&case.sig), - true, - emitter, - ctx, - data, - )?; - let call_reg = abi::nested_call_reg(emitter); - abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); - call_user_func_array::emit_call_descriptor_array_invoker( - LoadedArraySource::Result, - &arg_array_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - Some(PhpType::Mixed) -} - -/// Invokes a public instance-method or `__invoke` callable through its descriptor invoker. -fn emit_instance_method_descriptor_form( - object: &Expr, - method: &str, - shape: RuntimeInstanceCallableShape, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - arg_array: &Expr, -) -> Option { - let receiver_ty = functions::infer_contextual_type(object, ctx); - let class_name = functions::singular_object_class(&receiver_ty)?; - let case = - callable_dispatch::runtime_instance_method_case(ctx, data, class_name, method, shape)?; - if !case.has_invoker { - return None; - } - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - - let arr_ty = emit_expr(arg_array, emitter, ctx, data); - let call_reg = abi::nested_call_reg(emitter); - abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); - call_user_func_array::emit_call_descriptor_array_invoker( - LoadedArraySource::Result, - &arr_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - Some(PhpType::Mixed) -} - -/// Invokes a receiver-bound descriptor with a dynamic `call_user_func_array()` container. -fn emit_instance_method_descriptor_dynamic_arg_form( - object: &Expr, - method: &str, - shape: RuntimeInstanceCallableShape, - arg_array: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let inferred_arg_ty = functions::infer_contextual_type(arg_array, ctx); - if !matches!( - inferred_arg_ty.codegen_repr(), - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Mixed - ) { - return None; - } - let receiver_ty = functions::infer_contextual_type(object, ctx); - let class_name = functions::singular_object_class(&receiver_ty)?; - let case = - callable_dispatch::runtime_instance_method_case(ctx, data, class_name, method, shape)?; - if !case.has_invoker { - return None; - } - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - - if !receiver_call_args::emit_receiver_prefixed_dynamic_arg_mixed( - object, - arg_array, - &inferred_arg_ty, - emitter, - ctx, - data, - ) { - return None; - } - let call_reg = abi::nested_call_reg(emitter); - abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); - call_user_func_array::emit_call_descriptor_array_invoker( - LoadedArraySource::Result, - &PhpType::Mixed, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - Some(PhpType::Mixed) -} - -/// Invokes static-method positional+spread `call_user_func()` args through descriptors. -fn emit_static_method_descriptor_spread_form( - receiver: &StaticReceiver, - method: &str, - callback_args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let StaticReceiver::Named(class_name) = receiver else { - return None; - }; - let case = callable_dispatch::runtime_static_method_case( - ctx, - data, - class_name.as_str(), - method, - )?; - if !case.has_invoker { - return None; - } - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - - let arg_array_ty = descriptor_arg_builder::emit_positional_spread_invoker_arg_array( - &[], - callback_args, - Some(&case.sig), - true, - emitter, - ctx, - data, - )?; - let call_reg = abi::nested_call_reg(emitter); - abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); - call_user_func_array::emit_call_descriptor_array_invoker( - LoadedArraySource::Result, - &arg_array_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - Some(PhpType::Mixed) -} - -/// Invokes a public static-method callable array through its descriptor invoker. -fn emit_static_method_descriptor_form( - receiver: &StaticReceiver, - method: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - arg_array: &Expr, -) -> Option { - let StaticReceiver::Named(class_name) = receiver else { - return None; - }; - let case = callable_dispatch::runtime_static_method_case( - ctx, - data, - class_name.as_str(), - method, - )?; - if !case.has_invoker { - return None; - } - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - - let arr_ty = emit_expr(arg_array, emitter, ctx, data); - let call_reg = abi::nested_call_reg(emitter); - abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); - call_user_func_array::emit_call_descriptor_array_invoker( - LoadedArraySource::Result, - &arr_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - Some(PhpType::Mixed) -} - -/// Builds an indexed descriptor argument literal with receiver prepended. -fn receiver_prefixed_indexed_arg_array( - receiver: &Expr, - args: &[Expr], - span: crate::span::Span, -) -> Expr { - let mut elems = Vec::with_capacity(args.len() + 1); - elems.push(receiver.clone()); - elems.extend(args.iter().cloned()); - Expr::new(ExprKind::ArrayLiteral(elems), span) -} - -/// Builds descriptor invoker args for `call_user_func_array()` when safe to rewrite. -fn receiver_prefixed_call_user_func_array_args( - receiver: &Expr, - arg_array: &Expr, -) -> Option { - match &arg_array.kind { - ExprKind::ArrayLiteral(elems) => { - let mut prefixed = Vec::with_capacity(elems.len() + 1); - prefixed.push(receiver.clone()); - prefixed.extend(elems.iter().cloned()); - Some(Expr::new(ExprKind::ArrayLiteral(prefixed), arg_array.span)) - } - ExprKind::ArrayLiteralAssoc(pairs) => { - let mut prefixed = Vec::with_capacity(pairs.len() + 1); - prefixed.push(( - Expr::new(ExprKind::IntLiteral(0), arg_array.span), - receiver.clone(), - )); - prefixed.extend(pairs.iter().cloned()); - Some(Expr::new(ExprKind::ArrayLiteralAssoc(prefixed), arg_array.span)) - } - _ => None, - } -} - -/// Returns true when descriptor argument literal rewriting would need spread support. -fn callback_args_have_spread(args: &[Expr]) -> bool { - args.iter().any(|arg| matches!(arg.kind, ExprKind::Spread(_))) -} - -enum CallableForm { - InvokableObject { - object: Expr, - }, - InstanceMethod { - object: Expr, - method: String, - }, - StaticMethod { - receiver: StaticReceiver, - method: String, - }, -} - -/// Resolves callable form using the available compile-time metadata. -fn resolve_callable_form(callback: &Expr, ctx: &Context) -> Option { - if let ExprKind::Variable(var_name) = &callback.kind { - if let Some(target) = ctx.callable_array_targets.get(var_name) { - return callable_target_form(target); - } - } - - if let Some((receiver, method)) = callable_array_parts(callback) { - if let Some(receiver) = static_callable_receiver(receiver, ctx) { - return Some(CallableForm::StaticMethod { - receiver, - method: method.to_string(), - }); - } - let receiver_ty = functions::infer_contextual_type(receiver, ctx); - let class_name = functions::singular_object_class(&receiver_ty)?; - if ctx - .classes - .get(class_name) - .is_some_and(|class_info| class_info.methods.contains_key(&php_symbol_key(method))) - { - return Some(CallableForm::InstanceMethod { - object: receiver.clone(), - method: method.to_string(), - }); - } - return None; - } - - let callback_ty = functions::infer_contextual_type(callback, ctx); - let class_name = functions::singular_object_class(&callback_ty)?; - if ctx - .classes - .get(class_name) - .is_some_and(|class_info| class_info.methods.contains_key("__invoke")) - { - Some(CallableForm::InvokableObject { - object: callback.clone(), - }) - } else { - None - } -} - -/// Provides the Callable target form helper used by the callable forms module. -fn callable_target_form(target: &CallableTarget) -> Option { - match target { - CallableTarget::Method { object, method } => Some(CallableForm::InstanceMethod { - object: *object.clone(), - method: method.clone(), - }), - CallableTarget::StaticMethod { receiver, method } => Some(CallableForm::StaticMethod { - receiver: receiver.clone(), - method: method.clone(), - }), - CallableTarget::Function(_) => None, - } -} - -/// Provides the Callable array parts helper used by the callable forms module. -fn callable_array_parts(callback: &Expr) -> Option<(&Expr, &str)> { - let elems = match &callback.kind { - ExprKind::ArrayLiteral(elems) => elems, - _ => return None, - }; - if elems.len() != 2 { - return None; - } - let ExprKind::StringLiteral(method) = &elems[1].kind else { - return None; - }; - Some((&elems[0], method.as_str())) -} - -/// Provides the Static callable receiver helper used by the callable forms module. -fn static_callable_receiver(receiver: &Expr, ctx: &Context) -> Option { - let class_name = match &receiver.kind { - ExprKind::StringLiteral(class_name) => { - resolve_class_name(ctx, class_name).map(str::to_string) - } - ExprKind::ClassConstant { receiver } => resolve_static_receiver_class(receiver, ctx), - _ => None, - }?; - Some(StaticReceiver::Named(Name::from(class_name))) -} - -/// Resolves static receiver class using the available compile-time metadata. -fn resolve_static_receiver_class(receiver: &StaticReceiver, ctx: &Context) -> Option { - match receiver { - StaticReceiver::Named(name) => resolve_class_name(ctx, name.as_str()).map(str::to_string), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.parent.clone()), - } -} - -/// Resolves class name using the available compile-time metadata. -fn resolve_class_name<'a>(ctx: &'a Context, class_name: &str) -> Option<&'a str> { - let class_key = php_symbol_key(class_name.trim_start_matches('\\')); - ctx.classes - .keys() - .find(|existing| php_symbol_key(existing) == class_key) - .map(String::as_str) -} diff --git a/src/codegen/builtins/arrays/callback_env.rs b/src/codegen/builtins/arrays/callback_env.rs deleted file mode 100644 index 09c0bc0ae1..0000000000 --- a/src/codegen/builtins/arrays/callback_env.rs +++ /dev/null @@ -1,978 +0,0 @@ -//! Purpose: -//! Builds callback capture environments used by array and dynamic-call builtins. -//! Owns hidden capture materialization and deferred wrapper metadata for emitted callbacks. -//! -//! Called from: -//! - Array callback builtins such as `array_map()`, `array_filter()`, `array_reduce()`, and sort/walk helpers. -//! - Dynamic-call builtins such as `call_user_func()` and `call_user_func_array()`. -//! -//! Key details: -//! - Capture slots must preserve source-call evaluation order and ABI argument layout for wrapper calls. -//! - Descriptor-valued callbacks keep receiver and capture environments in descriptor storage. - -use crate::codegen::abi; -use crate::codegen::context::{Context, DeferredCallbackWrapper, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{emit_expr, expr_result_heap_ownership}; -use crate::codegen::platform::Arch; -use crate::names::{function_symbol, php_symbol_key}; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver}; -use crate::span::Span; -use crate::types::{FunctionSig, PhpType}; - -use super::super::callable_lookup::{lookup_function, FunctionLookup}; - -/// Metadata for a deferred callback wrapper emitted after the main function body. -/// Holds the environment layout so the wrapper can reload captures and forward the call. -pub(crate) struct CallbackEnv { - pub(crate) wrapper_label: String, - pub(crate) env_bytes: usize, - pub(crate) array_slot_offset: usize, -} - -/// Metadata for a descriptor-backed callback wrapper environment. -pub(crate) struct DescriptorCallbackEnv { - pub(crate) wrapper_label: String, - pub(crate) env_bytes: usize, - pub(crate) array_slot_offset: usize, -} - -/// Metadata for a callable-array target that can be invoked through a descriptor callback wrapper. -pub(crate) struct CallableArrayDescriptorCallback { - pub(crate) descriptor_label: String, - pub(crate) sig: FunctionSig, - pub(crate) receiver_prefix: Option<(Expr, PhpType)>, -} - -/// Resolves a callback expression and emits code to load its address into `call_reg`. -/// -/// Handles string literals, callable variables, and evaluated callback expressions. -/// Returns the list of captured variables with their types and by-ref flags. -pub(crate) fn materialize_callback_address( - callback: &Expr, - call_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Vec<(String, PhpType, bool)> { - match &callback.kind { - ExprKind::StringLiteral(name) => { - let resolved_name = match lookup_function(ctx, name) { - Some(FunctionLookup::UserFunction(name)) - | Some(FunctionLookup::IncludeVariant(name)) => name, - _ => name.clone(), - }; - let label = function_symbol(&resolved_name); - abi::emit_symbol_address(emitter, call_reg, &label); - Vec::new() - } - ExprKind::Variable(name) => { - let var = ctx.variables.get(name).expect("undefined callback variable"); - abi::load_at_offset(emitter, call_reg, var.stack_offset); // load the callback descriptor from the callable variable slot - if ctx.ref_params.contains(name) { - abi::emit_load_from_address(emitter, call_reg, call_reg, 0); - } - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - call_reg, - call_reg, - ); - crate::codegen::callables::callable_captures(callback, ctx) - } - _ => { - emit_expr(callback, emitter, ctx, data); - let result_reg = abi::int_result_reg(emitter); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // keep the evaluated callback descriptor in the nested-call scratch register - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - call_reg, - call_reg, - ); - crate::codegen::callables::callable_captures(callback, ctx) - } - } -} - -/// Resolves a local callable-array callback to a descriptor plus optional receiver prefix. -pub(crate) fn resolve_callable_array_descriptor_callback( - callback: &Expr, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let ExprKind::Variable(var_name) = &callback.kind else { - return None; - }; - let target = ctx.callable_array_targets.get(var_name).cloned()?; - match target { - CallableTarget::StaticMethod { receiver, method } => { - let class_name = resolve_static_receiver_class(&receiver, ctx)?; - let case = - crate::codegen::callable_dispatch::runtime_static_method_case(ctx, data, &class_name, &method)?; - Some(CallableArrayDescriptorCallback { - descriptor_label: case.descriptor_label, - sig: case.sig, - receiver_prefix: None, - }) - } - CallableTarget::Method { object, method } => { - let receiver = callable_array_slot_expr(var_name, 0); - let receiver_ty = - crate::codegen::functions::infer_contextual_type(&receiver, ctx).codegen_repr(); - let object_ty = crate::codegen::functions::infer_contextual_type(&object, ctx); - let class_name = - crate::codegen::functions::singular_object_class(&object_ty)?.to_string(); - let case = crate::codegen::callable_dispatch::runtime_instance_method_case( - ctx, - data, - &class_name, - &method, - crate::codegen::callable_dispatch::RuntimeInstanceCallableShape::InstanceMethod, - )?; - Some(CallableArrayDescriptorCallback { - descriptor_label: case.descriptor_label, - sig: case.sig, - receiver_prefix: Some((receiver, receiver_ty)), - }) - } - CallableTarget::Function(_) => None, - } -} - -/// Emits code to push each captured variable as a hidden argument before a deferred wrapper call. -/// -/// For by-ref captures, emits the variable's address; for value captures, loads the value from -/// the stack slot and pushes it. Appends corresponding types to `arg_types`. -pub(crate) fn push_captures_as_hidden_args( - captures: &[(String, PhpType, bool)], - emitter: &mut Emitter, - ctx: &Context, - arg_types: &mut Vec, -) { - if let Some(descriptor_offset) = ctx.runtime_capture_descriptor_offset { - push_descriptor_captures_as_hidden_args(captures, descriptor_offset, emitter, arg_types); - return; - } - - for (capture_name, capture_ty, by_ref) in captures { - emitter.comment(&format!("push callback capture ${}", capture_name)); - if *by_ref { - if !crate::codegen::expr::calls::args::emit_ref_arg_variable_address( - capture_name, - "callback capture ref", - emitter, - ctx, - ) { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - continue; - } - crate::codegen::expr::calls::args::push_arg_value(emitter, &PhpType::Int); - arg_types.push(PhpType::Int); - } else { - let Some(capture_info) = ctx.variables.get(capture_name) else { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - continue; - }; - abi::emit_load(emitter, capture_ty, capture_info.stack_offset); - crate::codegen::expr::calls::args::push_arg_value(emitter, capture_ty); - arg_types.push(capture_ty.clone()); - } - } -} - -/// Pushes hidden captures loaded from the runtime descriptor stored in a frame slot. -fn push_descriptor_captures_as_hidden_args( - captures: &[(String, PhpType, bool)], - descriptor_offset: usize, - emitter: &mut Emitter, - arg_types: &mut Vec, -) { - let descriptor_reg = abi::symbol_scratch_reg(emitter); - for (idx, (capture_name, capture_ty, by_ref)) in captures.iter().enumerate() { - emitter.comment(&format!("push descriptor capture ${}", capture_name)); - abi::load_at_offset(emitter, descriptor_reg, descriptor_offset); - if *by_ref { - crate::codegen::callable_descriptor::emit_load_runtime_capture_to_result( - emitter, - descriptor_reg, - idx, - &PhpType::Int, - ); - crate::codegen::expr::calls::args::push_arg_value(emitter, &PhpType::Int); - arg_types.push(PhpType::Int); - } else { - crate::codegen::callable_descriptor::emit_load_runtime_capture_to_result( - emitter, - descriptor_reg, - idx, - capture_ty, - ); - crate::codegen::expr::calls::args::push_arg_value(emitter, capture_ty); - arg_types.push(capture_ty.clone()); - } - } -} - -/// Allocates a temporary stack frame for the callback environment and stores the callback -/// address, array pointer, and all captures into it. Returns the wrapper label and stack layout. -pub(crate) fn emit_captured_callback_env( - callback_reg: &str, - array_reg: &str, - captures: &[(String, PhpType, bool)], - visible_arg_types: Vec, - emitter: &mut Emitter, - ctx: &mut Context, -) -> CallbackEnv { - let wrapper_label = ctx.next_label("callback_wrapper"); - ctx.deferred_callback_wrappers.push(DeferredCallbackWrapper { - label: wrapper_label.clone(), - visible_arg_types, - target_visible_arg_types: None, - capture_types: captures - .iter() - .map(|(_, ty, by_ref)| if *by_ref { PhpType::Int } else { ty.clone() }) - .collect(), - descriptor_prefix_types: Vec::new(), - descriptor_return_type: None, - }); - - let env_slots = captures.len() + 2; - let env_bytes = env_slots * 16; - let array_slot_offset = (env_slots - 1) * 16; - - emitter.comment("callback capture environment"); - abi::emit_reserve_temporary_stack(emitter, env_bytes); - store_reg_to_env_slot(emitter, callback_reg, 0); - store_reg_to_env_slot(emitter, array_reg, array_slot_offset); - - for (idx, (capture_name, capture_ty, by_ref)) in captures.iter().enumerate() { - emitter.comment(&format!("store callback capture ${}", capture_name)); - if *by_ref { - if !crate::codegen::expr::calls::args::emit_ref_arg_variable_address( - capture_name, - "callback capture ref", - emitter, - ctx, - ) { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - continue; - } - store_current_result_to_env_slot(emitter, &PhpType::Int, (idx + 1) * 16); - } else { - let Some(capture_info) = ctx.variables.get(capture_name) else { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - continue; - }; - abi::emit_load(emitter, capture_ty, capture_info.stack_offset); - store_current_result_to_env_slot(emitter, capture_ty, (idx + 1) * 16); - } - } - - CallbackEnv { - wrapper_label, - env_bytes, - array_slot_offset, - } -} - -/// Emits assembly for persistent callback env from result. -pub(crate) fn emit_persistent_callback_env_from_result( - captures: &[(String, PhpType, bool)], - visible_arg_types: Vec, - target_visible_arg_types: Vec, - emitter: &mut Emitter, - ctx: &mut Context, -) -> String { - let wrapper_label = ctx.next_label("callback_wrapper"); - ctx.deferred_callback_wrappers.push(DeferredCallbackWrapper { - label: wrapper_label.clone(), - visible_arg_types, - target_visible_arg_types: Some(target_visible_arg_types), - capture_types: captures - .iter() - .map(|(_, ty, by_ref)| if *by_ref { PhpType::Int } else { ty.clone() }) - .collect(), - descriptor_prefix_types: Vec::new(), - descriptor_return_type: None, - }); - - let env_bytes = (captures.len() + 1) * 16; - emitter.comment("persistent callback capture environment"); - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - abi::int_result_reg(emitter), - abi::int_result_reg(emitter), - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the original callback entry address while allocating its env - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", env_bytes)); // request persistent callback environment storage - emitter.instruction("bl __rt_heap_alloc"); // allocate the persistent callback environment - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", env_bytes)); // request persistent callback environment storage - emitter.instruction("call __rt_heap_alloc"); // allocate the persistent callback environment - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the env pointer above the saved callback entry address - store_saved_callback_to_persistent_env(emitter); - - for (idx, (capture_name, capture_ty, by_ref)) in captures.iter().enumerate() { - emitter.comment(&format!("store persistent callback capture ${}", capture_name)); - let slot_offset = (idx + 1) * 16; - if *by_ref { - if !crate::codegen::expr::calls::args::emit_ref_arg_variable_address( - capture_name, - "callback capture ref", - emitter, - ctx, - ) { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - continue; - } - store_current_result_to_persistent_env_slot(emitter, &PhpType::Int, slot_offset); - } else { - let Some(capture_info) = ctx.variables.get(capture_name) else { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - continue; - }; - abi::emit_load(emitter, capture_ty, capture_info.stack_offset); - store_current_result_to_persistent_env_slot(emitter, capture_ty, slot_offset); - retain_persistent_capture_result(emitter, capture_ty); - } - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the persistent env pointer as the current result - abi::emit_release_temporary_stack(emitter, 16); // discard the saved original callback entry address - wrapper_label -} - -/// Emits a heap-backed descriptor callback environment from the current descriptor result. -pub(crate) fn emit_persistent_descriptor_callback_env_from_result( - callback: &Expr, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) -> Option { - let ownership = callable_descriptor_result_ownership(callback); - if !matches!(ownership, HeapOwnership::Owned | HeapOwnership::Borrowed) { - return None; - } - if matches!(ownership, HeapOwnership::Borrowed) { - crate::codegen::callable_descriptor::emit_retain_current_descriptor(emitter); - } - - let wrapper_label = ctx.next_label("descriptor_callback_wrapper"); - ctx.deferred_callback_wrappers.push(DeferredCallbackWrapper { - label: wrapper_label.clone(), - visible_arg_types, - target_visible_arg_types: None, - capture_types: Vec::new(), - descriptor_prefix_types: Vec::new(), - descriptor_return_type: Some(descriptor_return_type), - }); - - let env_bytes = 16; - emitter.comment("persistent descriptor callback environment"); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the selected callable descriptor while allocating its env - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", env_bytes)); // request persistent descriptor callback environment storage - emitter.instruction("bl __rt_heap_alloc"); // allocate the persistent descriptor callback environment - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", env_bytes)); // request persistent descriptor callback environment storage - emitter.instruction("call __rt_heap_alloc"); // allocate the persistent descriptor callback environment - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the env pointer above the saved selected descriptor - store_saved_callback_to_persistent_env(emitter); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the persistent descriptor env pointer as the current result - abi::emit_release_temporary_stack(emitter, 16); // discard the saved selected callable descriptor - Some(wrapper_label) -} - -/// Emits a heap-backed descriptor callback environment from a static descriptor label. -/// -/// Any descriptor-prefix values must already be pushed on the temporary stack in -/// source order. The helper stores them in persistent environment slots and -/// releases those temporary stack slots before returning the env pointer. -pub(crate) fn emit_persistent_descriptor_callback_env_from_static_descriptor( - descriptor_label: &str, - visible_arg_types: Vec, - descriptor_prefix_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) -> String { - let wrapper_label = ctx.next_label("descriptor_callback_wrapper"); - let prefix_count = descriptor_prefix_types.len(); - ctx.deferred_callback_wrappers.push(DeferredCallbackWrapper { - label: wrapper_label.clone(), - visible_arg_types, - target_visible_arg_types: None, - capture_types: Vec::new(), - descriptor_prefix_types: descriptor_prefix_types.clone(), - descriptor_return_type: Some(descriptor_return_type), - }); - - let env_bytes = (prefix_count + 1) * 16; - let prefix_bytes = prefix_count * 16; - emitter.comment("persistent static descriptor callback environment"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", env_bytes)); // request persistent descriptor callback environment storage - emitter.instruction("bl __rt_heap_alloc"); // allocate the persistent descriptor callback environment - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", env_bytes)); // request persistent descriptor callback environment storage - emitter.instruction("call __rt_heap_alloc"); // allocate the persistent descriptor callback environment - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the env pointer above saved descriptor-prefix values - abi::emit_symbol_address(emitter, abi::int_result_reg(emitter), descriptor_label); - store_current_result_to_persistent_env_slot(emitter, &PhpType::Callable, 0); - for (idx, prefix_ty) in descriptor_prefix_types.iter().enumerate() { - let saved_offset = 16 + (prefix_count - 1 - idx) * 16; - load_temporary_stack_slot_to_current_result(emitter, prefix_ty, saved_offset); - store_current_result_to_persistent_env_slot(emitter, prefix_ty, (idx + 1) * 16); - retain_persistent_capture_result(emitter, prefix_ty); - } - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the persistent descriptor env pointer as the current result - abi::emit_release_temporary_stack(emitter, prefix_bytes); // discard saved descriptor-prefix values after env storage - wrapper_label -} - -/// Returns true when a callback expression must preserve the selected runtime descriptor. -pub(crate) fn expr_call_needs_descriptor_callback_env(callback: &Expr, ctx: &Context) -> bool { - if runtime_callable_expr_result_needs_descriptor_callback_env(callback, ctx) { - return true; - } - - match &callback.kind { - ExprKind::Closure { captures, .. } => !captures.is_empty(), - ExprKind::FirstClassCallable(target) => first_class_target_needs_runtime_capture(target), - ExprKind::Variable(name) => callable_variable_needs_descriptor_callback_env(name, ctx), - ExprKind::Assignment { value, .. } => expr_produces_captured_callable(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => { - expr_produces_captured_callable(then_expr, ctx) - || expr_produces_captured_callable(else_expr, ctx) - } - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - expr_produces_captured_callable(value, ctx) - || expr_produces_captured_callable(default, ctx) - } - _ => false, - } -} - -/// Returns true when a runtime-produced callable result must keep its descriptor environment. -fn runtime_callable_expr_result_needs_descriptor_callback_env( - callback: &Expr, - ctx: &Context, -) -> bool { - if !matches!( - crate::codegen::functions::infer_contextual_type(callback, ctx).codegen_repr(), - PhpType::Callable - ) { - return false; - } - - match &callback.kind { - ExprKind::Variable(name) => callable_variable_needs_descriptor_callback_env(name, ctx), - ExprKind::ArrayAccess { .. } - | ExprKind::PropertyAccess { .. } - | ExprKind::DynamicPropertyAccess { .. } - | ExprKind::StaticPropertyAccess { .. } - | ExprKind::Assignment { .. } - | ExprKind::Ternary { .. } - | ExprKind::ShortTernary { .. } - | ExprKind::NullCoalesce { .. } - | ExprKind::FunctionCall { .. } - | ExprKind::MethodCall { .. } - | ExprKind::StaticMethodCall { .. } - | ExprKind::ExprCall { .. } => true, - _ => false, - } -} - -/// Returns true when a local callable variable should be carried as a descriptor. -fn callable_variable_needs_descriptor_callback_env(name: &str, ctx: &Context) -> bool { - if ctx.callable_param_names.contains(name) { - return true; - } - if ctx.runtime_callable_vars.contains(name) { - return true; - } - if ctx - .closure_captures - .get(name) - .is_some_and(|captures| !captures.is_empty()) - { - return true; - } - if ctx - .first_class_callable_targets - .get(name) - .is_some_and(first_class_target_needs_runtime_capture) - { - return true; - } - false -} - -/// Returns true when the selected descriptor can be owned safely by a callback environment. -pub(crate) fn descriptor_callback_env_supported(callback: &Expr) -> bool { - matches!( - callable_descriptor_result_ownership(callback), - HeapOwnership::Owned | HeapOwnership::Borrowed - ) -} - -/// Retains a borrowed descriptor result before later source-order argument evaluation. -pub(crate) fn retain_borrowed_descriptor_callback_result( - callback: &Expr, - emitter: &mut Emitter, -) -> bool { - if !matches!( - callable_descriptor_result_ownership(callback), - HeapOwnership::Borrowed - ) { - return false; - } - crate::codegen::callable_descriptor::emit_retain_current_descriptor(emitter); - true -} - -/// Emits a descriptor-backed callback environment from the current descriptor result. -pub(crate) fn emit_descriptor_callback_env_from_result( - callback: &Expr, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) -> Option { - emit_descriptor_callback_env_from_result_inner( - callback, - array_reg, - visible_arg_types, - descriptor_return_type, - true, - emitter, - ctx, - ) -} - -/// Emits a descriptor-backed callback environment from a descriptor already retained if borrowed. -pub(crate) fn emit_descriptor_callback_env_from_retained_result( - callback: &Expr, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) -> Option { - emit_descriptor_callback_env_from_result_inner( - callback, - array_reg, - visible_arg_types, - descriptor_return_type, - false, - emitter, - ctx, - ) -} - -/// Emits descriptor callback environment storage for a statically selected descriptor label. -pub(crate) fn emit_descriptor_callback_env_from_static_descriptor( - descriptor_label: &str, - visible_arg_types: Vec, - descriptor_prefix_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) -> DescriptorCallbackEnv { - let wrapper_label = ctx.next_label("descriptor_callback_wrapper"); - let env_slots = descriptor_prefix_types.len() + 2; - ctx.deferred_callback_wrappers.push(DeferredCallbackWrapper { - label: wrapper_label.clone(), - visible_arg_types, - target_visible_arg_types: None, - capture_types: Vec::new(), - descriptor_prefix_types, - descriptor_return_type: Some(descriptor_return_type), - }); - - let env_bytes = env_slots * 16; - let array_slot_offset = (env_slots - 1) * 16; - emitter.comment("static descriptor callback environment"); - abi::emit_reserve_temporary_stack(emitter, env_bytes); - let descriptor_reg = abi::int_result_reg(emitter); - abi::emit_symbol_address(emitter, descriptor_reg, descriptor_label); - store_current_result_to_env_slot(emitter, &PhpType::Callable, 0); - - DescriptorCallbackEnv { - wrapper_label, - env_bytes, - array_slot_offset, - } -} - -/// Emits a descriptor callback environment for a callable-array variable after saving an array. -/// -/// The caller must have pushed the runtime array pointer before calling this helper. If the -/// callback is a statically tracked callable-array variable, this evaluates any receiver prefix -/// at the callback-expression point, restores the saved array, and stores both into the new -/// descriptor environment. Returns `None` without touching the stack for unsupported callbacks. -pub(crate) fn emit_callable_array_descriptor_env_after_saved_array( - callback: &Expr, - array_reg: &str, - call_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let array_callback = resolve_callable_array_descriptor_callback(callback, ctx, data)?; - if let Some((receiver, _)) = &array_callback.receiver_prefix { - emit_expr(receiver, emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, abi::int_result_reg(emitter))); // preserve callable-array receiver while restoring the saved array - } - abi::emit_pop_reg(emitter, array_reg); - - let descriptor_prefix_types = array_callback - .receiver_prefix - .iter() - .map(|(_, ty)| ty.clone()) - .collect(); - let wrapper = emit_descriptor_callback_env_from_static_descriptor( - &array_callback.descriptor_label, - visible_arg_types, - descriptor_prefix_types, - descriptor_return_type, - emitter, - ctx, - ); - if let Some((_, receiver_ty)) = &array_callback.receiver_prefix { - emitter.instruction(&format!("mov {}, {}", abi::int_result_reg(emitter), call_reg)); // restore callable-array receiver for descriptor prefix storage - store_descriptor_callback_prefix_result(&wrapper, 0, receiver_ty, emitter); - } - store_descriptor_callback_array_reg(&wrapper, array_reg, emitter); - Some(wrapper) -} - -/// Stores the current result in a descriptor callback prefix slot. -pub(crate) fn store_descriptor_callback_prefix_result( - _env: &DescriptorCallbackEnv, - idx: usize, - ty: &PhpType, - emitter: &mut Emitter, -) { - store_current_result_to_env_slot(emitter, ty, (idx + 1) * 16); -} - -/// Stores a runtime array pointer register into the descriptor callback environment. -pub(crate) fn store_descriptor_callback_array_reg( - env: &DescriptorCallbackEnv, - array_reg: &str, - emitter: &mut Emitter, -) { - store_reg_to_env_slot(emitter, array_reg, env.array_slot_offset); -} - -/// Emits descriptor callback environment storage, optionally retaining borrowed descriptors. -fn emit_descriptor_callback_env_from_result_inner( - callback: &Expr, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - retain_borrowed: bool, - emitter: &mut Emitter, - ctx: &mut Context, -) -> Option { - let ownership = callable_descriptor_result_ownership(callback); - if !matches!(ownership, HeapOwnership::Owned | HeapOwnership::Borrowed) { - return None; - } - if retain_borrowed && matches!(ownership, HeapOwnership::Borrowed) { - crate::codegen::callable_descriptor::emit_retain_current_descriptor(emitter); - } - - let wrapper_label = ctx.next_label("descriptor_callback_wrapper"); - ctx.deferred_callback_wrappers.push(DeferredCallbackWrapper { - label: wrapper_label.clone(), - visible_arg_types, - target_visible_arg_types: None, - capture_types: Vec::new(), - descriptor_prefix_types: Vec::new(), - descriptor_return_type: Some(descriptor_return_type), - }); - - let env_bytes = 32; - let array_slot_offset = 16; - emitter.comment("descriptor callback environment"); - abi::emit_reserve_temporary_stack(emitter, env_bytes); - store_current_result_to_env_slot(emitter, &PhpType::Callable, 0); - store_reg_to_env_slot(emitter, array_reg, array_slot_offset); - - Some(DescriptorCallbackEnv { - wrapper_label, - env_bytes, - array_slot_offset, - }) -} - -/// Releases a descriptor-backed callback environment after its runtime helper returns. -pub(crate) fn release_descriptor_callback_env( - env: &DescriptorCallbackEnv, - emitter: &mut Emitter, -) { - let descriptor_reg = abi::int_result_reg(emitter); - abi::emit_push_reg(emitter, descriptor_reg); // preserve the callback runtime result while releasing the selected descriptor - abi::emit_load_temporary_stack_slot(emitter, descriptor_reg, 16); - crate::codegen::callable_descriptor::emit_release_current_descriptor(emitter); - abi::emit_pop_reg(emitter, descriptor_reg); // restore the callback runtime result after descriptor release - abi::emit_release_temporary_stack(emitter, env.env_bytes); -} - -/// Builds `$callback[$index]` for reading slots out of a stored callable array. -fn callable_array_slot_expr(var: &str, index: i64) -> Expr { - Expr::new( - ExprKind::ArrayAccess { - array: Box::new(Expr::new(ExprKind::Variable(var.to_string()), Span::dummy())), - index: Box::new(Expr::new(ExprKind::IntLiteral(index), Span::dummy())), - }, - Span::dummy(), - ) -} - -/// Resolves a static callable receiver against the current codegen class context. -fn resolve_static_receiver_class(receiver: &StaticReceiver, ctx: &Context) -> Option { - match receiver { - StaticReceiver::Named(name) => resolve_class_name(ctx, name.as_str()).map(str::to_string), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.parent.clone()), - } -} - -/// Resolves class names case-insensitively against the codegen class table. -fn resolve_class_name<'a>(ctx: &'a Context, class_name: &str) -> Option<&'a str> { - let class_key = php_symbol_key(class_name.trim_start_matches('\\')); - ctx.classes - .keys() - .find(|existing| php_symbol_key(existing) == class_key) - .map(String::as_str) -} - -/// Returns the ownership class for a callable descriptor expression result. -fn callable_descriptor_result_ownership(callback: &Expr) -> HeapOwnership { - match &callback.kind { - ExprKind::Assignment { .. } => HeapOwnership::Borrowed, - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => callable_descriptor_result_ownership(then_expr) - .merge(callable_descriptor_result_ownership(else_expr)), - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - callable_descriptor_result_ownership(value) - .merge(callable_descriptor_result_ownership(default)) - } - _ => expr_result_heap_ownership(callback), - } -} - -/// Returns true if an expression produces a callable with descriptor-owned environment. -fn expr_produces_captured_callable(expr: &Expr, ctx: &Context) -> bool { - match &expr.kind { - ExprKind::Closure { captures, .. } => !captures.is_empty(), - ExprKind::FirstClassCallable(target) => first_class_target_needs_runtime_capture(target), - ExprKind::Variable(name) => { - ctx.closure_captures - .get(name) - .is_some_and(|captures| !captures.is_empty()) - || ctx - .first_class_callable_targets - .get(name) - .is_some_and(first_class_target_needs_runtime_capture) - } - ExprKind::Assignment { value, .. } => expr_produces_captured_callable(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => { - expr_produces_captured_callable(then_expr, ctx) - || expr_produces_captured_callable(else_expr, ctx) - } - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - expr_produces_captured_callable(value, ctx) - || expr_produces_captured_callable(default, ctx) - } - _ => false, - } -} - -/// Returns true when a first-class callable target carries receiver environment. -fn first_class_target_needs_runtime_capture(target: &CallableTarget) -> bool { - matches!( - target, - CallableTarget::Method { .. } - | CallableTarget::StaticMethod { - receiver: StaticReceiver::Static, - .. - } - ) -} - -/// Loads a value from an environment slot into `reg` by computing the slot address on the -/// temporary stack and performing a type-aware load. -pub(crate) fn load_env_slot_to_reg(emitter: &mut Emitter, reg: &str, offset: usize) { - let scratch = abi::symbol_scratch_reg(emitter); - abi::emit_temporary_stack_address(emitter, scratch, offset); - abi::emit_load_from_address(emitter, reg, scratch, 0); -} - -/// Emits the address of the base of the temporary callback environment stack frame into `reg`. -/// Used by the deferred wrapper to locate the environment. -pub(crate) fn load_env_pointer_to_reg(emitter: &mut Emitter, reg: &str) { - abi::emit_temporary_stack_address(emitter, reg, 0); -} - -/// Stores the raw value in `reg` directly into the environment slot at `offset` using a -/// temporary stack address scratch register. -fn store_reg_to_env_slot(emitter: &mut Emitter, reg: &str, offset: usize) { - let scratch = abi::symbol_scratch_reg(emitter); - abi::emit_temporary_stack_address(emitter, scratch, offset); - abi::emit_store_to_address(emitter, reg, scratch, 0); -} - -/// Stores the current ABI result register(s) into the environment slot at `offset` using a -/// temporary stack address scratch register. Handles float, string (ptr+len), and integer -/// representations per `ty.codegen_repr()`. No-op for `Void`/`Never` types. -fn store_current_result_to_env_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - let scratch = abi::symbol_scratch_reg(emitter); - abi::emit_temporary_stack_address(emitter, scratch, offset); - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), scratch, 0); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_store_to_address(emitter, ptr_reg, scratch, 0); - abi::emit_store_to_address(emitter, len_reg, scratch, 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), scratch, 0); - } - } -} - -/// Stores saved callback to persistent env into runtime storage or stack state. -fn store_saved_callback_to_persistent_env(emitter: &mut Emitter) { - let env_reg = abi::symbol_scratch_reg(emitter); - let callback_reg = abi::secondary_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, env_reg, 0); - abi::emit_load_temporary_stack_slot(emitter, callback_reg, 16); - abi::emit_store_to_address(emitter, callback_reg, env_reg, 0); -} - -/// Stores current result to persistent env slot into runtime storage or stack state. -fn store_current_result_to_persistent_env_slot( - emitter: &mut Emitter, - ty: &PhpType, - offset: usize, -) { - let env_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, env_reg, 0); - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), env_reg, offset); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_store_to_address(emitter, ptr_reg, env_reg, offset); - abi::emit_store_to_address(emitter, len_reg, env_reg, offset + 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), env_reg, offset); - } - } -} - -/// Loads a temporary-stack value into the standard expression result registers. -fn load_temporary_stack_slot_to_current_result(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_load_temporary_stack_slot(emitter, abi::float_result_reg(emitter), offset); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, offset); - abi::emit_load_temporary_stack_slot(emitter, len_reg, offset + 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), offset); - } - } -} - -/// Retains persistent capture result so ownership remains valid across runtime calls. -fn retain_persistent_capture_result(emitter: &mut Emitter, ty: &PhpType) { - match ty.codegen_repr() { - PhpType::Str => { - let (ptr_reg, _) = abi::string_result_regs(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, {}", ptr_reg)); // pass the captured string pointer to the retain helper - emitter.instruction("bl __rt_incref"); // retain the captured string for the persistent callback env - } - Arch::X86_64 => { - if ptr_reg != "rax" { - emitter.instruction(&format!("mov rax, {}", ptr_reg)); // pass the captured string pointer to the retain helper - } - emitter.instruction("call __rt_incref"); // retain the captured string for the persistent callback env - } - } - } - other if other.is_refcounted() => { - abi::emit_incref_if_refcounted(emitter, &other); - } - _ => {} - } -} diff --git a/src/codegen/builtins/arrays/count.rs b/src/codegen/builtins/arrays/count.rs deleted file mode 100644 index a2001220d2..0000000000 --- a/src/codegen/builtins/arrays/count.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Purpose: -//! Emits PHP `count` calls for arrays and countable runtime values. -//! Loads lengths from typed array layouts or boxed runtime structures as needed. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Ownership state is observed but count must not consume or mutate the counted value. - -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::codegen::expr::objects::dispatch::emit_dispatch_instance_method; -use crate::codegen::expr::{emit_expr, expr_result_heap_ownership}; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `count` builtin call. -/// -/// Dispatches based on the argument's type: -/// - Objects implementing `Countable`: calls the instance method via `emit_dispatch_instance_method` -/// - `Mixed` type: calls runtime helper `__rt_mixed_count` which reads the array/hash header -/// - Owned refcounted arrays/hashes: reads element count directly from header at offset 0 -/// - Borrowed refcounted arrays/hashes: increments refcount before reading to prevent aliasing -/// -/// Returns `PhpType::Int` on success. Does not consume or mutate the counted value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("count()"); - let source_ty = emit_expr(&args[0], emitter, ctx, data); - - if let PhpType::Object(class_name) = &source_ty { - if class_implements_countable(class_name, ctx) { - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // forward Countable receiver to first arg slot - } - emit_dispatch_instance_method(class_name, "count", emitter, ctx); - return Some(PhpType::Int); - } - } - - let source_repr = source_ty.codegen_repr(); - let result_reg = abi::int_result_reg(emitter); - - if matches!(source_repr, PhpType::Mixed) { - // Mixed receivers: unbox the cell at runtime and dispatch to the - // shared count helper, which reads the array/hash header. Returns - // 0 when the boxed payload is not a container, mirroring PHP's - // long-standing "count(): Argument is not countable" warning - // behavior collapsed to a quiet zero for the most common idiom - // (count(json_decode($json, true))). - abi::emit_call_label(emitter, "__rt_mixed_count"); // unbox the Mixed cell and read the array/hash count from its payload header - return Some(PhpType::Int); - } - - if source_repr.is_refcounted() && expr_result_heap_ownership(&args[0]) != HeapOwnership::Owned { - let count_reg = abi::temp_int_reg(emitter.target); - abi::emit_incref_if_refcounted(emitter, &source_repr); // retain borrowed heap-backed arrays or hashes before reading their header in place - abi::emit_push_reg(emitter, result_reg); // preserve the retained heap pointer while extracting the element count - abi::emit_load_from_address(emitter, count_reg, result_reg, 0); // load the element count from the first header field without consuming the retained pointer - abi::emit_pop_reg(emitter, result_reg); // restore the retained heap pointer as the decref helper argument - abi::emit_push_reg(emitter, count_reg); // preserve the computed count across the decref helper call - abi::emit_decref_if_refcounted(emitter, &source_repr); // release the temporary owner once the header count has been captured - abi::emit_pop_reg(emitter, result_reg); // restore the computed count as the builtin integer result - } else { - // -- read element count from array/hash header -- - abi::emit_load_from_address(emitter, result_reg, result_reg, 0); // load element count directly when the current expression already owns its heap payload - } - - Some(PhpType::Int) -} - -/// Returns `true` if the given class implements the `Countable` interface. -fn class_implements_countable(class_name: &str, ctx: &Context) -> bool { - ctx.classes - .get(class_name) - .map(|info| info.interfaces.iter().any(|i| i == "Countable")) - .unwrap_or(false) -} diff --git a/src/codegen/builtins/arrays/descriptor_arg_builder.rs b/src/codegen/builtins/arrays/descriptor_arg_builder.rs deleted file mode 100644 index 20f9208c0e..0000000000 --- a/src/codegen/builtins/arrays/descriptor_arg_builder.rs +++ /dev/null @@ -1,416 +0,0 @@ -//! Purpose: -//! Builds raw descriptor-invoker argument arrays without generic array-literal spread lowering. -//! Handles positional prefixes followed by indexed spread sources for callable invoker paths. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::callable_forms` -//! - `crate::codegen::expr::calls::descriptor_invoker_args` -//! -//! Key details: -//! - The destination array uses boxed Mixed slots so descriptor invokers can apply metadata at runtime. -//! - Spread sources are cloned to Mixed slots before merging, preserving string lengths and refcounted payloads. - -use crate::codegen::abi; -use crate::codegen::builtins::arrays::call_user_func_array; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{emit_expr, expr_result_heap_ownership}; -use crate::codegen::expr::arrays::emit_array_value_type_stamp; -use crate::codegen::expr::calls::args as call_args; -use crate::codegen::functions; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; - -/// Emits an indexed Mixed argument array, optionally storing variable args as ref-cell markers. -pub(crate) fn emit_indexed_invoker_arg_array( - args: &[Expr], - encode_variable_refs: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("descriptor invoker indexed argument array"); - emit_new_mixed_indexed_array(args.len().max(4), emitter); - emit_array_value_type_stamp(emitter, abi::int_result_reg(emitter), &PhpType::Mixed); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the descriptor argument array alive while filling Mixed slots - - for (i, arg) in args.iter().enumerate() { - if encode_variable_refs { - if let ExprKind::Variable(var_name) = &arg.kind { - if !call_args::emit_ref_arg_variable_address( - var_name, - "descriptor invoker arg", - emitter, - ctx, - ) { - panic!("descriptor invoker argument variable not found"); - } - emit_box_current_ref_arg_address_for_invoker(var_name, emitter, ctx); - emit_store_current_mixed_slot(i, emitter); - continue; - } - } - - emit_store_next_mixed_slot(arg, i, emitter, ctx, data); - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the filled descriptor argument array - PhpType::Array(Box::new(PhpType::Mixed)) -} - -/// Emits an indexed Mixed argument array with a saved object receiver in slot zero. -pub(crate) fn emit_indexed_invoker_arg_array_with_saved_object_prefix( - object_stack_offset: usize, - args: &[Expr], - sig: Option<&FunctionSig>, - encode_variable_refs: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("descriptor invoker receiver-prefixed indexed argument array"); - emit_new_mixed_indexed_array((args.len() + 1).max(4), emitter); - emit_array_value_type_stamp(emitter, abi::int_result_reg(emitter), &PhpType::Mixed); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the receiver-prefixed descriptor argument array alive while filling Mixed slots - emit_store_saved_object_prefix_slot(object_stack_offset + 16, 0, emitter); - - for (idx, arg) in args.iter().enumerate() { - emit_store_invoker_arg_slot( - arg, - idx + 1, - sig, - encode_variable_refs, - "descriptor invoker receiver-prefixed arg", - emitter, - ctx, - data, - ); - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the filled receiver-prefixed descriptor argument array - PhpType::Array(Box::new(PhpType::Mixed)) -} - -/// Emits a raw indexed argument array for positional args plus indexed spreads. -pub(crate) fn emit_positional_spread_invoker_arg_array( - leading_args: &[Expr], - args: &[Expr], - sig: Option<&FunctionSig>, - encode_variable_refs: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let plan = positional_spread_plan(args, ctx)?; - emitter.comment("descriptor invoker positional spread argument array"); - emit_new_mixed_indexed_array((leading_args.len() + plan.prefix_args.len()).max(16), emitter); - emit_array_value_type_stamp(emitter, abi::int_result_reg(emitter), &PhpType::Mixed); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the descriptor argument array alive while positional slots and spreads are appended - - let mut slot = 0usize; - for arg in leading_args { - emit_store_invoker_arg_slot( - arg, - slot, - sig, - encode_variable_refs, - "descriptor invoker leading arg", - emitter, - ctx, - data, - ); - slot += 1; - } - for arg in plan.prefix_args { - emit_store_invoker_arg_slot( - arg, - slot, - sig, - encode_variable_refs, - "descriptor invoker spread-prefix arg", - emitter, - ctx, - data, - ); - slot += 1; - } - for (spread, elem_ty) in plan.spreads { - emit_merge_indexed_spread(spread, &elem_ty, emitter, ctx, data); - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed positional-spread argument array - Some(PhpType::Array(Box::new(PhpType::Mixed))) -} - -/// Emits a raw indexed argument array with a saved object receiver followed by positional args/spreads. -#[allow(clippy::too_many_arguments)] -pub(crate) fn emit_positional_spread_invoker_arg_array_with_saved_object_prefix( - object_stack_offset: usize, - args: &[Expr], - sig: Option<&FunctionSig>, - encode_variable_refs: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let plan = positional_spread_plan(args, ctx)?; - emitter.comment("descriptor invoker receiver-prefixed positional spread argument array"); - emit_new_mixed_indexed_array((plan.prefix_args.len() + 1).max(16), emitter); - emit_array_value_type_stamp(emitter, abi::int_result_reg(emitter), &PhpType::Mixed); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the receiver-prefixed descriptor argument array alive while positional slots and spreads are appended - - let mut slot = 0usize; - emit_store_saved_object_prefix_slot(object_stack_offset + 16, slot, emitter); - slot += 1; - for arg in plan.prefix_args { - emit_store_invoker_arg_slot( - arg, - slot, - sig, - encode_variable_refs, - "descriptor invoker receiver-prefixed spread-prefix arg", - emitter, - ctx, - data, - ); - slot += 1; - } - for (spread, elem_ty) in plan.spreads { - emit_merge_indexed_spread(spread, &elem_ty, emitter, ctx, data); - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed receiver-prefixed positional-spread argument array - Some(PhpType::Array(Box::new(PhpType::Mixed))) -} - -/// Stores one positional descriptor argument, preserving variable storage when runtime by-ref metadata may need it. -#[allow(clippy::too_many_arguments)] -fn emit_store_invoker_arg_slot( - arg: &Expr, - index: usize, - sig: Option<&FunctionSig>, - encode_variable_refs: bool, - context_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if should_encode_variable_ref_arg(sig, index, arg, encode_variable_refs) { - if let ExprKind::Variable(var_name) = &arg.kind { - if !call_args::emit_ref_arg_variable_address(var_name, context_label, emitter, ctx) { - panic!("descriptor invoker argument variable not found"); - } - emit_box_current_ref_arg_address_for_invoker(var_name, emitter, ctx); - emit_store_current_mixed_slot(index, emitter); - return; - } - } - - emit_store_next_mixed_slot(arg, index, emitter, ctx, data); -} - -/// Returns true when this positional descriptor slot should carry an invoker ref-cell marker. -fn should_encode_variable_ref_arg( - sig: Option<&FunctionSig>, - index: usize, - arg: &Expr, - encode_variable_refs: bool, -) -> bool { - encode_variable_refs - && matches!(arg.kind, ExprKind::Variable(_)) - && sig.is_none_or(|sig| sig.ref_params.get(index).copied().unwrap_or(false)) -} - -/// Plans a positional-only argument list with one or more indexed spread tails. -fn positional_spread_plan<'a>(args: &'a [Expr], ctx: &Context) -> Option> { - let mut prefix_args = Vec::new(); - let mut spreads = Vec::new(); - let mut seen_spread = false; - - for arg in args { - match &arg.kind { - ExprKind::Spread(inner) => { - seen_spread = true; - let elem_ty = indexed_spread_element_type(inner, ctx)?; - spreads.push((inner.as_ref(), elem_ty)); - } - ExprKind::NamedArg { .. } => return None, - _ if seen_spread => return None, - _ => prefix_args.push(arg), - } - } - - if spreads.is_empty() { - return None; - } - - Some(PositionalSpreadPlan { - prefix_args, - spreads, - }) -} - -/// Boxes a saved object pointer and stores it into a descriptor argument slot. -fn emit_store_saved_object_prefix_slot( - object_stack_offset: usize, - index: usize, - emitter: &mut Emitter, -) { - let object_reg = abi::secondary_scratch_reg(emitter); - let zero_reg = abi::tertiary_scratch_reg(emitter); - let tag_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, object_reg, object_stack_offset); - abi::emit_load_int_immediate(emitter, zero_reg, 0); - abi::emit_load_int_immediate( - emitter, - tag_reg, - crate::codegen::runtime_value_tag(&PhpType::Object(String::new())) as i64, - ); - crate::codegen::emit_box_runtime_payload_as_mixed(emitter, tag_reg, object_reg, zero_reg); - emit_store_current_mixed_slot(index, emitter); -} - -/// Returns the element type for a spread source when it is statically indexed-array-shaped. -fn indexed_spread_element_type(spread: &Expr, ctx: &Context) -> Option { - match functions::infer_contextual_type(spread, ctx).codegen_repr() { - PhpType::Array(elem_ty) => Some(*elem_ty), - _ => None, - } -} - -/// Allocates an indexed array with Mixed slots for descriptor invoker arguments. -fn emit_new_mixed_indexed_array(capacity: usize, emitter: &mut Emitter) { - let capacity_reg = abi::int_arg_reg_name(emitter.target, 0); - let elem_size_reg = abi::int_arg_reg_name(emitter.target, 1); - abi::emit_load_int_immediate(emitter, capacity_reg, capacity as i64); - abi::emit_load_int_immediate(emitter, elem_size_reg, 8); - abi::emit_call_label(emitter, "__rt_array_new"); -} - -/// Boxes `arg` as Mixed and stores it into the destination slot at `index`. -fn emit_store_next_mixed_slot( - arg: &Expr, - index: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let mut ty = emit_expr(arg, emitter, ctx, data); - let boxed_iterable = crate::codegen::emit_box_iterable_value_for_mixed_container( - emitter, - &mut ty, - ); - if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - crate::codegen::emit_box_current_expr_value_as_mixed_for_container(emitter, arg, &ty); - } else if !boxed_iterable { - retain_borrowed_mixed_arg(emitter, arg, &ty); - } - emit_store_current_mixed_slot(index, emitter); -} - -/// Retains a borrowed Mixed payload before storing it in the invoker container. -fn retain_borrowed_mixed_arg(emitter: &mut Emitter, arg: &Expr, ty: &PhpType) { - if ty.codegen_repr().is_refcounted() && expr_result_heap_ownership(arg) != HeapOwnership::Owned { - abi::emit_incref_if_refcounted(emitter, &ty.codegen_repr()); - } -} - -/// Stores the current boxed Mixed value into the destination argument array. -fn emit_store_current_mixed_slot(index: usize, emitter: &mut Emitter) { - let array_reg = abi::symbol_scratch_reg(emitter); - let len_reg = abi::secondary_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, array_reg, 0); - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), array_reg, 24 + index * 8); - abi::emit_load_int_immediate(emitter, len_reg, (index + 1) as i64); - abi::emit_store_to_address(emitter, len_reg, array_reg, 0); -} - -/// Boxes the current variable storage address as an invoker-only Mixed marker. -pub(crate) fn emit_box_current_ref_arg_address_for_invoker( - var_name: &str, - emitter: &mut Emitter, - ctx: &Context, -) { - let ref_cell_reg = abi::secondary_scratch_reg(emitter); - let marker_tag_reg = abi::tertiary_scratch_reg(emitter); - let source_tag_reg = abi::symbol_scratch_reg(emitter); - emitter.instruction(&format!("mov {}, {}", ref_cell_reg, abi::int_result_reg(emitter))); // preserve the source variable storage address before Mixed marker boxing - abi::emit_load_int_immediate( - emitter, - marker_tag_reg, - call_user_func_array::INVOKER_ARG_REF_CELL_TAG, - ); - abi::emit_load_int_immediate( - emitter, - source_tag_reg, - variable_runtime_value_tag(var_name, ctx) as i64, - ); - crate::codegen::emit_box_runtime_payload_as_mixed( - emitter, - marker_tag_reg, - ref_cell_reg, - source_tag_reg, - ); -} - -/// Returns the runtime tag for a variable's current codegen type. -fn variable_runtime_value_tag(var_name: &str, ctx: &Context) -> u8 { - ctx.variables - .get(var_name) - .map(|var| crate::codegen::runtime_value_tag(&var.ty.codegen_repr())) - .unwrap_or_else(|| crate::codegen::runtime_value_tag(&PhpType::Int)) -} - -/// Appends an indexed spread source to the destination Mixed argument array. -fn emit_merge_indexed_spread( - spread: &Expr, - inferred_elem_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let spread_ty = emit_expr(spread, emitter, ctx, data); - let elem_ty = match spread_ty { - PhpType::Array(elem_ty) => *elem_ty, - _ => inferred_elem_ty.clone(), - }; - call_user_func_array::emit_clone_indexed_array_for_invoker( - abi::int_result_reg(emitter), - &elem_ty, - emitter, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the cloned Mixed spread source while merging it into the destination array - - let dest_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let source_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - abi::emit_load_temporary_stack_slot(emitter, dest_arg_reg, 16); - abi::emit_load_temporary_stack_slot(emitter, source_arg_reg, 0); - abi::emit_call_label(emitter, "__rt_array_merge_into_refcounted"); - abi::emit_store_to_address( - emitter, - abi::int_result_reg(emitter), - temporary_stack_reg(emitter), - 16, - ); - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the merged destination while releasing the cloned spread source - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, &PhpType::Array(Box::new(PhpType::Mixed))); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the merged descriptor argument array after source-clone release - abi::emit_release_temporary_stack(emitter, 16); // discard the cloned spread-source stack slot -} - -/// Returns the active stack pointer register for direct temporary-slot stores. -fn temporary_stack_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "sp", - crate::codegen::platform::Arch::X86_64 => "rsp", - } -} - -/// Borrowed view of a positional-spread descriptor argument list. -struct PositionalSpreadPlan<'a> { - prefix_args: Vec<&'a Expr>, - spreads: Vec<(&'a Expr, PhpType)>, -} diff --git a/src/codegen/builtins/arrays/ensure_unique_arg.rs b/src/codegen/builtins/arrays/ensure_unique_arg.rs deleted file mode 100644 index a6748cace7..0000000000 --- a/src/codegen/builtins/arrays/ensure_unique_arg.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Purpose: -//! Emits copy-on-write uniqueness checks for array arguments before in-place mutation. -//! Centralizes the COW guard shared by PHP mutating array builtins. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::*::emit() for mutating array builtins`. -//! -//! Key details: -//! - The array pointer may change after uniqueness repair, so callers must store it back when mutating by reference. - -use crate::codegen::abi; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::types::PhpType; - -/// Emits a copy-on-write uniqueness check for array arguments before in-place mutation. -/// -/// On ARM64: uses `bl` to call `__rt_array_ensure_unique` or `__rt_hash_ensure_unique`. -/// On x86_64: moves the array pointer from `rax` into `rdi` (first argument register), then calls the runtime function. -/// -/// After the call, the array pointer in `rax` may have changed due to COW repair. Callers -/// must store the returned pointer back when mutating by reference. -pub(crate) fn emit_ensure_unique_arg(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Array(_) => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("bl __rt_array_ensure_unique"); // split shared indexed arrays before a mutating builtin runs - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the candidate indexed-array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_array_ensure_unique"); // split shared indexed arrays before a mutating builtin runs - } - } - } - PhpType::AssocArray { .. } => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("bl __rt_hash_ensure_unique"); // split shared associative arrays before a mutating builtin runs - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the candidate associative-array pointer into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_hash_ensure_unique"); // split shared associative arrays before a mutating builtin runs - } - } - } - _ => {} - } -} diff --git a/src/codegen/builtins/arrays/function_exists.rs b/src/codegen/builtins/arrays/function_exists.rs deleted file mode 100644 index 77b6ed511d..0000000000 --- a/src/codegen/builtins/arrays/function_exists.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Purpose: -//! Emits PHP `function_exists` checks for builtins, user functions, and include variants. -//! Connects codegen-visible declarations to PHP runtime boolean results. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Builtin checks must reflect the canonical catalog so case-insensitive and namespace fallback behavior stays coherent. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::codegen::platform::Arch; -use crate::name_resolver::is_date_procedural_alias; -use crate::names::function_variant_active_symbol; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -use super::super::callable_lookup::{lookup_function, FunctionLookup}; - -/// Emits a `function_exists` check for builtins, user functions, and include variants. -/// -/// # Arguments -/// - `args[0]` must be a `StringLiteral` containing the function name to check. -/// The function name is resolved case-insensitively per PHP semantics. -/// -/// # Behavior -/// - For include variants: emits code that loads and compares the variant's active-symbol -/// pointer at runtime, returning true only when an include has activated that variant. -/// - For builtins, externs, and user functions: emits constant `1` (function exists). -/// - For unknown names: emits constant `0`. -/// -/// # Returns -/// Always `PhpType::Bool`. -/// -/// # Panics -/// If `args[0]` is not a `StringLiteral`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("function_exists()"); - - // -- resolve function name at compile time -- - let func_name = match &args[0].kind { - ExprKind::StringLiteral(name) => name.clone(), - _ => panic!("function_exists() argument must be a string literal"), - }; - - // -- emit constant true/false based on whether function is known -- - // Procedural date/time aliases (date_create, idate, gmstrftime, ...) are recognized as - // existing functions even though the resolver rewrites them into OOP/built-in expressions - // before they reach the builtin catalog. Mirrors PHP's function_exists() behavior. - if is_date_procedural_alias(&func_name) { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1); - return Some(PhpType::Bool); - } - match lookup_function(ctx, &func_name) { - Some(FunctionLookup::IncludeVariant(variant_name)) => { - emit_variant_function_exists(&variant_name, emitter, data); - return Some(PhpType::Bool); - } - Some( - FunctionLookup::Builtin(_) - | FunctionLookup::Extern(_) - | FunctionLookup::UserFunction(_), - ) => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1); - } - None => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - } - - Some(PhpType::Bool) -} - -/// Emits code to check whether a named include-variant function is currently active. -/// -/// # Arguments -/// - `func_name`: the variant function name (e.g. `variant_foo__include_1`). -/// -/// # Behavior -/// - Reserves 8 bytes of BSS for the variant's active-symbol pointer via `data.add_comm`. -/// - Loads the symbol address into `int_result_reg`. -/// - On ARM64: compares to 0 and uses `cset` to produce a boolean (1 = active, 0 = inactive). -/// - On x86_64: uses `test`/`setne`/`movzx` to produce a widened integer boolean. -/// -/// # Output -/// Writes a 0/1 integer result to `int_result_reg` per the target ABI. -fn emit_variant_function_exists( - func_name: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let active_symbol = function_variant_active_symbol(func_name); - data.add_comm(active_symbol.clone(), 8); - let result_reg = abi::int_result_reg(emitter); - abi::emit_load_symbol_to_reg(emitter, result_reg, &active_symbol, 0); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #0", result_reg)); // test whether an include has activated this function variant - emitter.instruction(&format!("cset {}, ne", result_reg)); // return true only when a function variant is active - } - Arch::X86_64 => { - emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // test whether an include has activated this function variant - emitter.instruction("setne al"); // return true only when a function variant is active - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } -} diff --git a/src/codegen/builtins/arrays/hash_value_type_tag.rs b/src/codegen/builtins/arrays/hash_value_type_tag.rs deleted file mode 100644 index 47157136f2..0000000000 --- a/src/codegen/builtins/arrays/hash_value_type_tag.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Maps PHP element types to runtime hash-array value tags. -//! Provides the compact tag contract used when building associative array payloads. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::{array_combine,array_fill_keys}::emit()`. -//! -//! Key details: -//! - Tag values must stay synchronized with runtime hash helpers that interpret Mixed and typed payloads. - -use crate::types::PhpType; - -/// Maps a `PhpType` to its corresponding runtime hash-array value tag. -/// -/// The returned tag is embedded in hash table payloads to identify the type of -/// each stored value. Tags `0`–`10` map to specific PHP types; tag `7` is used -/// as a fallback for `Mixed`, `Union`, and `Iterable` since they can hold any type. -/// -/// # Arguments -/// * `ty` — the PHP type to map to a tag -/// -/// # Returns -/// A `u8` tag value in range `0..=10` identifying the value type for hash storage. -pub(super) fn hash_value_type_tag(ty: &PhpType) -> u8 { - match ty { - PhpType::Int => 0, - PhpType::Str => 1, - PhpType::Float => 2, - PhpType::Bool => 3, - PhpType::Array(_) => 4, - PhpType::AssocArray { .. } => 5, - PhpType::Object(_) => 6, - PhpType::Mixed => 7, - PhpType::Union(_) => 7, - PhpType::Iterable => 7, - PhpType::Void => 8, - PhpType::Resource(_) => 9, - PhpType::Callable => 10, - PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) | PhpType::Never => 0, - PhpType::TaggedScalar => { - unreachable!("TaggedScalar must be narrowed or boxed before hash storage") - } - } -} diff --git a/src/codegen/builtins/arrays/in_array.rs b/src/codegen/builtins/arrays/in_array.rs deleted file mode 100644 index b538a6a49f..0000000000 --- a/src/codegen/builtins/arrays/in_array.rs +++ /dev/null @@ -1,388 +0,0 @@ -//! Purpose: -//! Emits PHP `in_array` builtin calls for array values. -//! Materializes arguments and delegates payload work to the matching runtime helper or inline lowering. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Array element type and ownership assumptions must match the type checker and runtime layout. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `in_array(needle, array, strict)` builtin call. -/// -/// Searches for `needle` (args[0]) in `array` (args[1]) using linear/insertion-order -/// iteration. Always returns `Some(PhpType::Int)` (1=true, 0=false). Strict mode -/// (args[2]) is accepted but not yet implemented. -/// -/// # Stack layout (associative arrays) -/// - sp+0: iter_index (16 bytes) -/// - sp+16: needle (16 bytes) -/// - sp+32: hash_table_ptr (16 bytes) -/// -/// # Stack layout (indexed string arrays) -/// - sp+0: needle ptr+len (16 bytes) -/// - sp+16: array pointer (16 bytes) -/// -/// # ABI constraints -/// - Needle evaluated after array (source order preserved). -/// - For associative arrays: hash-table pointer preserved in `int_result_reg` during needle evaluation. -/// - For string needles in assoc arrays: string ptr+len preserved in string result regs. -/// - For indexed arrays: array pointer preserved in `int_result_reg` during needle evaluation. -/// - Result returned in `int_result_reg` (x0 on ARM64, rax on x86_64). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("in_array()"); - - // -- evaluate array (second arg) first to get its type -- - let arr_ty = emit_expr(&args[1], emitter, ctx, data); - - if let PhpType::AssocArray { value, .. } = &arr_ty { - let val_ty = *value.clone(); - // -- save hash table pointer, evaluate needle -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the associative-array hash-table pointer while evaluating the searched needle - - let needle_ty = emit_expr(&args[0], emitter, ctx, data); - - let found_label = ctx.next_label("in_array_assoc_found"); - let end_label = ctx.next_label("in_array_assoc_end"); - let loop_label = ctx.next_label("in_array_assoc_loop"); - let skip_label = ctx.next_label("in_array_assoc_skip"); - let mixed_mismatch_label = ctx.next_label("in_array_assoc_mixed_mismatch"); - - match &val_ty { - PhpType::Str => { - // -- needle is a string in x1/x2, save it -- - abi::emit_push_reg_pair(emitter, abi::string_result_regs(emitter).0, abi::string_result_regs(emitter).1); // preserve the string needle across associative-array iteration - } - PhpType::Mixed if matches!(needle_ty, PhpType::Str) => { - abi::emit_push_reg_pair(emitter, abi::string_result_regs(emitter).0, abi::string_result_regs(emitter).1); // preserve the string needle across mixed associative-array iteration - } - PhpType::Mixed if matches!(needle_ty, PhpType::Float) => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmov x0, d0"); // move the float needle bits into an integer register for mixed-entry comparison - } - Arch::X86_64 => { - emitter.instruction("movq rax, xmm0"); // move the float needle bits into the integer result register for mixed-entry comparison - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the float needle bits across associative-array iteration - } - _ => { - // -- needle is an integer/bool in x0, save it -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the scalar needle across associative-array iteration - } - } - - // -- push iteration index onto stack -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str xzr, [sp, #-16]!"); // push iter_cursor = 0 (start from the associative-array header head slot) - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one temporary stack slot for the associative-array iterator cursor - emitter.instruction("mov QWORD PTR [rsp], 0"); // initialize the associative-array iterator cursor to the hash-header head sentinel - } - } - - // Stack layout (top to bottom): - // sp+0: iter_index (16 bytes) - // sp+16: needle (16 bytes) - // sp+32: hash_table_ptr (16 bytes) - - emitter.label(&loop_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #32]"); // load the associative-array hash-table pointer for the next insertion-order iteration step - emitter.instruction("ldr x1, [sp]"); // load the current associative-array iterator cursor - emitter.instruction("bl __rt_hash_iter_next"); // advance one associative-array insertion-order entry and return its key plus payload - emitter.instruction("cmn x0, #1"); // has associative-array iteration reached the done sentinel? - emitter.instruction(&format!("b.eq {}", end_label)); // stop searching once the associative-array iterator is exhausted - emitter.instruction("str x0, [sp]"); // save the updated associative-array iterator cursor for the next loop step - - match &val_ty { - PhpType::Str => { - emitter.instruction("mov x1, x3"); // move the associative-array entry string pointer into the first string-compare register - emitter.instruction("mov x2, x4"); // move the associative-array entry string length into the paired string-compare register - emitter.instruction("ldp x3, x4, [sp, #16]"); // reload the saved string needle from the associative-array search stack frame - emitter.instruction("bl __rt_str_eq"); // compare the associative-array entry string value against the searched needle - emitter.instruction(&format!("cbnz x0, {}", found_label)); // stop once the associative-array string value matches the searched needle - } - PhpType::Mixed => { - let expected_tag = crate::codegen::runtime_value_tag(&needle_ty); - emitter.instruction(&format!("mov x6, #{}", expected_tag)); // materialize the expected mixed-entry runtime tag for the searched needle - emitter.instruction("cmp x5, x6"); // does the current associative-array mixed entry match the searched needle kind? - emitter.instruction(&format!("b.ne {}", mixed_mismatch_label)); // skip associative-array entries whose mixed kind differs from the needle - match &needle_ty { - PhpType::Str => { - emitter.instruction("mov x1, x3"); // move the associative-array mixed entry string pointer into the first string-compare register - emitter.instruction("mov x2, x4"); // move the associative-array mixed entry string length into the paired string-compare register - emitter.instruction("ldp x3, x4, [sp, #16]"); // reload the saved string needle from the associative-array search stack frame - emitter.instruction("bl __rt_str_eq"); // compare the associative-array mixed string entry against the searched needle - emitter.instruction(&format!("cbnz x0, {}", found_label)); // stop once the associative-array mixed string value matches the needle - } - PhpType::Void => { - emitter.instruction(&format!("b {}", found_label)); // null needles match associative-array entries tagged null - } - _ => { - emitter.instruction("ldr x6, [sp, #16]"); // reload the saved scalar mixed needle payload from the associative-array search stack frame - emitter.instruction("cmp x3, x6"); // compare the associative-array mixed entry payload against the searched scalar needle - emitter.instruction(&format!("b.eq {}", found_label)); // stop once the associative-array mixed scalar payload matches the needle - } - } - emitter.label(&mixed_mismatch_label); - } - _ => { - emitter.instruction("ldr x5, [sp, #16]"); // reload the saved scalar needle payload from the associative-array search stack frame - emitter.instruction("cmp x3, x5"); // compare the associative-array entry payload against the searched scalar needle - emitter.instruction(&format!("b.eq {}", found_label)); // stop once the associative-array entry payload matches the searched scalar needle - } - } - emitter.instruction(&format!("b {}", loop_label)); // continue scanning the remaining associative-array insertion-order entries - - emitter.label(&found_label); - emitter.instruction("mov x0, #1"); // return true once the searched needle matches an associative-array entry value - emitter.instruction(&format!("b {}", skip_label)); // jump to the common associative-array cleanup after a match - - emitter.label(&end_label); - emitter.instruction("mov x0, #0"); // return false once associative-array iteration finishes without a match - - emitter.label(&skip_label); - emitter.instruction("add sp, sp, #48"); // drop the associative-array iterator cursor, needle, and hash-table stack slots - } - Arch::X86_64 => { - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // load the associative-array hash-table pointer for the next insertion-order iteration step - emitter.instruction("mov rsi, QWORD PTR [rsp]"); // load the current associative-array iterator cursor - emitter.instruction("call __rt_hash_iter_next"); // advance one associative-array insertion-order entry and return its key plus payload - emitter.instruction("cmp rax, -1"); // has associative-array iteration reached the done sentinel? - emitter.instruction(&format!("je {}", end_label)); // stop searching once the associative-array iterator is exhausted - emitter.instruction("mov QWORD PTR [rsp], rax"); // save the updated associative-array iterator cursor for the next loop step - - match &val_ty { - PhpType::Str => { - emitter.instruction("mov rdi, rcx"); // move the associative-array entry string pointer into the first string-compare register - emitter.instruction("mov rsi, r8"); // move the associative-array entry string length into the paired string-compare register - emitter.instruction("mov rdx, QWORD PTR [rsp + 16]"); // reload the saved string needle pointer from the associative-array search stack frame - emitter.instruction("mov rcx, QWORD PTR [rsp + 24]"); // reload the saved string needle length from the associative-array search stack frame - emitter.instruction("call __rt_str_eq"); // compare the associative-array entry string value against the searched needle - emitter.instruction("test rax, rax"); // did the associative-array string value match the searched needle? - emitter.instruction(&format!("jne {}", found_label)); // stop once the associative-array string value matches the searched needle - } - PhpType::Mixed => { - let expected_tag = crate::codegen::runtime_value_tag(&needle_ty) as i64; - abi::emit_load_int_immediate(emitter, "r10", expected_tag); // materialize the expected mixed-entry runtime tag for the searched needle - emitter.instruction("cmp r9, r10"); // does the current associative-array mixed entry match the searched needle kind? - emitter.instruction(&format!("jne {}", mixed_mismatch_label)); // skip associative-array entries whose mixed kind differs from the needle - match &needle_ty { - PhpType::Str => { - emitter.instruction("mov rdi, rcx"); // move the associative-array mixed entry string pointer into the first string-compare register - emitter.instruction("mov rsi, r8"); // move the associative-array mixed entry string length into the paired string-compare register - emitter.instruction("mov rdx, QWORD PTR [rsp + 16]"); // reload the saved string needle pointer from the associative-array search stack frame - emitter.instruction("mov rcx, QWORD PTR [rsp + 24]"); // reload the saved string needle length from the associative-array search stack frame - emitter.instruction("call __rt_str_eq"); // compare the associative-array mixed string entry against the searched needle - emitter.instruction("test rax, rax"); // did the associative-array mixed string value match the searched needle? - emitter.instruction(&format!("jne {}", found_label)); // stop once the associative-array mixed string value matches the needle - } - PhpType::Void => { - emitter.instruction(&format!("jmp {}", found_label)); // null needles match associative-array entries tagged null - } - _ => { - emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // reload the saved scalar mixed needle payload from the associative-array search stack frame - emitter.instruction("cmp rcx, r10"); // compare the associative-array mixed entry payload against the searched scalar needle - emitter.instruction(&format!("je {}", found_label)); // stop once the associative-array mixed scalar payload matches the needle - } - } - emitter.label(&mixed_mismatch_label); - } - _ => { - emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // reload the saved scalar needle payload from the associative-array search stack frame - emitter.instruction("cmp rcx, r10"); // compare the associative-array entry payload against the searched scalar needle - emitter.instruction(&format!("je {}", found_label)); // stop once the associative-array entry payload matches the searched scalar needle - } - } - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning the remaining associative-array insertion-order entries - - emitter.label(&found_label); - emitter.instruction("mov rax, 1"); // return true once the searched needle matches an associative-array entry value - emitter.instruction(&format!("jmp {}", skip_label)); // jump to the common associative-array cleanup after a match - - emitter.label(&end_label); - emitter.instruction("xor eax, eax"); // return false once associative-array iteration finishes without a match - - emitter.label(&skip_label); - emitter.instruction("add rsp, 48"); // drop the associative-array iterator cursor, needle, and hash-table stack slots - } - } - } else { - // -- indexed array: linear scan -- - let elem_ty = match &arr_ty { - PhpType::Array(t) => *t.clone(), - _ => PhpType::Int, - }; - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the indexed-array pointer while evaluating the searched needle - let _needle_ty = emit_expr(&args[0], emitter, ctx, data); - - let found_label = ctx.next_label("in_array_found"); - let end_label = ctx.next_label("in_array_end"); - let done_label = ctx.next_label("in_array_done"); - let loop_label = ctx.next_label("in_array_loop"); - - match &elem_ty { - PhpType::Str => { - match emitter.target.arch { - Arch::AArch64 => { - // -- save needle string (x1=ptr, x2=len) and set up loop -- - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push needle ptr+len - emitter.instruction("ldr x0, [sp, #16]"); // reload array pointer - emitter.instruction("ldr x9, [x0]"); // load array length - emitter.instruction("add x10, x0, #24"); // x10 = pointer to data region - emitter.instruction("mov x12, #0"); // initialize loop counter - - // Stack layout: - // sp+0: needle ptr+len (16 bytes) - // sp+16: array pointer (16 bytes) - - emitter.label(&loop_label); - // -- check if all elements have been scanned -- - emitter.instruction("cmp x12, x9"); // check if counter reached array length - emitter.instruction(&format!("b.ge {}", end_label)); // exit loop if all elements checked - - // -- load string element at index x12 (16 bytes per element) -- - emitter.instruction("lsl x13, x12, #4"); // x13 = index * 16 - emitter.instruction("ldr x1, [x10, x13]"); // x1 = element string pointer - emitter.instruction("add x14, x13, #8"); // x14 = offset to length field - emitter.instruction("ldr x2, [x10, x14]"); // x2 = element string length - - // -- save loop state before calling __rt_str_eq -- - emitter.instruction("stp x9, x10, [sp, #-16]!"); // push array len + data ptr - emitter.instruction("str x12, [sp, #-16]!"); // push loop counter - - // -- load needle and compare -- - emitter.instruction("ldp x3, x4, [sp, #32]"); // reload needle ptr+len from stack - emitter.instruction("bl __rt_str_eq"); // x0 = 1 if strings are equal - - // -- restore loop state -- - emitter.instruction("ldr x12, [sp], #16"); // pop loop counter - emitter.instruction("ldp x9, x10, [sp], #16"); // pop array len + data ptr - - emitter.instruction(&format!("cbnz x0, {}", found_label)); // if equal, found - emitter.instruction("add x12, x12, #1"); // increment loop counter - emitter.instruction(&format!("b {}", loop_label)); // continue searching - - // -- needle found -- - emitter.label(&found_label); - emitter.instruction("mov x0, #1"); // return true - emitter.instruction(&format!("b {}", done_label)); // jump to cleanup - - // -- needle not found -- - emitter.label(&end_label); - emitter.instruction("mov x0, #0"); // return false - - emitter.label(&done_label); - emitter.instruction("add sp, sp, #32"); // drop needle + array ptr - } - Arch::X86_64 => { - // -- save needle string (rax=ptr, rdx=len) and set up loop -- - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the searched string across the indexed-array scan - emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // reload the indexed-array pointer from the temporary stack frame - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the indexed-array length from the fixed header - emitter.instruction("lea r12, [r10 + 24]"); // compute the base address of the indexed-array string payload region - emitter.instruction("xor r13d, r13d"); // initialize the indexed-array loop counter to zero - - // Stack layout: - // rsp+0: needle ptr+len (16 bytes) - // rsp+16: array pointer (16 bytes) - - emitter.label(&loop_label); - emitter.instruction("cmp r13, r11"); // have we scanned every indexed-array string element? - emitter.instruction(&format!("jge {}", end_label)); // stop once the loop counter reaches the indexed-array length - - emitter.instruction("mov rcx, r13"); // copy the loop index before scaling it to one 16-byte string slot - emitter.instruction("shl rcx, 4"); // convert the indexed-array element index into a byte offset inside the string payload region - emitter.instruction("mov rdi, QWORD PTR [r12 + rcx]"); // load the current indexed-array string pointer into the first str_eq argument register - emitter.instruction("mov rsi, QWORD PTR [r12 + rcx + 8]"); // load the current indexed-array string length into the paired str_eq argument register - - abi::emit_push_reg_pair(emitter, "r11", "r12"); // preserve the indexed-array length and payload base across the string-compare helper call - abi::emit_push_reg(emitter, "r13"); // preserve the indexed-array loop counter across the string-compare helper call - - emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // reload the searched string pointer from the temporary stack frame under the saved loop state - emitter.instruction("mov rcx, QWORD PTR [rsp + 40]"); // reload the searched string length from the temporary stack frame under the saved loop state - emitter.instruction("call __rt_str_eq"); // compare the current indexed-array string element against the searched needle - - abi::emit_pop_reg(emitter, "r13"); // restore the indexed-array loop counter after the helper call - abi::emit_pop_reg_pair(emitter, "r11", "r12"); // restore the indexed-array length and payload base after the helper call - - emitter.instruction("test rax, rax"); // did the current indexed-array string element match the searched needle? - emitter.instruction(&format!("jne {}", found_label)); // return true as soon as one indexed-array string element matches - emitter.instruction("add r13, 1"); // advance to the next indexed-array string element after a mismatch - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning the indexed-array string payloads - - emitter.label(&found_label); - emitter.instruction("mov rax, 1"); // return true once the searched string matches an indexed-array element - emitter.instruction(&format!("jmp {}", done_label)); // skip the not-found write and jump to the common indexed-array cleanup - - emitter.label(&end_label); - emitter.instruction("xor eax, eax"); // return false once the indexed-array scan finishes without a string match - - emitter.label(&done_label); - emitter.instruction("add rsp, 32"); // drop the saved string needle and indexed-array pointer from the temporary stack frame - } - } - } - _ => { - match emitter.target.arch { - Arch::AArch64 => { - // -- integer/bool needle: simple comparison loop -- - emitter.instruction("mov x11, x0"); // save needle value in x11 - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer - emitter.instruction("ldr x9, [x0]"); // load array length into x9 - emitter.instruction("add x10, x0, #24"); // x10 = pointer to data (past 24-byte header) - emitter.instruction("mov x12, #0"); // initialize loop counter to 0 - - emitter.label(&loop_label); - emitter.instruction("cmp x12, x9"); // check if counter reached array length - emitter.instruction(&format!("b.ge {}", end_label)); // exit loop if all elements checked - emitter.instruction("ldr x13, [x10, x12, lsl #3]"); // load element at index x12 (offset = x12 * 8) - emitter.instruction("cmp x13, x11"); // compare element with needle - emitter.instruction(&format!("b.eq {}", found_label)); // branch to found if element matches - emitter.instruction("add x12, x12, #1"); // increment loop counter - emitter.instruction(&format!("b {}", loop_label)); // jump back to loop start - - emitter.label(&found_label); - emitter.instruction("mov x0, #1"); // set return value to 1 (true) - emitter.instruction(&format!("b {}", done_label)); // jump to done - - emitter.label(&end_label); - emitter.instruction("mov x0, #0"); // set return value to 0 (false) - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the searched scalar needle into the second SysV runtime-helper argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the indexed-array pointer into the first SysV runtime-helper argument register - abi::emit_call_label(emitter, "__rt_array_search"); // search the indexed-array payloads for the first matching scalar element - emitter.instruction("cmp rax, -1"); // did the indexed-array search helper fail to find any matching scalar element? - emitter.instruction("setne al"); // convert the found-vs-missing condition into a one-byte boolean result - emitter.instruction("movzx rax, al"); // zero-extend the boolean result into the standard integer result register - } - } - } - } - } - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/arrays/isset.rs b/src/codegen/builtins/arrays/isset.rs deleted file mode 100644 index 22075d851d..0000000000 --- a/src/codegen/builtins/arrays/isset.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Purpose: -//! Emits PHP `isset` checks without evaluating to ordinary truthiness. -//! Owns null/unset sentinel handling for variables and array element probes. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Must distinguish PHP null/unset semantics from false, zero, and empty string values. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::codegen::NULL_SENTINEL; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// The null sentinel value used to represent PHP `null` in scalar runtime representations. -/// -/// This value is distinct from all valid PHP scalar values (integers, booleans, floats) -/// and is used by the runtime to distinguish a loaded null from false, zero, or empty. - -/// Emits PHP `isset(...)` for one or more arguments. -/// -/// Returns `PhpType::Int` to indicate the result is always treated as integer (0 or 1). -/// When multiple arguments are given, all must be set for the result to be true. -/// -/// # Arguments -/// * `_name` - Unused; included for parity with the builtin call signature dispatcher. -/// * `args` - The PHP expressions to check for set-ness. -/// * `emitter` - The assembly emitter. -/// * `ctx` - Codegen context (labels, scope). -/// * `data` - Data section for constants and runtime symbols. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("isset()"); - if args.is_empty() { - emit_bool_result(false, emitter); - return Some(PhpType::Int); - } - - let false_label = ctx.next_label("isset_false"); - let done_label = ctx.next_label("isset_done"); - for (idx, arg) in args.iter().enumerate() { - emit_isset_arg(arg, emitter, ctx, data); - if idx + 1 < args.len() { - abi::emit_branch_if_int_result_zero(emitter, &false_label); - } - } - - if args.len() > 1 { - abi::emit_jump(emitter, &done_label); - emitter.label(&false_label); - emit_bool_result(false, emitter); - emitter.label(&done_label); - } - - Some(PhpType::Int) -} - -/// Emits `isset` checks for a single argument expression. -/// -/// Dispatches to the appropriate specialized emitter based on the expression kind: -/// - `ArrayAccess` on array/object types → object offset or array element check -/// - `ArrayAccess` on strings → string offset bounds check -/// - Other expressions → null-sentinel or type-based check on the loaded value -fn emit_isset_arg( - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if let ExprKind::ArrayAccess { array, index } = &arg.kind { - let array_ty = crate::codegen::functions::infer_contextual_type(array, ctx); - if crate::codegen::expr::arrays::type_is_array_access_object(&array_ty, ctx) { - crate::codegen::expr::arrays::emit_array_access_offset_exists( - array, index, emitter, ctx, data, - ); - return; - } - - match &array_ty { - PhpType::Str => { - emit_expr(arg, emitter, ctx, data); - emit_string_offset_isset_result(emitter); - return; - } - PhpType::Array(elem_ty) => { - emit_indexed_array_isset(array, index, elem_ty, emitter, ctx, data); - return; - } - PhpType::AssocArray { value, .. } => { - emit_assoc_array_isset(array, index, value, emitter, ctx, data); - return; - } - PhpType::Mixed => { - emit_expr(arg, emitter, ctx, data); - emit_mixed_result_not_null(emitter); - return; - } - _ => {} - } - } - - let ty = emit_expr(arg, emitter, ctx, data); - emit_loaded_result_isset(&ty, emitter); -} - -/// Emits an `isset` check on a value whose type is already known. -/// -/// Uses the type's codegen representation to determine null-ness: -/// - `Void`/`Never` → false (these types cannot hold values) -/// - `Mixed` → runtime unbox and null tag check -/// - `Int`/`Bool` → compare against the null sentinel -/// - All other types → true (e.g., arrays, objects, resources always exist) -fn emit_loaded_result_isset(ty: &PhpType, emitter: &mut Emitter) { - match ty.codegen_repr() { - PhpType::Void | PhpType::Never => emit_bool_result(false, emitter), - PhpType::Mixed => emit_mixed_result_not_null(emitter), - PhpType::TaggedScalar => emit_tagged_scalar_result_not_null(emitter), - PhpType::Int | PhpType::Bool if crate::codegen::sentinels::null_repr_is_tagged() => { - emit_bool_result(true, emitter) - } - PhpType::Int | PhpType::Bool => emit_scalar_result_not_null(emitter), - _ => emit_bool_result(true, emitter), - } -} - -/// Emits an `isset` check for an indexed array element access. -/// -/// Loads the array pointer and index, validates the index is non-negative and within -/// bounds, then checks the element type to determine null-ness. Uses the null sentinel -/// for `Mixed` elements and unconditionally returns true for other non-void types. -fn emit_indexed_array_isset( - array: &Expr, - index: &Expr, - elem_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_expr(array, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the indexed array pointer while evaluating the index expression - emit_expr(index, emitter, ctx, data); - let array_reg = abi::symbol_scratch_reg(emitter); - let len_reg = abi::secondary_scratch_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let false_label = ctx.next_label("isset_array_false"); - let done_label = ctx.next_label("isset_array_done"); - abi::emit_pop_reg(emitter, array_reg); // restore the indexed array pointer for the bounds probe - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #0", result_reg)); // reject negative indexes as missing array elements - emitter.instruction(&format!("b.lt {}", false_label)); // return false when the requested index is negative - abi::emit_load_from_address(emitter, len_reg, array_reg, 0); - emitter.instruction(&format!("cmp {}, {}", result_reg, len_reg)); // compare the requested index against the array length - emitter.instruction(&format!("b.ge {}", false_label)); // return false when the requested index is out of bounds - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, 0", result_reg)); // reject negative indexes as missing array elements - emitter.instruction(&format!("jl {}", false_label)); // return false when the requested index is negative - abi::emit_load_from_address(emitter, len_reg, array_reg, 0); - emitter.instruction(&format!("cmp {}, {}", result_reg, len_reg)); // compare the requested index against the array length - emitter.instruction(&format!("jge {}", false_label)); // return false when the requested index is out of bounds - } - } - - match elem_ty.codegen_repr() { - PhpType::Void | PhpType::Never => emit_bool_result(false, emitter), - PhpType::Mixed => { - load_indexed_array_element_pointer(array_reg, result_reg, emitter); - emit_mixed_result_not_null(emitter); - } - _ => emit_bool_result(true, emitter), - } - abi::emit_jump(emitter, &done_label); - emitter.label(&false_label); - emit_bool_result(false, emitter); - emitter.label(&done_label); -} - -/// Computes the element pointer for an indexed array element on AArch64 or x86_64. -/// -/// Adds the indexed array header size (24 bytes) to the array pointer to skip the -/// length field and type tag, then loads the boxed `Mixed` element pointer at -/// `element_base + index * 8`. -/// -/// # Arguments -/// * `array_reg` - Register holding the indexed array pointer (modified in place). -/// * `index_reg` - Register holding the element index. -/// * `emitter` - The assembly emitter. -fn load_indexed_array_element_pointer(array_reg: &str, index_reg: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip the indexed array header to reach element storage - emitter.instruction(&format!("ldr x0, [{}, {}, lsl #3]", array_reg, index_reg)); // load the boxed Mixed element pointer for null inspection - } - Arch::X86_64 => { - emitter.instruction(&format!("lea {}, [{} + 24]", array_reg, array_reg)); // skip the indexed array header to reach element storage - emitter.instruction(&format!("mov rax, QWORD PTR [{} + {} * 8]", array_reg, index_reg)); // load the boxed Mixed element pointer for null inspection - } - } -} - -/// Emits an `isset` check for an associative array element access. -/// -/// Normalizes the index expression to a string key, calls `__rt_hash_get` to probe -/// the hash table, then checks whether the lookup succeeded and the value is not null. -fn emit_assoc_array_isset( - array: &Expr, - index: &Expr, - _value_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_expr(array, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the hash-table pointer while evaluating the offset expression - crate::codegen::emit_normalized_hash_key(index, emitter, ctx, data); - let (key_ptr_reg, key_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, key_ptr_reg, key_len_reg); // preserve the normalized key while restoring the hash-table pointer - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the normalized key into hash-get argument registers - abi::emit_pop_reg(emitter, "x0"); // restore the hash-table pointer into the hash-get receiver argument - } - Arch::X86_64 => { - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore the normalized key into hash-get argument registers - abi::emit_pop_reg(emitter, "rdi"); // restore the hash-table pointer into the hash-get receiver argument - } - } - abi::emit_call_label(emitter, "__rt_hash_get"); // return the hash lookup found flag plus borrowed payload metadata - emit_hash_found_and_not_null(emitter, ctx); -} - -/// Emits post-hash-lookup null check after `__rt_hash_get` returns. -/// -/// Consumes the runtime return values from `__rt_hash_get`: -/// - x86_64: `rax` = found flag, `rcx` = value tag -/// - AArch64: `x0` = found flag, `x3` = value tag -/// -/// Emits true when the key was found AND the value tag is not 8 (PHP null). -fn emit_hash_found_and_not_null(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("isset_hash_false"); - let done_label = ctx.next_label("isset_hash_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x0, {}", false_label)); // return false when the associative lookup misses - emitter.instruction("cmp x3, #8"); // runtime tag 8 means the stored value is PHP null - emitter.instruction(&format!("b.eq {}", false_label)); // return false when the stored value is null - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // check whether the associative lookup found a matching key - emitter.instruction(&format!("je {}", false_label)); // return false when the associative lookup misses - emitter.instruction("cmp rcx, 8"); // runtime tag 8 means the stored value is PHP null - emitter.instruction(&format!("je {}", false_label)); // return false when the stored value is null - } - } - emit_bool_result(true, emitter); - abi::emit_jump(emitter, &done_label); - emitter.label(&false_label); - emit_bool_result(false, emitter); - emitter.label(&done_label); -} - -/// Emits the result of an `isset` check on a string offset expression. -/// -/// After evaluating a string `ArrayAccess` expression (e.g., `$s[0]`), the string -/// result registers contain the character (or null byte) and the length. This -/// function returns true only when the length is non-zero, indicating a valid -/// in-bounds offset was accessed. -fn emit_string_offset_isset_result(emitter: &mut Emitter) { - let (_, len_reg) = abi::string_result_regs(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #0", len_reg)); // check whether string offset access produced a character - emitter.instruction("cset x0, ne"); // return true only when the string offset is in bounds - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, 0", len_reg)); // check whether string offset access produced a character - emitter.instruction("setne al"); // return true only when the string offset is in bounds - emitter.instruction("movzx eax, al"); // widen the boolean byte into the canonical integer result - } - } -} - -/// Emits the result of an `isset` check on a `Mixed` runtime value. -/// -/// Calls `__rt_mixed_unbox` to inspect the boxed `Mixed` payload tag. Returns true -/// only when the tag is not 8 (PHP null). -fn emit_mixed_result_not_null(emitter: &mut Emitter) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect the boxed Mixed payload tag for PHP null - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #8"); // runtime tag 8 means the Mixed payload is PHP null - emitter.instruction("cset x0, ne"); // return true only when the Mixed payload is not null - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 8"); // runtime tag 8 means the Mixed payload is PHP null - emitter.instruction("setne al"); // set the low result byte when the Mixed payload is not null - emitter.instruction("movzx rax, al"); // widen the Mixed null-check result into the integer result register - } - } -} - -/// Emits the result of an `isset` check on a tagged scalar runtime value: true unless -/// the runtime tag word marks the value as PHP null. -fn emit_tagged_scalar_result_not_null(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x1, #8"); // runtime tag 8 means the tagged scalar is PHP null - emitter.instruction("cset x0, ne"); // return true only when the tagged scalar is not null - } - Arch::X86_64 => { - emitter.instruction("cmp rdx, 8"); // runtime tag 8 means the tagged scalar is PHP null - emitter.instruction("setne al"); // set the low result byte when the tagged scalar is not null - emitter.instruction("movzx rax, al"); // widen the tagged null-check result into the integer result register - } - } -} - -/// Emits the result of an `isset` check on a scalar (Int or Bool) runtime value. -/// -/// Compares the scalar result register against the null sentinel and returns true -/// only when they differ, indicating the value is not PHP null. On AArch64 uses -/// `x9` as scratch; on x86_64 uses `r10`. -fn emit_scalar_result_not_null(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_int_immediate(emitter, "x9", NULL_SENTINEL); - emitter.instruction("cmp x0, x9"); // compare the scalar result against the shared null sentinel - emitter.instruction("cset x0, ne"); // return true only when the scalar result is not null - } - Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "r10", NULL_SENTINEL); - emitter.instruction("cmp rax, r10"); // compare the scalar result against the shared null sentinel - emitter.instruction("setne al"); // set the low result byte when the scalar result is not null - emitter.instruction("movzx rax, al"); // widen the scalar null-check result into the integer result register - } - } -} - -/// Emits a constant boolean result for `isset`. -/// -/// Materializes `value` as an integer (1 for true, 0 for false) into the canonical -/// integer result register (`x0` on AArch64, `rax` on x86_64). -fn emit_bool_result(value: bool, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(if value { "mov x0, #1" } else { "mov x0, #0" }); // materialize the isset boolean result on AArch64 - } - Arch::X86_64 => { - emitter.instruction(if value { "mov rax, 1" } else { "xor eax, eax" }); // materialize the isset boolean result on x86_64 - } - } -} diff --git a/src/codegen/builtins/arrays/krsort.rs b/src/codegen/builtins/arrays/krsort.rs deleted file mode 100644 index 4ada0fed7e..0000000000 --- a/src/codegen/builtins/arrays/krsort.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Purpose: -//! Emits PHP `krsort` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `krsort` runtime helper, which sorts an associative array -/// by its keys in descending order, mutating the array in place. -/// -/// # Arguments -/// - `_name`: Unused (builtin dispatch is by arity/signature). -/// - `args`: Must contain exactly one argument — the array to sort. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context (carries variable layout, ownership state). -/// - `data`: Data section for relocations and constant data. -/// -/// # Returns -/// Always returns `Some(PhpType::Void)` because `krsort` has no meaningful return value -/// in PHP — it operates purely by side effect (in-place mutation). -/// -/// # Safety & Ownership -/// The single argument is emitted as a reference-like operand so the runtime helper -/// writes back to the caller's storage. No value-temp preevaluation occurs, preserving -/// PHP's semantics where the original variable is modified directly. -/// -/// # PHP Semantics -/// `krsort($arr)` sorts `$arr` by keys in descending order. Flags (e.g., `SORT_REGULAR`) -/// are not yet supported; the runtime helper uses default comparison. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("krsort()"); - emit_expr(&args[0], emitter, ctx, data); - // -- sort associative array by keys descending -- - abi::emit_call_label(emitter, "__rt_krsort"); // call the target-aware runtime helper that sorts associative-array keys descending in place - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/ksort.rs b/src/codegen/builtins/arrays/ksort.rs deleted file mode 100644 index 4599b6df35..0000000000 --- a/src/codegen/builtins/arrays/ksort.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Purpose: -//! Emits PHP `ksort` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `ksort` runtime helper, sorting an associative array by keys in-place. -/// -/// # Arguments -/// - `_name`: Unused name parameter (present for dispatcher uniformity). -/// - `args`: Must contain at least the array expression to sort. Additional arguments (e.g., `SORT_REGULAR`) are currently ignored. -/// - `emitter`: Target-aware instruction emitter. -/// - `ctx`: Codegen context carrying variable layout and ownership state. -/// - `data`: Data section for relocations and static data. -/// -/// # Returns -/// `Some(PhpType::Void)` — `ksort` always returns void in PHP; the array is mutated in-place. -/// -/// # PHP Behavior -/// PHP's `ksort()` sorts an array by keys in ascending order, maintaining key-value correlations. -/// The return value is always `true` (1) in PHP, but since the return is typically ignored, -/// this emitter discards the return and always emits `PhpType::Void`. -/// -/// # Side Effects -/// The runtime `__rt_ksort` helper mutates the array in-place. COW is handled by the caller -/// (via `emit_expr` on the array argument) before this function is invoked. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ksort()"); - emit_expr(&args[0], emitter, ctx, data); - // -- sort associative array by keys ascending -- - abi::emit_call_label(emitter, "__rt_ksort"); // call the target-aware runtime helper that sorts associative-array keys ascending in place - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/mod.rs b/src/codegen/builtins/arrays/mod.rs deleted file mode 100644 index 7335daf1d2..0000000000 --- a/src/codegen/builtins/arrays/mod.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Purpose: -//! Dispatches array, callable, and buffer-related PHP builtins to their focused codegen emitters. -//! Keeps the public builtin category surface small while leaf files own lowering details. -//! -//! Called from: -//! - `crate::codegen::builtins::emit_builtin_call()`. -//! -//! Key details: -//! - Dispatcher names must stay aligned with the builtin catalog and signature normalization layer. - -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; -pub(crate) mod array_keys; -mod array_map; -mod array_map_callback_returns_str; -mod array_map_expr_is_str; -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; -pub(crate) mod array_values; -mod array_walk; -mod arsort; -mod asort; -mod buffer_free; -mod buffer_len; -mod call_user_func; -pub(crate) mod call_user_func_array; -mod callable_forms; -pub(crate) mod callback_env; -mod count; -pub(crate) mod descriptor_arg_builder; -mod ensure_unique_arg; -mod function_exists; -mod in_array; -mod isset; -mod krsort; -mod ksort; -mod natcasesort; -mod natsort; -mod range; -pub(crate) mod receiver_call_args; -pub(crate) mod runtime_callable_array_callback; -pub(crate) mod runtime_string_callback; -mod hash_value_type_tag; -mod rsort; -mod shuffle_fn; -mod sort; -mod store_mutating_arg; -mod uasort; -mod uksort; -mod usort; - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Dispatches array, buffer, and callable-related PHP builtins to their leaf codegen emitters. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "count" => count::emit(name, args, emitter, ctx, data), - "array_push" => array_push::emit(name, args, emitter, ctx, data), - "array_pop" => array_pop::emit(name, args, emitter, ctx, data), - "in_array" => in_array::emit(name, args, emitter, ctx, data), - "array_keys" => array_keys::emit(name, args, emitter, ctx, data), - "array_values" => array_values::emit(name, args, emitter, ctx, data), - "sort" => sort::emit(name, args, emitter, ctx, data), - "rsort" => rsort::emit(name, args, emitter, ctx, data), - "isset" => isset::emit(name, args, emitter, ctx, data), - "array_key_exists" => array_key_exists::emit(name, args, emitter, ctx, data), - "array_search" => array_search::emit(name, args, emitter, ctx, data), - "array_reverse" => array_reverse::emit(name, args, emitter, ctx, data), - "array_unique" => array_unique::emit(name, args, emitter, ctx, data), - "array_sum" => array_sum::emit(name, args, emitter, ctx, data), - "array_product" => array_product::emit(name, args, emitter, ctx, data), - "array_shift" => array_shift::emit(name, args, emitter, ctx, data), - "array_unshift" => array_unshift::emit(name, args, emitter, ctx, data), - "array_merge" => array_merge::emit(name, args, emitter, ctx, data), - "array_slice" => array_slice::emit(name, args, emitter, ctx, data), - "array_splice" => array_splice::emit(name, args, emitter, ctx, data), - "array_combine" => array_combine::emit(name, args, emitter, ctx, data), - "array_flip" => array_flip::emit(name, args, emitter, ctx, data), - "array_chunk" => array_chunk::emit(name, args, emitter, ctx, data), - "array_column" => array_column::emit(name, args, emitter, ctx, data), - "array_pad" => array_pad::emit(name, args, emitter, ctx, data), - "array_fill" => array_fill::emit(name, args, emitter, ctx, data), - "array_fill_keys" => array_fill_keys::emit(name, args, emitter, ctx, data), - "array_diff" => array_diff::emit(name, args, emitter, ctx, data), - "array_intersect" => array_intersect::emit(name, args, emitter, ctx, data), - "array_diff_key" => array_diff_key::emit(name, args, emitter, ctx, data), - "array_intersect_key" => array_intersect_key::emit(name, args, emitter, ctx, data), - "array_rand" => array_rand::emit(name, args, emitter, ctx, data), - "shuffle" => shuffle_fn::emit(name, args, emitter, ctx, data), - "range" => range::emit(name, args, emitter, ctx, data), - "asort" => asort::emit(name, args, emitter, ctx, data), - "arsort" => arsort::emit(name, args, emitter, ctx, data), - "ksort" => ksort::emit(name, args, emitter, ctx, data), - "krsort" => krsort::emit(name, args, emitter, ctx, data), - "natsort" => natsort::emit(name, args, emitter, ctx, data), - "natcasesort" => natcasesort::emit(name, args, emitter, ctx, data), - "array_map" => array_map::emit(name, args, emitter, ctx, data), - "array_filter" => array_filter::emit(name, args, emitter, ctx, data), - "array_reduce" => array_reduce::emit(name, args, emitter, ctx, data), - "array_walk" => array_walk::emit(name, args, emitter, ctx, data), - "buffer_free" => buffer_free::emit(name, args, emitter, ctx, data), - "buffer_len" => buffer_len::emit(name, args, emitter, ctx, data), - "usort" => usort::emit(name, args, emitter, ctx, data), - "uksort" => uksort::emit(name, args, emitter, ctx, data), - "uasort" => uasort::emit(name, args, emitter, ctx, data), - "call_user_func" => call_user_func::emit(name, args, emitter, ctx, data), - "call_user_func_array" => call_user_func_array::emit(name, args, emitter, ctx, data), - "function_exists" => function_exists::emit(name, args, emitter, ctx, data), - _ => None, - } -} diff --git a/src/codegen/builtins/arrays/natcasesort.rs b/src/codegen/builtins/arrays/natcasesort.rs deleted file mode 100644 index a072a4de53..0000000000 --- a/src/codegen/builtins/arrays/natcasesort.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Purpose: -//! Emits PHP `natcasesort` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `natcasesort` runtime helper, which sorts the input array -/// in place using case-insensitive natural order. The array argument is evaluated -/// once, made unique (COW), and then a pointer to the potentially-reallocated array -/// is written back to caller storage before the sort routine is invoked. -/// -/// Arguments: -/// - `args[0]`: the array to sort (must be an indexed integer-array for the runtime helper) -/// - `emitter`: writes the call sequence -/// - `ctx`: provides variable layout and mutating-arg storage info -/// - `data`: data section for literals and runtime metadata -/// -/// Returns: `Some(PhpType::Void)` — the function has no PHP-visible return value, -/// but the call sequence produces side effects on the array argument. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("natcasesort()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - // -- sort array using case-insensitive natural order algorithm -- - abi::emit_call_label(emitter, "__rt_natcasesort"); // call the target-aware runtime helper that sorts indexed integer arrays by case-insensitive natural order - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/natsort.rs b/src/codegen/builtins/arrays/natsort.rs deleted file mode 100644 index 42f4d81b95..0000000000 --- a/src/codegen/builtins/arrays/natsort.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Purpose: -//! Emits PHP `natsort` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the `natsort()` builtin, which sorts an array in place using natural order. -/// Takes a single array argument, ensures it is COW-safe, stores the array pointer for mutation, -/// then calls `__rt_natsort` to perform the actual sort. Returns `PhpType::Void`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("natsort()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - // -- sort array using natural order algorithm -- - abi::emit_call_label(emitter, "__rt_natsort"); // call the target-aware runtime helper that sorts indexed integer arrays by natural ascending order - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/range.rs b/src/codegen/builtins/arrays/range.rs deleted file mode 100644 index eb20b4af76..0000000000 --- a/src/codegen/builtins/arrays/range.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Purpose: -//! Emits PHP `range` builtin calls that allocate or reshape array values. -//! Coordinates element type selection with runtime helpers that build indexed or associative arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Returned arrays must use the payload layout expected by later codegen and GC/refcount paths. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `range($start, $end)` builtin call. -/// -/// Evaluates the `$start` and `$end` expressions, calls the `__rt_range` runtime helper, -/// and returns an `array` of integers from `$start` to `$end` (inclusive). -/// -/// # Architecture-specific ABI -/// - **x86_64**: Evaluates `$start` into `rax`, pushes it onto the stack, evaluates -/// `$end` into `rax`, then arranges arguments into `rdi` (start) and `rsi` (end) -/// before calling `__rt_range`. -/// - **ARM64**: Pushes `$start` onto the stack, evaluates `$end` into `x0`, pops the -/// saved start into `x0`, and moves end to `x1` (AAPCS64 register ordering) before -/// calling `__rt_range`. -/// -/// # Return type -/// Always returns `array` of `int` (`PhpType::Array(Box::new(PhpType::Int))`). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("range()"); - if emitter.target.arch == Arch::X86_64 { - let start_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &start_ty); // unbox a Mixed/Union range start into a raw integer - abi::emit_push_reg(emitter, "rax"); // preserve the range start value while evaluating the range end value expression - let end_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &end_ty); // unbox a Mixed/Union range end into a raw integer - emitter.instruction("mov rsi, rax"); // place the inclusive range end value in the second x86_64 runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the inclusive range start value into the first x86_64 runtime argument register - abi::emit_call_label(emitter, "__rt_range"); // build the integer range array through the x86_64 runtime helper - return Some(PhpType::Array(Box::new(PhpType::Int))); - } - - let start_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &start_ty); // unbox a Mixed/Union range start into a raw integer - // -- save start value, evaluate end value -- - emitter.instruction("str x0, [sp, #-16]!"); // push start value onto stack - let end_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &end_ty); // unbox a Mixed/Union range end into a raw integer - // -- call runtime to create array from start to end -- - emitter.instruction("mov x1, x0"); // move end value to x1 (second arg) - emitter.instruction("ldr x0, [sp], #16"); // pop start value into x0 (first arg) - emitter.instruction("bl __rt_range"); // call runtime: create range → x0=new array - - Some(PhpType::Array(Box::new(PhpType::Int))) -} diff --git a/src/codegen/builtins/arrays/receiver_call_args.rs b/src/codegen/builtins/arrays/receiver_call_args.rs deleted file mode 100644 index 284eb69678..0000000000 --- a/src/codegen/builtins/arrays/receiver_call_args.rs +++ /dev/null @@ -1,611 +0,0 @@ -//! Purpose: -//! Builds receiver-prefixed argument containers for descriptor-based callable invocation. -//! Converts receiver-bound call_user_func_array() inputs into boxed Mixed containers. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::callable_forms` -//! - `crate::codegen::expr::calls::callable_array_runtime` -//! -//! Key details: -//! - The synthetic receiver occupies descriptor argument slot zero; numeric source keys shift by one. -//! - Source argument arrays are cloned/retained before rewriting so caller-visible containers stay unchanged. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::arrays::emit_array_value_type_stamp; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::call_user_func_array; - -const HASH_SCRATCH_BYTES: usize = 48; -const CURSOR_OFF: usize = 0; -const KEY_PTR_OFF: usize = 8; -const KEY_LEN_OFF: usize = 16; -const VALUE_LO_OFF: usize = 24; -const VALUE_HI_OFF: usize = 32; -const VALUE_TAG_OFF: usize = 40; - -/// Emits a boxed Mixed argument container for dynamic receiver-bound call_user_func_array() args. -/// -/// Returns `true` when `arg_array_ty` is a supported dynamic container shape and -/// leaves the boxed Mixed container in the integer result register. Indexed arrays -/// become `[receiver, ...$args]`; associative hashes become a new hash whose -/// numeric keys are shifted by one and whose string keys are preserved. -pub(crate) fn emit_receiver_prefixed_dynamic_arg_mixed( - receiver: &Expr, - arg_array: &Expr, - arg_array_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - match arg_array_ty { - PhpType::Array(arg_elem_ty) => { - emit_receiver_prefixed_indexed_arg_mixed( - receiver, - arg_array, - arg_elem_ty.as_ref(), - emitter, - ctx, - data, - ); - true - } - PhpType::AssocArray { .. } => { - emit_receiver_prefixed_assoc_arg_mixed(receiver, arg_array, emitter, ctx, data); - true - } - PhpType::Mixed | PhpType::Union(_) => { - emit_receiver_prefixed_opaque_arg_mixed(receiver, arg_array, emitter, ctx, data); - true - } - _ => false, - } -} - -/// Emits a boxed Mixed argument container using a receiver pointer already saved on the temp stack. -pub(crate) fn emit_saved_receiver_prefixed_dynamic_arg_mixed( - object_stack_offset: usize, - arg_array: &Expr, - arg_array_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - match arg_array_ty.codegen_repr() { - PhpType::Array(arg_elem_ty) => { - emit_saved_receiver_prefixed_indexed_arg_mixed( - object_stack_offset, - arg_array, - arg_elem_ty.as_ref(), - emitter, - ctx, - data, - ); - true - } - PhpType::AssocArray { .. } => { - emit_saved_receiver_prefixed_assoc_arg_mixed( - object_stack_offset, - arg_array, - emitter, - ctx, - data, - ); - true - } - PhpType::Mixed | PhpType::Union(_) => { - emit_saved_receiver_prefixed_opaque_arg_mixed( - object_stack_offset, - arg_array, - emitter, - ctx, - data, - ); - true - } - _ => false, - } -} - -/// Builds a boxed Mixed argument container `[receiver, ...$args]` for indexed arrays. -fn emit_receiver_prefixed_indexed_arg_mixed( - receiver: &Expr, - arg_array: &Expr, - inferred_arg_elem_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("receiver-prefixed indexed call_user_func_array descriptor args"); - let receiver_ty = emit_expr(receiver, emitter, ctx, data); - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, - receiver, - &receiver_ty, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed receiver Mixed cell before evaluating the source indexed array - - let arr_ty = emit_expr(arg_array, emitter, ctx, data); - let source_elem_ty = match &arr_ty { - PhpType::Array(elem_ty) => elem_ty.as_ref(), - _ => inferred_arg_elem_ty, - }; - emit_receiver_prefixed_indexed_payload_arg_mixed(source_elem_ty, emitter); -} - -/// Builds `[saved receiver, ...$args]` for indexed arrays. -fn emit_saved_receiver_prefixed_indexed_arg_mixed( - object_stack_offset: usize, - arg_array: &Expr, - inferred_arg_elem_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("saved receiver-prefixed indexed call_user_func_array descriptor args"); - emit_push_saved_object_receiver_mixed(object_stack_offset, emitter); - - let arr_ty = emit_expr(arg_array, emitter, ctx, data); - let source_elem_ty = match &arr_ty { - PhpType::Array(elem_ty) => elem_ty.as_ref(), - _ => inferred_arg_elem_ty, - }; - emit_receiver_prefixed_indexed_payload_arg_mixed(source_elem_ty, emitter); -} - -/// Builds `[receiver, ...$args]` from a loaded raw indexed-array payload. -fn emit_receiver_prefixed_indexed_payload_arg_mixed( - source_elem_ty: &PhpType, - emitter: &mut Emitter, -) { - let result_reg = abi::int_result_reg(emitter); - call_user_func_array::emit_clone_indexed_array_for_invoker( - result_reg, - source_elem_ty, - emitter, - ); - emit_receiver_prefixed_indexed_clone_arg_mixed(emitter); -} - -/// Builds `[receiver, ...$args]` from a loaded runtime-typed indexed-array payload. -fn emit_receiver_prefixed_runtime_indexed_payload_arg_mixed(emitter: &mut Emitter) { - let result_reg = abi::int_result_reg(emitter); - call_user_func_array::emit_clone_indexed_array_for_invoker_with_runtime_tag( - result_reg, - emitter, - ); - emit_receiver_prefixed_indexed_clone_arg_mixed(emitter); -} - -/// Builds the receiver-prefixed destination from a cloned Mixed indexed-array payload. -fn emit_receiver_prefixed_indexed_clone_arg_mixed(emitter: &mut Emitter) { - let result_reg = abi::int_result_reg(emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the cloned Mixed source array before allocating the receiver-prefixed container - - let capacity_reg = abi::int_arg_reg_name(emitter.target, 0); - let elem_size_reg = abi::int_arg_reg_name(emitter.target, 1); - abi::emit_load_int_immediate(emitter, capacity_reg, 16); - abi::emit_load_int_immediate(emitter, elem_size_reg, 8); - abi::emit_call_label(emitter, "__rt_array_new"); - emit_array_value_type_stamp(emitter, result_reg, &PhpType::Mixed); - - let scratch_reg = abi::secondary_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, scratch_reg, 16); - abi::emit_store_to_address(emitter, scratch_reg, result_reg, 24); - abi::emit_load_int_immediate(emitter, scratch_reg, 1); - abi::emit_store_to_address(emitter, scratch_reg, result_reg, 0); - - abi::emit_push_reg(emitter, result_reg); // preserve the destination array while merging the cloned source tail - let dest_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let source_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - abi::emit_load_temporary_stack_slot(emitter, dest_arg_reg, 0); - abi::emit_load_temporary_stack_slot(emitter, source_arg_reg, 16); - abi::emit_call_label(emitter, "__rt_array_merge_into_refcounted"); - - let normalized_array_ty = PhpType::Array(Box::new(PhpType::Mixed)); - abi::emit_push_reg(emitter, result_reg); // preserve the merged receiver-prefixed array while releasing the source clone - abi::emit_load_temporary_stack_slot(emitter, result_reg, 32); - abi::emit_decref_if_refcounted(emitter, &normalized_array_ty); - abi::emit_pop_reg(emitter, result_reg); // restore the merged receiver-prefixed array after source-clone release - abi::emit_release_temporary_stack(emitter, 48); // discard stale destination, source-clone, and receiver stack slots - emit_box_receiver_prefixed_container(result_reg, &normalized_array_ty, emitter); -} - -/// Builds a boxed Mixed hash with the receiver in numeric slot zero. -fn emit_receiver_prefixed_assoc_arg_mixed( - receiver: &Expr, - arg_array: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("receiver-prefixed assoc call_user_func_array descriptor args"); - let receiver_ty = emit_expr(receiver, emitter, ctx, data); - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, - receiver, - &receiver_ty, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed receiver Mixed cell before evaluating the source hash - - let _ = emit_expr(arg_array, emitter, ctx, data); - emit_receiver_prefixed_assoc_payload_arg_mixed(emitter, ctx); -} - -/// Builds a boxed Mixed hash with a saved receiver in numeric slot zero. -fn emit_saved_receiver_prefixed_assoc_arg_mixed( - object_stack_offset: usize, - arg_array: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("saved receiver-prefixed assoc call_user_func_array descriptor args"); - emit_push_saved_object_receiver_mixed(object_stack_offset, emitter); - - let _ = emit_expr(arg_array, emitter, ctx, data); - emit_receiver_prefixed_assoc_payload_arg_mixed(emitter, ctx); -} - -/// Builds a receiver-prefixed hash from a loaded raw associative-array payload. -fn emit_receiver_prefixed_assoc_payload_arg_mixed(emitter: &mut Emitter, ctx: &mut Context) { - let result_reg = abi::int_result_reg(emitter); - call_user_func_array::emit_clone_assoc_array_for_invoker(result_reg, emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the cloned Mixed source hash before allocating the receiver-prefixed hash - - emit_new_receiver_prefixed_hash(0, emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the destination hash while inserting receiver and shifted source entries - emit_insert_receiver_hash_entry(emitter); - emit_copy_shifted_assoc_hash_entries(emitter, ctx); - - let normalized_hash_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; - abi::emit_load_temporary_stack_slot(emitter, result_reg, 16); - abi::emit_decref_if_refcounted(emitter, &normalized_hash_ty); - abi::emit_load_temporary_stack_slot(emitter, result_reg, 0); - abi::emit_release_temporary_stack(emitter, 48); // discard destination, source-clone, and receiver stack slots - emit_box_receiver_prefixed_container(result_reg, &normalized_hash_ty, emitter); -} - -/// Builds a receiver-prefixed argument container from a runtime Mixed array/hash. -fn emit_receiver_prefixed_opaque_arg_mixed( - receiver: &Expr, - arg_array: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("receiver-prefixed mixed call_user_func_array descriptor args"); - let receiver_ty = emit_expr(receiver, emitter, ctx, data); - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, - receiver, - &receiver_ty, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed receiver Mixed cell before evaluating the opaque source container - - emit_receiver_prefixed_opaque_arg_mixed_after_receiver_push(arg_array, emitter, ctx, data); -} - -/// Builds a receiver-prefixed argument container after the boxed receiver was pushed. -fn emit_receiver_prefixed_opaque_arg_mixed_after_receiver_push( - arg_array: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let _ = emit_expr(arg_array, emitter, ctx, data); - let mixed_reg = abi::int_result_reg(emitter); - let tag_reg = abi::secondary_scratch_reg(emitter); - let payload_reg = abi::tertiary_scratch_reg(emitter); - let indexed_label = ctx.next_label("receiver_mixed_indexed_args"); - let assoc_label = ctx.next_label("receiver_mixed_assoc_args"); - let done_label = ctx.next_label("receiver_mixed_args_done"); - let indexed_ty = PhpType::Array(Box::new(PhpType::Mixed)); - let assoc_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; - - abi::emit_load_from_address(emitter, tag_reg, mixed_reg, 0); - abi::emit_load_from_address(emitter, payload_reg, mixed_reg, 8); - abi::emit_push_reg(emitter, payload_reg); // preserve the unboxed runtime argument container while branching by Mixed tag - call_user_func_array::emit_branch_if_mixed_arg_tag( - tag_reg, - crate::codegen::runtime_value_tag(&indexed_ty), - &indexed_label, - emitter, - ); - call_user_func_array::emit_branch_if_mixed_arg_tag( - tag_reg, - crate::codegen::runtime_value_tag(&assoc_ty), - &assoc_label, - emitter, - ); - call_user_func_array::emit_call_user_func_array_invalid_mixed_args_abort(emitter, data); - - emitter.label(&indexed_label); - abi::emit_load_temporary_stack_slot(emitter, mixed_reg, 0); - abi::emit_release_temporary_stack(emitter, 16); // discard the borrowed unboxed indexed-array pointer before rebuilding args - emit_receiver_prefixed_runtime_indexed_payload_arg_mixed(emitter); - abi::emit_jump(emitter, &done_label); - - emitter.label(&assoc_label); - abi::emit_load_temporary_stack_slot(emitter, mixed_reg, 0); - abi::emit_release_temporary_stack(emitter, 16); // discard the borrowed unboxed hash pointer before rebuilding args - emit_receiver_prefixed_assoc_payload_arg_mixed(emitter, ctx); - - emitter.label(&done_label); -} - -/// Builds a receiver-prefixed argument container from a runtime Mixed array/hash and saved receiver. -fn emit_saved_receiver_prefixed_opaque_arg_mixed( - object_stack_offset: usize, - arg_array: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("saved receiver-prefixed mixed call_user_func_array descriptor args"); - emit_push_saved_object_receiver_mixed(object_stack_offset, emitter); - emit_receiver_prefixed_opaque_arg_mixed_after_receiver_push(arg_array, emitter, ctx, data); -} - -/// Boxes a saved object pointer and preserves it as the synthetic receiver Mixed cell. -fn emit_push_saved_object_receiver_mixed(object_stack_offset: usize, emitter: &mut Emitter) { - let object_reg = abi::secondary_scratch_reg(emitter); - let zero_reg = abi::tertiary_scratch_reg(emitter); - let tag_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, object_reg, object_stack_offset); - abi::emit_load_int_immediate(emitter, zero_reg, 0); - abi::emit_load_int_immediate( - emitter, - tag_reg, - crate::codegen::runtime_value_tag(&PhpType::Object(String::new())) as i64, - ); - crate::codegen::emit_box_runtime_payload_as_mixed(emitter, tag_reg, object_reg, zero_reg); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed saved receiver before evaluating the source argument container -} - -/// Allocates a Mixed-valued hash sized for receiver plus the cloned source hash. -fn emit_new_receiver_prefixed_hash(source_stack_offset: usize, emitter: &mut Emitter) { - let capacity_reg = abi::int_arg_reg_name(emitter.target, 0); - let tag_reg = abi::int_arg_reg_name(emitter.target, 1); - let source_reg = abi::secondary_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, source_reg, source_stack_offset); - abi::emit_load_from_address(emitter, capacity_reg, source_reg, 8); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #1", capacity_reg, capacity_reg)); // reserve one extra hash slot for the synthetic receiver argument - } - Arch::X86_64 => { - emitter.instruction(&format!("add {}, 1", capacity_reg)); // reserve one extra hash slot for the synthetic receiver argument - } - } - abi::emit_load_int_immediate( - emitter, - tag_reg, - crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64, - ); - abi::emit_call_label(emitter, "__rt_hash_new"); -} - -/// Inserts the boxed receiver Mixed cell into numeric hash key zero. -fn emit_insert_receiver_hash_entry(emitter: &mut Emitter) { - let dest_hash_off = 0; - let receiver_off = 32; - let hash_reg = abi::int_arg_reg_name(emitter.target, 0); - let key_ptr_reg = abi::int_arg_reg_name(emitter.target, 1); - let key_len_reg = abi::int_arg_reg_name(emitter.target, 2); - let value_lo_reg = abi::int_arg_reg_name(emitter.target, 3); - let value_hi_reg = abi::int_arg_reg_name(emitter.target, 4); - let value_tag_reg = abi::int_arg_reg_name(emitter.target, 5); - let stack_reg = temporary_stack_reg(emitter); - - abi::emit_load_temporary_stack_slot(emitter, hash_reg, dest_hash_off); - abi::emit_load_int_immediate(emitter, key_ptr_reg, 0); - abi::emit_load_int_immediate(emitter, key_len_reg, -1); - abi::emit_load_temporary_stack_slot(emitter, value_lo_reg, receiver_off); - abi::emit_load_int_immediate(emitter, value_hi_reg, 0); - abi::emit_load_int_immediate( - emitter, - value_tag_reg, - crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64, - ); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), stack_reg, dest_hash_off); -} - -/// Copies cloned Mixed source entries into the destination hash, shifting numeric keys by one. -fn emit_copy_shifted_assoc_hash_entries(emitter: &mut Emitter, ctx: &mut Context) { - let loop_label = ctx.next_label("receiver_assoc_loop"); - let done_label = ctx.next_label("receiver_assoc_done"); - let numeric_key_label = ctx.next_label("receiver_assoc_numeric_key"); - let insert_label = ctx.next_label("receiver_assoc_insert"); - let dest_hash_off = HASH_SCRATCH_BYTES; - let source_hash_off = HASH_SCRATCH_BYTES + 16; - let stack_reg = temporary_stack_reg(emitter); - - abi::emit_reserve_temporary_stack(emitter, HASH_SCRATCH_BYTES); - abi::emit_load_int_immediate(emitter, abi::secondary_scratch_reg(emitter), 0); - abi::emit_store_to_address( - emitter, - abi::secondary_scratch_reg(emitter), - stack_reg, - CURSOR_OFF, - ); - - emitter.label(&loop_label); - emit_load_next_assoc_entry(source_hash_off, emitter); - emit_branch_if_assoc_iteration_done(&done_label, emitter); - emit_store_loaded_assoc_entry(emitter); - emit_branch_if_assoc_key_is_numeric(&numeric_key_label, emitter); - abi::emit_jump(emitter, &insert_label); - - emitter.label(&numeric_key_label); - emit_shift_numeric_assoc_key(emitter); - - emitter.label(&insert_label); - emit_retain_loaded_mixed_value(emitter); - emit_insert_loaded_assoc_entry(dest_hash_off, emitter); - abi::emit_jump(emitter, &loop_label); - - emitter.label(&done_label); - abi::emit_release_temporary_stack(emitter, HASH_SCRATCH_BYTES); -} - -/// Calls the hash iterator for the cloned source hash and current cursor. -fn emit_load_next_assoc_entry(source_hash_off: usize, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x0", source_hash_off); - abi::emit_load_temporary_stack_slot(emitter, "x1", CURSOR_OFF); - abi::emit_call_label(emitter, "__rt_hash_iter_next"); - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rdi", source_hash_off); - abi::emit_load_temporary_stack_slot(emitter, "rsi", CURSOR_OFF); - abi::emit_call_label(emitter, "__rt_hash_iter_next"); - } - } -} - -/// Branches to `done_label` when the source hash iterator has reached the end. -fn emit_branch_if_assoc_iteration_done(done_label: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmn x0, #1"); // has the receiver-prefixed source hash scan reached the terminal cursor? - emitter.instruction(&format!("b.eq {}", done_label)); // finish copying source entries when the hash iterator is exhausted - } - Arch::X86_64 => { - emitter.instruction("cmp rax, -1"); // has the receiver-prefixed source hash scan reached the terminal cursor? - emitter.instruction(&format!("je {}", done_label)); // finish copying source entries when the hash iterator is exhausted - } - } -} - -/// Stores the hash iterator outputs in scratch slots before nested helper calls. -fn emit_store_loaded_assoc_entry(emitter: &mut Emitter) { - let stack_reg = temporary_stack_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_store_to_address(emitter, "x0", stack_reg, CURSOR_OFF); - abi::emit_store_to_address(emitter, "x1", stack_reg, KEY_PTR_OFF); - abi::emit_store_to_address(emitter, "x2", stack_reg, KEY_LEN_OFF); - abi::emit_store_to_address(emitter, "x3", stack_reg, VALUE_LO_OFF); - abi::emit_store_to_address(emitter, "x4", stack_reg, VALUE_HI_OFF); - abi::emit_store_to_address(emitter, "x5", stack_reg, VALUE_TAG_OFF); - } - Arch::X86_64 => { - abi::emit_store_to_address(emitter, "rax", stack_reg, CURSOR_OFF); - abi::emit_store_to_address(emitter, "rdi", stack_reg, KEY_PTR_OFF); - abi::emit_store_to_address(emitter, "rdx", stack_reg, KEY_LEN_OFF); - abi::emit_store_to_address(emitter, "rcx", stack_reg, VALUE_LO_OFF); - abi::emit_store_to_address(emitter, "r8", stack_reg, VALUE_HI_OFF); - abi::emit_store_to_address(emitter, "r9", stack_reg, VALUE_TAG_OFF); - } - } -} - -/// Branches to `numeric_label` when the current source key is numeric. -fn emit_branch_if_assoc_key_is_numeric(numeric_label: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x9", KEY_LEN_OFF); - emitter.instruction("cmn x9, #1"); // does the copied argument key use PHP's integer-key sentinel? - emitter.instruction(&format!("b.eq {}", numeric_label)); // shift numeric keys so descriptor slot zero remains the receiver - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r11", KEY_LEN_OFF); - emitter.instruction("cmp r11, -1"); // does the copied argument key use PHP's integer-key sentinel? - emitter.instruction(&format!("je {}", numeric_label)); // shift numeric keys so descriptor slot zero remains the receiver - } - } -} - -/// Rewrites a numeric source key from `n` to `n + 1` for the receiver slot. -fn emit_shift_numeric_assoc_key(emitter: &mut Emitter) { - let stack_reg = temporary_stack_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x9", KEY_PTR_OFF); - emitter.instruction("add x9, x9, #1"); // shift a positional argument key after the synthetic receiver slot - abi::emit_store_to_address(emitter, "x9", stack_reg, KEY_PTR_OFF); - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r11", KEY_PTR_OFF); - emitter.instruction("add r11, 1"); // shift a positional argument key after the synthetic receiver slot - abi::emit_store_to_address(emitter, "r11", stack_reg, KEY_PTR_OFF); - } - } -} - -/// Retains the loaded Mixed value cell before inserting it into the destination hash. -fn emit_retain_loaded_mixed_value(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x0", VALUE_LO_OFF); - abi::emit_call_label(emitter, "__rt_incref"); - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rax", VALUE_LO_OFF); - abi::emit_call_label(emitter, "__rt_incref"); - } - } -} - -/// Inserts the retained source entry into the receiver-prefixed destination hash. -fn emit_insert_loaded_assoc_entry(dest_hash_off: usize, emitter: &mut Emitter) { - let hash_reg = abi::int_arg_reg_name(emitter.target, 0); - let key_ptr_reg = abi::int_arg_reg_name(emitter.target, 1); - let key_len_reg = abi::int_arg_reg_name(emitter.target, 2); - let value_lo_reg = abi::int_arg_reg_name(emitter.target, 3); - let value_hi_reg = abi::int_arg_reg_name(emitter.target, 4); - let value_tag_reg = abi::int_arg_reg_name(emitter.target, 5); - let stack_reg = temporary_stack_reg(emitter); - - abi::emit_load_temporary_stack_slot(emitter, hash_reg, dest_hash_off); - abi::emit_load_temporary_stack_slot(emitter, key_ptr_reg, KEY_PTR_OFF); - abi::emit_load_temporary_stack_slot(emitter, key_len_reg, KEY_LEN_OFF); - abi::emit_load_temporary_stack_slot(emitter, value_lo_reg, VALUE_LO_OFF); - abi::emit_load_temporary_stack_slot(emitter, value_hi_reg, VALUE_HI_OFF); - abi::emit_load_temporary_stack_slot(emitter, value_tag_reg, VALUE_TAG_OFF); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), stack_reg, dest_hash_off); -} - -/// Boxes the finished receiver-prefixed container while moving it away from the result register first. -fn emit_box_receiver_prefixed_container( - result_reg: &str, - container_ty: &PhpType, - emitter: &mut Emitter, -) { - let container_reg = abi::nested_call_reg(emitter); - if container_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", container_reg, result_reg)); // move the receiver-prefixed container away from the result register before Mixed boxing installs the tag - } - call_user_func_array::emit_box_invoker_arg_clone_as_mixed( - container_reg, - container_ty, - emitter, - ); - if container_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", result_reg, container_reg)); // return the boxed Mixed argument container through the standard expression result register - } -} - -/// Returns the target stack pointer register name for temporary stack slots. -fn temporary_stack_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "sp", - Arch::X86_64 => "rsp", - } -} diff --git a/src/codegen/builtins/arrays/rsort.rs b/src/codegen/builtins/arrays/rsort.rs deleted file mode 100644 index 4c0a177155..0000000000 --- a/src/codegen/builtins/arrays/rsort.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Purpose: -//! Emits PHP `rsort` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the PHP `rsort` builtin, mutating the first argument array in place -/// in descending order. COW is handled via `emit_ensure_unique_arg` before the call, and -/// any replacement array pointer is written back to caller storage via -/// `emit_store_mutating_arg` after the call. -/// -/// # Arguments -/// - `_name`: unused, matches the `BuiltinDef` dispatcher signature -/// - `args`: first arg is the array to sort; additional args (flags) are currently unused -/// - `emitter`, `ctx`, `data`: standard codegen context -/// -/// # Returns -/// `Some(PhpType::Void)` since rsort has no meaningful return value for assignment -/// -/// # Runtime behavior -/// Calls `__rt_rsort_int` to sort indexed integer arrays descending in place; caller -/// must ensure no value-temp preevaluation occurs for mutating/ref-like arguments. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rsort()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - // -- sort the array in place, dispatching on the element family -- - let sort_runtime = match &arr_ty { - PhpType::Array(elem) if matches!(**elem, PhpType::Str) => "__rt_rsort_str", - _ => "__rt_rsort_int", - }; - abi::emit_call_label(emitter, sort_runtime); // sort the indexed array descending in place (string- or integer-aware) - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/runtime_callable_array_callback.rs b/src/codegen/builtins/arrays/runtime_callable_array_callback.rs deleted file mode 100644 index 41677bb22f..0000000000 --- a/src/codegen/builtins/arrays/runtime_callable_array_callback.rs +++ /dev/null @@ -1,1184 +0,0 @@ -//! Purpose: -//! Selects dynamic callable-array descriptors for callback-style runtimes. -//! Builds descriptor callback environments for `[$object, $method]` and -//! `[$class, $method]` values whose slots are only known at runtime. -//! -//! Called from: -//! - Fixed-return array callback builtins such as `array_filter()` and sort helpers. -//! - Caller-managed callback runtimes such as `preg_replace_callback()`. -//! -//! Key details: -//! - The caller must have already pushed the source array pointer before callback -//! selection, preserving PHP argument evaluation order for second-argument callbacks. -//! - For first-argument callbacks such as `array_map()`, this module can reserve -//! the saved-array slot before selector evaluation and fill it after the array is evaluated. -//! - Caller-managed runtimes can consume the selected descriptor after this module -//! has matched the runtime callable-array slots and discarded selector scratch storage. -//! - Each matched descriptor gets a shape-specific wrapper so instance methods can -//! receive their saved receiver prefix while static methods receive only visible args. - -use crate::codegen::abi; -use crate::codegen::callable_dispatch::{ - RuntimeCallableCase, RuntimeInstanceMethodCallableCase, RuntimeStaticMethodCallableCase, -}; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -use super::callback_env; - -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 STRING_METHOD_OFFSET: usize = 0; -const STRING_CLASS_OFFSET: usize = 16; -const STRING_SELECTOR_BYTES: usize = 32; -const SAVED_ARRAY_BYTES: usize = 16; - -/// Emits runtime callable-array descriptor selection for a callback runtime with a saved array. -#[allow(clippy::too_many_arguments)] -pub(crate) fn emit_after_saved_array( - callback: &Expr, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - mut emit_runtime_call: F, -) -> bool -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let ExprKind::Variable(var_name) = &callback.kind else { - return false; - }; - if ctx.callable_array_targets.contains_key(var_name) { - return false; - } - let Some(var_info) = ctx.variables.get(var_name) else { - return false; - }; - - match var_info.ty.codegen_repr() { - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Mixed) => { - emit_mixed_after_saved_array( - var_name, - array_reg, - visible_arg_types, - descriptor_return_type, - emitter, - ctx, - data, - &mut emit_runtime_call, - ); - true - } - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Str) => { - emit_string_after_saved_array( - var_name, - array_reg, - visible_arg_types, - descriptor_return_type, - emitter, - ctx, - data, - &mut emit_runtime_call, - ); - true - } - _ => false, - } -} - -/// Emits runtime callable-array descriptor selection before evaluating the source array. -#[allow(clippy::too_many_arguments)] -pub(crate) fn emit_before_array( - callback: &Expr, - array: &Expr, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - mut emit_runtime_call: F, -) -> bool -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let ExprKind::Variable(var_name) = &callback.kind else { - return false; - }; - if ctx.callable_array_targets.contains_key(var_name) { - return false; - } - let Some(var_info) = ctx.variables.get(var_name) else { - return false; - }; - - match var_info.ty.codegen_repr() { - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Mixed) => { - emit_mixed_before_array( - var_name, - array, - array_reg, - visible_arg_types, - descriptor_return_type, - emitter, - ctx, - data, - &mut emit_runtime_call, - ); - true - } - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Str) => { - emit_string_before_array( - var_name, - array, - array_reg, - visible_arg_types, - descriptor_return_type, - emitter, - ctx, - data, - &mut emit_runtime_call, - ); - true - } - _ => false, - } -} - -/// Emits runtime callable-array descriptor selection for caller-managed callback payloads. -/// -/// This mode matches a runtime `[$object, $method]` or `[$class, $method]` -/// variable, releases the selector scratch slots inside the matched case, and -/// then lets the caller build the descriptor environment and runtime call. For -/// instance-method cases, the selected receiver is pushed on top of the temporary -/// stack before `emit_selected_case` runs and must be consumed by the caller. -pub(crate) fn emit_without_saved_array( - callback: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - mut emit_selected_case: F, -) -> bool -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - let ExprKind::Variable(var_name) = &callback.kind else { - return false; - }; - if ctx.callable_array_targets.contains_key(var_name) { - return false; - } - let Some(var_info) = ctx.variables.get(var_name) else { - return false; - }; - - match var_info.ty.codegen_repr() { - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Mixed) => { - emit_mixed_without_saved_array(var_name, emitter, ctx, data, &mut emit_selected_case); - true - } - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Str) => { - emit_string_without_saved_array(var_name, emitter, ctx, data, &mut emit_selected_case); - true - } - _ => false, - } -} - -/// Emits runtime callable-array descriptor selection for a two-slot literal callback. -/// -/// The selected-case callback may consume an instance receiver from the temporary -/// stack, but it must leave the preserved literal array as the top stack slot so -/// this helper can release the temporary array before returning to the caller. -pub(crate) fn emit_literal_without_saved_array( - callback: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - mut emit_selected_case: F, -) -> bool -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - if !is_two_slot_callable_array_literal(callback) { - return false; - } - let callback_ty = crate::codegen::functions::infer_contextual_type(callback, ctx).codegen_repr(); - match callback_ty { - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Mixed) => { - emit_mixed_literal_without_saved_array( - callback, - emitter, - ctx, - data, - &mut emit_selected_case, - ); - true - } - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Str) => { - emit_string_literal_without_saved_array( - callback, - emitter, - ctx, - data, - &mut emit_selected_case, - ); - true - } - _ => false, - } -} - -/// Emits descriptor selection for heterogeneous callable arrays above a saved source array. -#[allow(clippy::too_many_arguments)] -fn emit_mixed_after_saved_array( - var_name: &str, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_runtime_call: &mut F, -) -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let instance_cases = - crate::codegen::callable_dispatch::runtime_public_instance_method_cases(ctx, data); - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - emit_mixed_selector_slots(var_name, emitter, ctx, data); - emit_mixed_dispatch( - &instance_cases, - &static_cases, - array_reg, - &visible_arg_types, - &descriptor_return_type, - emitter, - ctx, - data, - emit_runtime_call, - ); - abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES + SAVED_ARRAY_BYTES); -} - -/// Emits descriptor selection for heterogeneous callable arrays before the source array. -#[allow(clippy::too_many_arguments)] -fn emit_mixed_before_array( - var_name: &str, - array: &Expr, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_runtime_call: &mut F, -) -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let instance_cases = - crate::codegen::callable_dispatch::runtime_public_instance_method_cases(ctx, data); - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - abi::emit_reserve_temporary_stack(emitter, SAVED_ARRAY_BYTES); - emit_mixed_selector_slots(var_name, emitter, ctx, data); - crate::codegen::expr::emit_expr(array, emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", array_reg, abi::int_result_reg(emitter))); // preserve the mapped array pointer before descriptor-case selection - emit_store_saved_array_reg(array_reg, MIXED_SELECTOR_BYTES, emitter); - emit_mixed_dispatch( - &instance_cases, - &static_cases, - array_reg, - &visible_arg_types, - &descriptor_return_type, - emitter, - ctx, - data, - emit_runtime_call, - ); - abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES + SAVED_ARRAY_BYTES); -} - -/// Emits descriptor selection for string callable arrays above a saved source array. -#[allow(clippy::too_many_arguments)] -fn emit_string_after_saved_array( - var_name: &str, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_runtime_call: &mut F, -) -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - emit_string_selector_slots(var_name, emitter, ctx, data); - emit_string_dispatch( - &static_cases, - array_reg, - &visible_arg_types, - &descriptor_return_type, - emitter, - ctx, - data, - emit_runtime_call, - ); - abi::emit_release_temporary_stack(emitter, STRING_SELECTOR_BYTES + SAVED_ARRAY_BYTES); -} - -/// Emits descriptor selection for string callable arrays before the source array. -#[allow(clippy::too_many_arguments)] -fn emit_string_before_array( - var_name: &str, - array: &Expr, - array_reg: &str, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_runtime_call: &mut F, -) -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - abi::emit_reserve_temporary_stack(emitter, SAVED_ARRAY_BYTES); - emit_string_selector_slots(var_name, emitter, ctx, data); - crate::codegen::expr::emit_expr(array, emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", array_reg, abi::int_result_reg(emitter))); // preserve the mapped array pointer before descriptor-case selection - emit_store_saved_array_reg(array_reg, STRING_SELECTOR_BYTES, emitter); - emit_string_dispatch( - &static_cases, - array_reg, - &visible_arg_types, - &descriptor_return_type, - emitter, - ctx, - data, - emit_runtime_call, - ); - abi::emit_release_temporary_stack(emitter, STRING_SELECTOR_BYTES + SAVED_ARRAY_BYTES); -} - -/// Emits heterogeneous callable-array descriptor selection without a saved source array. -fn emit_mixed_without_saved_array( - var_name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_selected_case: &mut F, -) -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - let instance_cases = - crate::codegen::callable_dispatch::runtime_public_instance_method_cases(ctx, data); - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - emit_mixed_selector_slots(var_name, emitter, ctx, data); - emit_mixed_dispatch_without_saved_array( - &instance_cases, - &static_cases, - emitter, - ctx, - data, - emit_selected_case, - ); -} - -/// Emits static-method string callable-array descriptor selection without a saved source array. -fn emit_string_without_saved_array( - var_name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_selected_case: &mut F, -) -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - emit_string_selector_slots(var_name, emitter, ctx, data); - emit_string_dispatch_without_saved_array(&static_cases, emitter, ctx, data, emit_selected_case); -} - -/// Emits heterogeneous callable-array descriptor selection for a runtime literal. -fn emit_mixed_literal_without_saved_array( - callback: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_selected_case: &mut F, -) -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - let instance_cases = - crate::codegen::callable_dispatch::runtime_public_instance_method_cases(ctx, data); - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - crate::codegen::expr::emit_expr(callback, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the runtime callable-array literal while selecting its descriptor - emit_mixed_literal_selector_slots(emitter); - emit_mixed_dispatch_without_saved_array( - &instance_cases, - &static_cases, - emitter, - ctx, - data, - emit_selected_case, - ); - release_preserved_literal_array_after_selection( - &PhpType::Array(Box::new(PhpType::Mixed)), - emitter, - ); -} - -/// Emits static-method string callable-array descriptor selection for a runtime literal. -fn emit_string_literal_without_saved_array( - callback: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_selected_case: &mut F, -) -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - crate::codegen::expr::emit_expr(callback, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the runtime static callable-array literal while selecting its descriptor - emit_string_literal_selector_slots(emitter); - emit_string_dispatch_without_saved_array(&static_cases, emitter, ctx, data, emit_selected_case); - release_preserved_literal_array_after_selection(&PhpType::Array(Box::new(PhpType::Str)), emitter); -} - -/// Saves the unboxed receiver and method slots for a runtime heterogeneous callable array. -fn emit_mixed_selector_slots( - var_name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("runtime callback callable-array mixed selector"); - let receiver = callable_array_slot_expr(var_name, 0); - crate::codegen::expr::emit_expr(&receiver, emitter, ctx, data); - emit_unbox_mixed_result(emitter); - emit_push_mixed_unbox_payload(emitter); - - let method = callable_array_slot_expr(var_name, 1); - crate::codegen::expr::emit_expr(&method, emitter, ctx, data); - emit_unbox_mixed_result(emitter); - emit_push_mixed_unbox_payload(emitter); -} - -/// Saves class and method string slots for a runtime static-method callable array. -fn emit_string_selector_slots( - var_name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("runtime callback callable-array string selector"); - let class = callable_array_slot_expr(var_name, 0); - crate::codegen::expr::emit_expr(&class, emitter, ctx, data); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime class string while the method slot is read - - let method = callable_array_slot_expr(var_name, 1); - crate::codegen::expr::emit_expr(&method, emitter, ctx, data); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime method string for descriptor-case selection -} - -/// Saves selector slots read from an already-evaluated mixed callable-array literal. -fn emit_mixed_literal_selector_slots(emitter: &mut Emitter) { - emitter.comment("runtime callback callable-array literal mixed selector"); - emit_unbox_mixed_literal_slot(0, 0, emitter); - emit_push_mixed_unbox_payload(emitter); - emit_unbox_mixed_literal_slot(32, 1, emitter); - emit_push_mixed_unbox_payload(emitter); -} - -/// Saves selector slots read from an already-evaluated string callable-array literal. -fn emit_string_literal_selector_slots(emitter: &mut Emitter) { - emitter.comment("runtime callback callable-array literal string selector"); - emit_push_string_literal_slot(0, 0, emitter); - emit_push_string_literal_slot(16, 1, emitter); -} - -/// Loads and unboxes one boxed Mixed slot from a preserved callable-array literal. -fn emit_unbox_mixed_literal_slot(array_stack_offset: usize, slot: usize, emitter: &mut Emitter) { - let array_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, array_reg, array_stack_offset); - abi::emit_load_from_address( - emitter, - abi::int_result_reg(emitter), - array_reg, - 24 + slot * 8, - ); - emit_unbox_mixed_result(emitter); -} - -/// Loads and saves one string slot from a preserved callable-array literal. -fn emit_push_string_literal_slot(array_stack_offset: usize, slot: usize, emitter: &mut Emitter) { - let array_reg = abi::symbol_scratch_reg(emitter); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, array_reg, array_stack_offset); - abi::emit_load_from_address(emitter, ptr_reg, array_reg, 24 + slot * 16); - abi::emit_load_from_address(emitter, len_reg, array_reg, 24 + slot * 16 + 8); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime string slot for descriptor-case selection -} - -/// Unboxes the current Mixed result into target-specific tag and payload registers. -fn emit_unbox_mixed_result(emitter: &mut Emitter) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); -} - -/// Pushes the unboxed Mixed tag and payload onto the temporary stack. -fn emit_push_mixed_unbox_payload(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the unboxed callable-array payload words for callback selection - abi::emit_push_reg(emitter, "x0"); // preserve the unboxed callable-array tag beside its payload - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rdi", "rdx"); // preserve the unboxed callable-array payload words for callback selection - abi::emit_push_reg(emitter, "rax"); // preserve the unboxed callable-array tag beside its payload - } - } -} - -/// Stores the mapped source array into the reserved saved-array stack slot. -fn emit_store_saved_array_reg(array_reg: &str, offset: usize, emitter: &mut Emitter) { - let scratch = abi::symbol_scratch_reg(emitter); - abi::emit_temporary_stack_address(emitter, scratch, offset); - abi::emit_store_to_address(emitter, array_reg, scratch, 0); -} - -/// Releases a preserved callable-array literal after caller-managed descriptor selection. -fn release_preserved_literal_array_after_selection(arr_ty: &PhpType, emitter: &mut Emitter) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the selected-case result while releasing the temporary callable-array literal - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, arr_ty); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the selected-case result after literal cleanup - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callable-array literal slot -} - -/// Dispatches a heterogeneous callable array to a descriptor-backed callback runtime call. -#[allow(clippy::too_many_arguments)] -fn emit_mixed_dispatch( - instance_cases: &[RuntimeInstanceMethodCallableCase], - static_cases: &[RuntimeStaticMethodCallableCase], - array_reg: &str, - visible_arg_types: &[PhpType], - descriptor_return_type: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_runtime_call: &mut F, -) -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let done_label = ctx.next_label("runtime_callback_array_done"); - for case in instance_cases { - let next_case = ctx.next_label("runtime_callback_array_instance_next"); - emit_branch_if_mixed_instance_case_mismatch(case, &next_case, emitter, ctx, data); - emit_instance_case_callback( - &case.case, - array_reg, - visible_arg_types, - descriptor_return_type, - emitter, - ctx, - emit_runtime_call, - data, - ); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - for case in static_cases { - let next_case = ctx.next_label("runtime_callback_array_static_next"); - emit_branch_if_mixed_static_case_mismatch(case, &next_case, emitter, ctx, data); - emit_static_case_callback( - &case.case, - array_reg, - MIXED_SELECTOR_BYTES, - visible_arg_types, - descriptor_return_type, - emitter, - ctx, - emit_runtime_call, - data, - ); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - emit_no_match_abort(emitter, data); - emitter.label(&done_label); -} - -/// Dispatches a string callable array to a descriptor-backed callback runtime call. -#[allow(clippy::too_many_arguments)] -fn emit_string_dispatch( - static_cases: &[RuntimeStaticMethodCallableCase], - array_reg: &str, - visible_arg_types: &[PhpType], - descriptor_return_type: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_runtime_call: &mut F, -) -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let done_label = ctx.next_label("runtime_callback_array_done"); - for case in static_cases { - let next_case = ctx.next_label("runtime_callback_array_static_next"); - emit_branch_if_string_static_case_mismatch(case, &next_case, emitter, ctx, data); - emit_static_case_callback( - &case.case, - array_reg, - STRING_SELECTOR_BYTES, - visible_arg_types, - descriptor_return_type, - emitter, - ctx, - emit_runtime_call, - data, - ); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - emit_no_match_abort(emitter, data); - emitter.label(&done_label); -} - -/// Dispatches a heterogeneous callable array for caller-managed callback runtime emission. -fn emit_mixed_dispatch_without_saved_array( - instance_cases: &[RuntimeInstanceMethodCallableCase], - static_cases: &[RuntimeStaticMethodCallableCase], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_selected_case: &mut F, -) -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - let done_label = ctx.next_label("runtime_callback_array_done"); - for case in instance_cases { - let next_case = ctx.next_label("runtime_callback_array_instance_next"); - emit_branch_if_mixed_instance_case_mismatch(case, &next_case, emitter, ctx, data); - emit_instance_case_without_saved_array( - &case.case, - emitter, - ctx, - data, - emit_selected_case, - ); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - for case in static_cases { - let next_case = ctx.next_label("runtime_callback_array_static_next"); - emit_branch_if_mixed_static_case_mismatch(case, &next_case, emitter, ctx, data); - emit_static_case_without_saved_array( - &case.case, - MIXED_SELECTOR_BYTES, - emitter, - ctx, - data, - emit_selected_case, - ); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - emit_no_match_abort(emitter, data); - emitter.label(&done_label); -} - -/// Dispatches a string callable array for caller-managed callback runtime emission. -fn emit_string_dispatch_without_saved_array( - static_cases: &[RuntimeStaticMethodCallableCase], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_selected_case: &mut F, -) -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - let done_label = ctx.next_label("runtime_callback_array_done"); - for case in static_cases { - let next_case = ctx.next_label("runtime_callback_array_static_next"); - emit_branch_if_string_static_case_mismatch(case, &next_case, emitter, ctx, data); - emit_static_case_without_saved_array( - &case.case, - STRING_SELECTOR_BYTES, - emitter, - ctx, - data, - emit_selected_case, - ); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - emit_no_match_abort(emitter, data); - emitter.label(&done_label); -} - -/// Emits the runtime call for one selected instance-method descriptor case. -#[allow(clippy::too_many_arguments)] -fn emit_instance_case_callback( - case: &RuntimeCallableCase, - array_reg: &str, - visible_arg_types: &[PhpType], - descriptor_return_type: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - emit_runtime_call: &mut F, - data: &mut DataSection, -) -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - let receiver_ty = case - .sig - .params - .first() - .map(|(_, ty)| ty.clone()) - .unwrap_or(PhpType::Mixed); - let call_reg = abi::nested_call_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, call_reg, MIXED_RECEIVER_PAYLOAD_OFFSET); - abi::emit_load_temporary_stack_slot(emitter, array_reg, MIXED_SELECTOR_BYTES); - let wrapper = callback_env::emit_descriptor_callback_env_from_static_descriptor( - &case.descriptor_label, - visible_arg_types.to_vec(), - vec![receiver_ty.clone()], - descriptor_return_type.clone(), - emitter, - ctx, - ); - emitter.instruction(&format!("mov {}, {}", abi::int_result_reg(emitter), call_reg)); // restore the runtime callable-array receiver for descriptor prefix storage - callback_env::store_descriptor_callback_prefix_result(&wrapper, 0, &receiver_ty, emitter); - callback_env::store_descriptor_callback_array_reg(&wrapper, array_reg, emitter); - emit_runtime_call(&wrapper, emitter, ctx, data); - callback_env::release_descriptor_callback_env(&wrapper, emitter); -} - -/// Emits the runtime call for one selected static-method descriptor case. -#[allow(clippy::too_many_arguments)] -fn emit_static_case_callback( - case: &RuntimeCallableCase, - array_reg: &str, - saved_array_offset: usize, - visible_arg_types: &[PhpType], - descriptor_return_type: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - emit_runtime_call: &mut F, - data: &mut DataSection, -) -where - F: FnMut( - &callback_env::DescriptorCallbackEnv, - &mut Emitter, - &mut Context, - &mut DataSection, - ), -{ - abi::emit_load_temporary_stack_slot(emitter, array_reg, saved_array_offset); - let wrapper = callback_env::emit_descriptor_callback_env_from_static_descriptor( - &case.descriptor_label, - visible_arg_types.to_vec(), - Vec::new(), - descriptor_return_type.clone(), - emitter, - ctx, - ); - callback_env::store_descriptor_callback_array_reg(&wrapper, array_reg, emitter); - emit_runtime_call(&wrapper, emitter, ctx, data); - callback_env::release_descriptor_callback_env(&wrapper, emitter); -} - -/// Emits one selected instance-method case for a caller-managed callback runtime. -/// -/// The receiver is copied out of the selector scratch slots before they are -/// released, then pushed back onto the temporary stack so the caller can prepend -/// it to the descriptor environment after evaluating any remaining PHP arguments. -fn emit_instance_case_without_saved_array( - case: &RuntimeCallableCase, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_selected_case: &mut F, -) -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - let receiver_ty = case - .sig - .params - .first() - .map(|(_, ty)| ty.clone()) - .unwrap_or(PhpType::Mixed); - let call_reg = abi::nested_call_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, call_reg, MIXED_RECEIVER_PAYLOAD_OFFSET); - abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); - abi::emit_push_reg(emitter, call_reg); // preserve the selected runtime callable-array receiver for caller-managed emission - emit_selected_case(case, Some(&receiver_ty), emitter, ctx, data); -} - -/// Emits one selected static-method case for a caller-managed callback runtime. -fn emit_static_case_without_saved_array( - case: &RuntimeCallableCase, - selector_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - emit_selected_case: &mut F, -) -where - F: FnMut(&RuntimeCallableCase, Option<&PhpType>, &mut Emitter, &mut Context, &mut DataSection), -{ - abi::emit_release_temporary_stack(emitter, selector_bytes); - emit_selected_case(case, None, emitter, ctx, data); -} - -/// Branches when the saved heterogeneous callable-array slots do not match an instance-method case. -fn emit_branch_if_mixed_instance_case_mismatch( - case: &RuntimeInstanceMethodCallableCase, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_branch_if_stack_tag_mismatch(MIXED_RECEIVER_TAG_OFFSET, 6, next_case, emitter); - emit_branch_if_stack_tag_mismatch(MIXED_METHOD_TAG_OFFSET, 1, next_case, emitter); - emit_branch_if_receiver_class_id_mismatch( - case.class_id, - MIXED_RECEIVER_PAYLOAD_OFFSET, - next_case, - emitter, - ); - emit_branch_if_stack_string_mismatch( - MIXED_METHOD_PAYLOAD_OFFSET, - MIXED_METHOD_PAYLOAD_OFFSET + 8, - case.method_name.as_bytes(), - next_case, - emitter, - ctx, - data, - ); -} - -/// Branches when the saved heterogeneous callable-array slots do not match a static-method case. -fn emit_branch_if_mixed_static_case_mismatch( - case: &RuntimeStaticMethodCallableCase, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_branch_if_stack_tag_mismatch(MIXED_RECEIVER_TAG_OFFSET, 1, next_case, emitter); - emit_branch_if_stack_tag_mismatch(MIXED_METHOD_TAG_OFFSET, 1, next_case, emitter); - emit_branch_if_static_class_string_mismatch( - MIXED_RECEIVER_PAYLOAD_OFFSET, - MIXED_RECEIVER_PAYLOAD_OFFSET + 8, - &case.class_name, - next_case, - emitter, - ctx, - data, - ); - emit_branch_if_stack_string_mismatch( - MIXED_METHOD_PAYLOAD_OFFSET, - MIXED_METHOD_PAYLOAD_OFFSET + 8, - case.method_name.as_bytes(), - next_case, - emitter, - ctx, - data, - ); -} - -/// Branches when the saved string callable-array slots do not match a static-method case. -fn emit_branch_if_string_static_case_mismatch( - case: &RuntimeStaticMethodCallableCase, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_branch_if_static_class_string_mismatch( - STRING_CLASS_OFFSET, - STRING_CLASS_OFFSET + 8, - &case.class_name, - next_case, - emitter, - ctx, - data, - ); - emit_branch_if_stack_string_mismatch( - STRING_METHOD_OFFSET, - STRING_METHOD_OFFSET + 8, - case.method_name.as_bytes(), - next_case, - emitter, - ctx, - data, - ); -} - -/// Branches when a saved Mixed tag stack slot does not equal `expected_tag`. -fn emit_branch_if_stack_tag_mismatch( - tag_offset: usize, - expected_tag: i64, - next_case: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x9", tag_offset); - emitter.instruction(&format!("cmp x9, #{}", expected_tag)); // compare the callable-array callback tag against this descriptor shape - emitter.instruction(&format!("b.ne {}", next_case)); // try the next callback descriptor case when the tag differs - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r10", tag_offset); - emitter.instruction(&format!("cmp r10, {}", expected_tag)); // compare the callable-array callback tag against this descriptor shape - emitter.instruction(&format!("jne {}", next_case)); // try the next callback descriptor case when the tag differs - } - } -} - -/// Branches when the saved receiver object's class id does not match `class_id`. -fn emit_branch_if_receiver_class_id_mismatch( - class_id: u64, - receiver_offset: usize, - next_case: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x9", receiver_offset); - emitter.instruction(&format!("cbz x9, {}", next_case)); // reject null callback receivers before reading their class id - emitter.instruction("ldr x10, [x9]"); // load the callback receiver runtime class id - abi::emit_load_int_immediate(emitter, "x11", class_id as i64); - emitter.instruction("cmp x10, x11"); // compare callback receiver class id against this descriptor case - emitter.instruction(&format!("b.ne {}", next_case)); // try the next descriptor case when the receiver class differs - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r10", receiver_offset); - emitter.instruction("test r10, r10"); // reject null callback receivers before reading their class id - emitter.instruction(&format!("je {}", next_case)); // try the next descriptor case when the receiver pointer is null - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the callback receiver runtime class id - abi::emit_load_int_immediate(emitter, "r10", class_id as i64); - emitter.instruction("cmp r11, r10"); // compare callback receiver class id against this descriptor case - emitter.instruction(&format!("jne {}", next_case)); // try the next descriptor case when the receiver class differs - } - } -} - -/// Branches when a saved class string does not match either bare or leading-slash form. -#[allow(clippy::too_many_arguments)] -fn emit_branch_if_static_class_string_mismatch( - ptr_offset: usize, - len_offset: usize, - class_name: &str, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let matched_label = ctx.next_label("runtime_callback_array_class_match"); - emit_stack_string_compare_branch( - ptr_offset, - len_offset, - class_name.as_bytes(), - &matched_label, - emitter, - data, - ); - let leading_slash = format!("\\{}", class_name); - emit_stack_string_compare_branch( - ptr_offset, - len_offset, - leading_slash.as_bytes(), - &matched_label, - emitter, - data, - ); - abi::emit_jump(emitter, next_case); - emitter.label(&matched_label); -} - -/// Branches when a saved stack string does not match the expected PHP name case-insensitively. -#[allow(clippy::too_many_arguments)] -fn emit_branch_if_stack_string_mismatch( - ptr_offset: usize, - len_offset: usize, - expected: &[u8], - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let matched_label = ctx.next_label("runtime_callback_array_string_match"); - emit_stack_string_compare_branch( - ptr_offset, - len_offset, - expected, - &matched_label, - emitter, - data, - ); - abi::emit_jump(emitter, next_case); - emitter.label(&matched_label); -} - -/// Compares a saved stack string with `expected` and branches to `matched_label` on equality. -fn emit_stack_string_compare_branch( - ptr_offset: usize, - len_offset: usize, - expected: &[u8], - matched_label: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (expected_label, expected_len) = data.add_string(expected); - match emitter.target.arch { - 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"); // did the callable-array callback string match this descriptor name? - emitter.instruction(&format!("b.eq {}", matched_label)); // select this callback descriptor case when names match - } - 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"); // did the callable-array callback string match this descriptor name? - emitter.instruction(&format!("je {}", matched_label)); // select this callback descriptor case when names match - } - } -} - -/// Emits the fatal diagnostic for callable arrays that cannot be resolved to a descriptor. -fn emit_no_match_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: callable array did not resolve to an invokable target\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the callable-array callback diagnostic to stderr - abi::emit_symbol_address(emitter, "x1", &message_label); - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the callable-array callback diagnostic length to write() - emitter.syscall(4); - abi::emit_exit(emitter, 1); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the callable-array callback diagnostic to stderr - abi::emit_symbol_address(emitter, "rsi", &message_label); - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the callable-array callback diagnostic length to write() - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal callable-array callback diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Builds `$callback[$index]`, a positional slot stored inside a callable-array value. -fn callable_array_slot_expr(var_name: &str, index: i64) -> Expr { - Expr::new( - ExprKind::ArrayAccess { - array: Box::new(Expr::new( - ExprKind::Variable(var_name.to_string()), - crate::span::Span::dummy(), - )), - index: Box::new(Expr::new( - ExprKind::IntLiteral(index), - crate::span::Span::dummy(), - )), - }, - crate::span::Span::dummy(), - ) -} - -/// Returns true for a PHP callable-array literal with receiver/class and method slots. -fn is_two_slot_callable_array_literal(callback: &Expr) -> bool { - matches!(&callback.kind, ExprKind::ArrayLiteral(elems) if elems.len() == 2) -} diff --git a/src/codegen/builtins/arrays/runtime_string_callback.rs b/src/codegen/builtins/arrays/runtime_string_callback.rs deleted file mode 100644 index 4015a2d7bc..0000000000 --- a/src/codegen/builtins/arrays/runtime_string_callback.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Purpose: -//! Emits descriptor-backed runtime string callback dispatch for array callback builtins. -//! Shared by callbacks that evaluate and save their source array before callback selection. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::array_filter::emit()`. -//! - `crate::codegen::builtins::arrays::array_reduce::emit()`. -//! - `crate::codegen::builtins::arrays::array_walk::emit()`. -//! -//! Key details: -//! - The caller must have pushed the source array before this helper evaluates the callback. -//! - The saved array remains below the runtime string slot while descriptor cases are checked. - -use crate::codegen::abi; -use crate::codegen::callable_dispatch::{self, RuntimeCallableSelector}; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::call_user_func_array; -use super::callback_env::{self, DescriptorCallbackEnv}; - -const SAVED_ARRAY_AFTER_STRING_OFFSET: usize = 16; - -/// Emits runtime string callback dispatch after the caller has saved the source array. -/// -/// Returns `true` after consuming the saved array and runtime string stack slots. Returns -/// `false` without emitting code when the callback is not a runtime string expression. -#[allow(clippy::too_many_arguments)] -pub(crate) fn emit_after_saved_array( - callback: &Expr, - source_arg_ty: Option<&PhpType>, - visible_arg_types: Vec, - descriptor_return_type: PhpType, - array_arg_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - mut emit_call: F, -) -> bool -where - F: FnMut(&DescriptorCallbackEnv, &mut Emitter, &mut Context, &mut DataSection), -{ - if !call_user_func_array::callback_is_runtime_string(callback, ctx) { - return false; - } - - let call_reg = abi::nested_call_reg(emitter); - let callback_ty = emit_expr(callback, emitter, ctx, data); - debug_assert!(matches!(callback_ty.codegen_repr(), PhpType::Str)); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime string callback name above the saved source array - - let cases = callable_dispatch::runtime_callable_cases(ctx, data, &[], source_arg_ty); - let done_label = ctx.next_label("array_runtime_string_callback_done"); - let selector = RuntimeCallableSelector::StringNameStack { - ptr_offset: 0, - len_offset: 8, - call_reg, - }; - - for case in &cases { - let next_case = ctx.next_label("array_runtime_string_callback_next"); - callable_dispatch::emit_branch_if_callable_case_mismatch( - &selector, - case, - &next_case, - emitter, - ctx, - data, - ); - abi::emit_load_temporary_stack_slot( - emitter, - array_arg_reg, - SAVED_ARRAY_AFTER_STRING_OFFSET, - ); - let wrapper = callback_env::emit_descriptor_callback_env_from_static_descriptor( - &case.descriptor_label, - visible_arg_types.clone(), - Vec::new(), - descriptor_return_type.clone(), - emitter, - ctx, - ); - callback_env::store_descriptor_callback_array_reg(&wrapper, array_arg_reg, emitter); - emit_call(&wrapper, emitter, ctx, data); - callback_env::release_descriptor_callback_env(&wrapper, emitter); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - - call_user_func_array::emit_dynamic_string_callback_abort(emitter, data); - emitter.label(&done_label); - abi::emit_release_temporary_stack(emitter, 32); // discard the runtime string callback name and saved source array - true -} diff --git a/src/codegen/builtins/arrays/shuffle_fn.rs b/src/codegen/builtins/arrays/shuffle_fn.rs deleted file mode 100644 index 74f0b45c67..0000000000 --- a/src/codegen/builtins/arrays/shuffle_fn.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `shuffle` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `shuffle($array)` which mutates the array argument in place. -/// -/// The emitted sequence is: -/// 1. Evaluate and emit the array expression (result in x1:x2) -/// 2. Call `ensure_unique_arg` to prepare the array for COW (copy-on-write) semantics -/// 3. Call `store_mutating_arg` to write the array pointer back to caller storage -/// (shuffle can reorder elements, so the array pointer itself may change) -/// 4. Call `__rt_shuffle` runtime helper which reorders elements in place -/// -/// Returns `PhpType::Void` because PHP's shuffle() returns bool (ignored by the compiler). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("shuffle()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - // -- call runtime to randomly reorder array elements in place -- - abi::emit_call_label(emitter, "__rt_shuffle"); // call the target-aware runtime helper that shuffles indexed arrays in place - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/sort.rs b/src/codegen/builtins/arrays/sort.rs deleted file mode 100644 index ab8b224738..0000000000 --- a/src/codegen/builtins/arrays/sort.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Purpose: -//! Emits PHP `sort` builtin calls that mutate array arguments in place. -//! Handles COW preparation and writes any replacement array pointer back to caller storage. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Mutating/ref-like arguments must avoid value-temp preevaluation so PHP-visible storage is updated. - -use crate::codegen::abi; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the PHP `sort()` builtin call, mutating the input array in place. -/// -/// Inputs: -/// - `args[0]` must be an array-typed expression; it is evaluated for uniqueness and -/// its storage is marked as mutating so the caller sees the updated pointer. -/// - `_name` is unused (present for dispatcher signature compatibility). -/// -/// Side effects: -/// - Calls `emit_ensure_unique_arg` to enforce COW before mutation. -/// - Calls `emit_store_mutating_arg` to preserve PHP-visible storage. -/// - Emits a call to `__rt_sort_int`, the target-aware runtime helper that sorts -/// indexed integer arrays in ascending order. -/// -/// Returns: -/// - `Some(PhpType::Void)` indicating `sort()` has no return value in PHP. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sort()"); - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - // -- sort the array in place, dispatching on the element family -- - let sort_runtime = match &arr_ty { - PhpType::Array(elem) if matches!(**elem, PhpType::Str) => "__rt_sort_str", - _ => "__rt_sort_int", - }; - abi::emit_call_label(emitter, sort_runtime); // sort the indexed array ascending in place (string- or integer-aware) - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/store_mutating_arg.rs b/src/codegen/builtins/arrays/store_mutating_arg.rs deleted file mode 100644 index 3294342a2f..0000000000 --- a/src/codegen/builtins/arrays/store_mutating_arg.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Purpose: -//! Stores a possibly replaced array pointer back into the original mutating argument storage. -//! Handles variable and addressable array arguments after COW or growth routines run. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::*::emit() for mutating array builtins`. -//! -//! Key details: -//! - Must match call-argument by-ref semantics so PHP-visible mutations update the caller slot. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; - -/// Stores a possibly replaced array pointer back into the original mutating argument slot. -/// -/// After COW or growth routines produce a new container pointer in `x0`/`rax`, this function -/// writes that pointer back to the caller's slot based on argument kind: -/// -/// - **Global variable**: stores via the global symbol address computed from `_gvar_` -/// - **By-ref parameter**: loads the reference pointer from the stack slot, then stores through it -/// - **Local variable**: stores directly into the stack frame at the variable's offset -/// -/// The caller is responsible for placing the updated pointer in the appropriate register -/// before calling this function (ARM64: `x0`, x86_64: `rax`). -pub(crate) fn emit_store_mutating_arg(emitter: &mut Emitter, ctx: &Context, arg: &Expr) { - let ExprKind::Variable(name) = &arg.kind else { - return; - }; - - if ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)) { - let label = format!("_gvar_{}", name); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", &label); // resolve the global variable storage address for the mutated array/hash - emitter.instruction("str x0, [x9]"); // overwrite the global slot with the updated container pointer - } - Arch::X86_64 => { - abi::emit_store_reg_to_symbol(emitter, "rax", &label, 0); // overwrite the global slot with the updated container pointer through the x86_64 symbol helper - } - } - } else if ctx.ref_params.contains(name) { - let offset = ctx - .variables - .get(name) - .expect("codegen bug: missing ref-param slot for mutating array builtin") - .stack_offset; - match emitter.target.arch { - Arch::AArch64 => { - abi::load_at_offset(emitter, "x9", offset); // load the by-reference slot that points at the mutating argument storage - emitter.instruction("str x0, [x9]"); // overwrite the referenced slot with the updated container pointer - } - Arch::X86_64 => { - abi::load_at_offset(emitter, "r11", offset); // load the by-reference slot that points at the mutating argument storage - abi::emit_store_to_address(emitter, "rax", "r11", 0); // overwrite the referenced slot with the updated container pointer - } - } - } else if let Some(var) = ctx.variables.get(name) { - match emitter.target.arch { - Arch::AArch64 => { - abi::store_at_offset(emitter, "x0", var.stack_offset); // store the updated container pointer in the local variable slot - } - Arch::X86_64 => { - abi::store_at_offset(emitter, "rax", var.stack_offset); // store the updated container pointer in the local variable slot - } - } - } -} diff --git a/src/codegen/builtins/arrays/uasort.rs b/src/codegen/builtins/arrays/uasort.rs deleted file mode 100644 index 9c2a61ca27..0000000000 --- a/src/codegen/builtins/arrays/uasort.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Purpose: -//! Emits PHP `uasort` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::abi; -use super::callback_env; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::runtime_callable_array_callback; -use super::runtime_string_callback; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `uasort(array &$array, callable $callback): bool` builtin call. -/// -/// Evaluates the array argument first, then resolves the callback address. -/// For captured callbacks, emits a comparator wrapper and calls `__rt_usort`. -/// For simple callbacks, directly calls `__rt_usort` with the comparator address. -/// -/// # Arguments -/// * `name` — builtin name (unused, matched by dispatcher) -/// * `args` — [array, callback] expressions -/// * `emitter` — assembly emitter -/// * `ctx` — codegen context (may be mutated for temporaries) -/// * `data` — data section for literals and symbols -/// -/// # Returns -/// `Some(PhpType::Void)` on success; the mutating `&$array` is handled in-place. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("uasort()"); - - // -- evaluate the array argument (first arg) -- - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let elem_ty = match &arr_ty { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Int, - }; - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - - // -- save array pointer -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the array pointer while the callback address is resolved for the target ABI - - // -- resolve callback function address -- - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - - if runtime_string_callback::emit_after_saved_array( - &args[1], - Some(&arr_ty), - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - array_arg_reg, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a runtime string descriptor comparator - }, - ) { - return Some(PhpType::Void); - } - - if let Some(wrapper) = callback_env::emit_callable_array_descriptor_env_after_saved_array( - &args[1], - array_arg_reg, - call_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - data, - ) { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a callable-array descriptor comparator - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Void); - } - - if runtime_callable_array_callback::emit_after_saved_array( - &args[1], - array_arg_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a runtime callable-array descriptor - }, - ) { - return Some(PhpType::Void); - } - - if callback_env::expr_call_needs_descriptor_callback_env(&args[1], ctx) - && callback_env::descriptor_callback_env_supported(&args[1]) - { - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // preserve the selected callable descriptor while recovering the sort array - abi::emit_pop_reg(emitter, array_arg_reg); // recover the array pointer before building the descriptor environment - emitter.instruction(&format!("mov {}, {}", result_reg, call_reg)); // restore the selected callable descriptor as the current result - let wrapper = callback_env::emit_descriptor_callback_env_from_result( - &args[1], - array_arg_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - ) - .expect("descriptor callback env support checked before emitting callback"); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a descriptor comparator wrapper - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Void); - } - - let captures = - callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); - - // -- call runtime: callback_addr + array_ptr -- - if !captures.is_empty() { - abi::emit_pop_reg(emitter, result_reg); // recover the array pointer before building the comparator capture environment - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![elem_ty.clone(), elem_ty], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a captured comparator wrapper - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Void); - } - abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the resolved comparator address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/uksort.rs b/src/codegen/builtins/arrays/uksort.rs deleted file mode 100644 index e585390c57..0000000000 --- a/src/codegen/builtins/arrays/uksort.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Purpose: -//! Emits PHP `uksort` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::abi; -use super::callback_env; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::runtime_callable_array_callback; -use super::runtime_string_callback; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the PHP `uksort` builtin call, which sorts an array by keys using a user-provided callback. -/// -/// # Arguments -/// - `_name`: Unused name for dispatch (actual function is determined by catalog). -/// - `args`:[0] the array to sort, [1] the user callback callable. -/// - `emitter`: Assembly emitter for the current target. -/// - `ctx`: Codegen context (carries function metadata, variable layout). -/// - `data`: Data section for literals and runtime symbols. -/// -/// # Returns -/// `Some(PhpType::Void)` since `uksort` has no return value. -/// -/// # Behavior -/// 1. Evaluates the array argument and extracts its element type. -/// 2. Ensures array is unique (COW) and marks it as mutating. -/// 3. Preserves the array pointer on the stack while resolving the callback address. -/// 4. If the callback has captures: builds a captured wrapper environment, loads the array -/// slot and wrapper address into argument registers, calls `__rt_usort`. -/// 5. If no captures: restores the array pointer to the second argument register, moves the -/// resolved callback address to the first argument register, sets env=0, calls `__rt_usort`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("uksort()"); - - // -- evaluate the array argument (first arg) -- - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let elem_ty = match &arr_ty { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Int, - }; - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - - // -- save array pointer -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the array pointer while the callback address is resolved for the target ABI - - // -- resolve callback function address -- - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - - if runtime_string_callback::emit_after_saved_array( - &args[1], - Some(&arr_ty), - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - array_arg_reg, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a runtime string descriptor comparator - }, - ) { - return Some(PhpType::Void); - } - - if let Some(wrapper) = callback_env::emit_callable_array_descriptor_env_after_saved_array( - &args[1], - array_arg_reg, - call_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - data, - ) { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a callable-array descriptor comparator - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Void); - } - - if runtime_callable_array_callback::emit_after_saved_array( - &args[1], - array_arg_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a runtime callable-array descriptor - }, - ) { - return Some(PhpType::Void); - } - - if callback_env::expr_call_needs_descriptor_callback_env(&args[1], ctx) - && callback_env::descriptor_callback_env_supported(&args[1]) - { - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // preserve the selected callable descriptor while recovering the sort array - abi::emit_pop_reg(emitter, array_arg_reg); // recover the array pointer before building the descriptor environment - emitter.instruction(&format!("mov {}, {}", result_reg, call_reg)); // restore the selected callable descriptor as the current result - let wrapper = callback_env::emit_descriptor_callback_env_from_result( - &args[1], - array_arg_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - ) - .expect("descriptor callback env support checked before emitting callback"); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a descriptor comparator wrapper - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Void); - } - - let captures = - callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); - - // -- call runtime: callback_addr + array_ptr -- - if !captures.is_empty() { - abi::emit_pop_reg(emitter, result_reg); // recover the array pointer before building the comparator capture environment - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![elem_ty.clone(), elem_ty], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a captured comparator wrapper - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Void); - } - abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the resolved comparator address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/arrays/usort.rs b/src/codegen/builtins/arrays/usort.rs deleted file mode 100644 index a8a9f83f8b..0000000000 --- a/src/codegen/builtins/arrays/usort.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Purpose: -//! Emits PHP `usort` builtin calls that invoke user-provided callbacks. -//! Owns callback argument materialization, result shape selection, and runtime helper calls. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::emit()`. -//! -//! Key details: -//! - Callback lowering must preserve PHP source evaluation order, captures, and callable return ownership. - -use crate::codegen::abi; -use super::callback_env; -use super::ensure_unique_arg::emit_ensure_unique_arg; -use super::runtime_callable_array_callback; -use super::runtime_string_callback; -use super::store_mutating_arg::emit_store_mutating_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `usort($array, $callback)` builtin call. -/// -/// Evaluates the array argument, preserves the array pointer across callback resolution, -/// materializes the comparator callback address, and calls `__rt_usort` with either a -/// captured comparator wrapper (when the callback captures variables) or the raw callback -/// address and a null environment pointer. -/// -/// Returns `PhpType::Void` on success. -/// -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("usort()"); - - // -- evaluate the array argument (first arg) -- - let arr_ty = emit_expr(&args[0], emitter, ctx, data); - let elem_ty = match &arr_ty { - PhpType::Array(elem_ty) => elem_ty.codegen_repr(), - _ => PhpType::Int, - }; - emit_ensure_unique_arg(emitter, &arr_ty); - emit_store_mutating_arg(emitter, ctx, &args[0]); - - // -- save array pointer -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the array pointer while the callback address is resolved for the target ABI - - // -- resolve callback function address -- - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - let callback_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 1); - let env_arg_reg = abi::int_arg_reg_name(emitter.target, 2); - - if runtime_string_callback::emit_after_saved_array( - &args[1], - Some(&arr_ty), - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - array_arg_reg, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a runtime string descriptor comparator - }, - ) { - return Some(PhpType::Void); - } - - if let Some(wrapper) = callback_env::emit_callable_array_descriptor_env_after_saved_array( - &args[1], - array_arg_reg, - call_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - data, - ) { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a callable-array descriptor comparator - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Void); - } - - if runtime_callable_array_callback::emit_after_saved_array( - &args[1], - array_arg_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - data, - |wrapper, emitter, _ctx, _data| { - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a runtime callable-array descriptor - }, - ) { - return Some(PhpType::Void); - } - - if callback_env::expr_call_needs_descriptor_callback_env(&args[1], ctx) - && callback_env::descriptor_callback_env_supported(&args[1]) - { - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, result_reg)); // preserve the selected callable descriptor while recovering the sort array - abi::emit_pop_reg(emitter, array_arg_reg); // recover the array pointer before building the descriptor environment - emitter.instruction(&format!("mov {}, {}", result_reg, call_reg)); // restore the selected callable descriptor as the current result - let wrapper = callback_env::emit_descriptor_callback_env_from_result( - &args[1], - array_arg_reg, - vec![elem_ty.clone(), elem_ty.clone()], - PhpType::Int, - emitter, - ctx, - ) - .expect("descriptor callback env support checked before emitting callback"); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a descriptor comparator wrapper - callback_env::release_descriptor_callback_env(&wrapper, emitter); - return Some(PhpType::Void); - } - - let captures = - callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data); - - // -- call runtime: callback_addr + array_ptr -- - if !captures.is_empty() { - abi::emit_pop_reg(emitter, result_reg); // recover the array pointer before building the comparator capture environment - let wrapper = callback_env::emit_captured_callback_env( - call_reg, - result_reg, - &captures, - vec![elem_ty.clone(), elem_ty], - emitter, - ctx, - ); - callback_env::load_env_slot_to_reg(emitter, array_arg_reg, wrapper.array_slot_offset); - abi::emit_symbol_address(emitter, callback_arg_reg, &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, env_arg_reg); - abi::emit_call_label(emitter, "__rt_usort"); // call the sort runtime helper with a captured comparator wrapper - abi::emit_release_temporary_stack(emitter, wrapper.env_bytes); - return Some(PhpType::Void); - } - abi::emit_pop_reg(emitter, array_arg_reg); // restore the array pointer into the second runtime argument register - emitter.instruction(&format!("mov {}, {}", callback_arg_reg, call_reg)); // move the resolved comparator address into the first runtime argument register - abi::emit_load_int_immediate(emitter, env_arg_reg, 0); - abi::emit_call_label(emitter, "__rt_usort"); // call the target-aware runtime helper that sorts the indexed array using the comparator callback - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/callable_lookup.rs b/src/codegen/builtins/callable_lookup.rs deleted file mode 100644 index ba98c14084..0000000000 --- a/src/codegen/builtins/callable_lookup.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Purpose: -//! Resolves string-literal function names used by callable/introspection builtins. -//! Shares PHP case-insensitive lookup between string-callback and introspection builtins. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::function_exists` -//! - `crate::codegen::builtins::types::is_callable` -//! -//! Key details: -//! - Include variants, externs, builtins, and user functions stay distinguishable so callers can choose the right lowering path. - -use crate::codegen::context::Context; -use crate::names::php_symbol_key; -use crate::types::checker::builtins::canonical_builtin_function_name; - -/// Discriminates include variants, externs, builtins, and user functions -/// so callers can pick the correct codegen lowering path. -/// -/// Variants carry the canonical (original-case) function name as reported by PHP: -/// - `Builtin`: a PHP builtin, lowercased by `canonical_builtin_function_name` -/// - `Extern`: an `extern` declaration -/// - `UserFunction`: a user-defined function from any included file -/// - `IncludeVariant`: a function variant discovered inside an include context -pub(crate) enum FunctionLookup { - Builtin(String), - Extern(String), - UserFunction(String), - IncludeVariant(String), -} - -/// Resolves a string-literal function name to a `FunctionLookup` variant. -/// -/// Checks order: include variants → externs → user functions → builtins. -/// The first match wins; builtin lookup is case-insensitive via `canonical_builtin_function_name`. -/// Leading global namespace separators are ignored for PHP string-introspection names. -/// Returns `None` if the name does not resolve to any known variant. -pub(crate) fn lookup_function(ctx: &Context, name: &str) -> Option { - let name = name.trim_start_matches('\\'); - if let Some(name) = lookup_folded(ctx.function_variant_groups.iter(), name) { - return Some(FunctionLookup::IncludeVariant(name)); - } - if let Some(name) = lookup_folded(ctx.extern_functions.keys(), name) { - return Some(FunctionLookup::Extern(name)); - } - if let Some(name) = lookup_folded(ctx.functions.keys(), name) { - return Some(FunctionLookup::UserFunction(name)); - } - canonical_builtin_function_name(name).map(FunctionLookup::Builtin) -} - -/// Case-insensitive lookup over an iterable of names using PHP's symbol key. -/// -/// `names` is any iterable of `String` candidates. `name` is the lookup key. -/// Both are compared via `php_symbol_key` (lowercased, unescaped) to emulate PHP's -/// case-insensitive function resolution. Returns the canonical (original-case) name -/// from `names` on the first match, or `None` if no candidate matches. -fn lookup_folded<'a, I>(names: I, name: &str) -> Option -where - I: IntoIterator, -{ - let key = php_symbol_key(name); - names - .into_iter() - .find(|candidate| php_symbol_key(candidate) == key) - .cloned() -} diff --git a/src/codegen/builtins/io/basename.rs b/src/codegen/builtins/io/basename.rs deleted file mode 100644 index 9e992aaf49..0000000000 --- a/src/codegen/builtins/io/basename.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Purpose: -//! Emits PHP `basename` path-oriented builtin calls. -//! Marshals path strings into runtime helpers that normalize, split, or enumerate filesystem paths. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returned strings and arrays must use runtime allocation/layout compatible with PHP false-on-failure behavior. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `basename(path, suffix?)` builtin. -/// -/// Handles both single-argument and two-argument forms. The path string is -/// passed in the primary string-argument register pair (x1/x2 on AArch64, rax/rdx -/// on x86_64). If a suffix is provided, it is evaluated after the path and placed -/// in the secondary string-argument pair (x3/x4 or rdi/rsi) before restoring the -/// path pair. When no suffix is supplied, null registers signal "no suffix" to -/// the runtime helper. -/// -/// Calls `__rt_basename` and returns `Some(PhpType::Str)` on success, or -/// propagates a runtime error on failure. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("basename()"); - emit_expr(&args[0], emitter, ctx, data); - if args.len() >= 2 { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the path ptr/len while the suffix expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the suffix pointer into the secondary runtime string-argument pair - emitter.instruction("mov x4, x2"); // move the suffix length into the secondary runtime string-argument pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the path ptr/len after evaluating the suffix expression - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the path ptr/len while the suffix expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the suffix pointer into the x86_64 secondary runtime string-argument slot - emitter.instruction("mov rsi, rdx"); // move the suffix length into the x86_64 secondary runtime string-argument slot - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the path ptr/len after evaluating the suffix expression - } - } - } else { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, #0"); // no suffix supplied: pointer = 0 - emitter.instruction("mov x4, #0"); // no suffix supplied: length = 0 (runtime branches on this) - } - Arch::X86_64 => { - emitter.instruction("xor edi, edi"); // no suffix supplied: pointer = 0 - emitter.instruction("xor esi, esi"); // no suffix supplied: length = 0 (runtime branches on this) - } - } - } - abi::emit_call_label(emitter, "__rt_basename"); // call the target-aware runtime helper that returns the trailing name component - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/chdir.rs b/src/codegen/builtins/io/chdir.rs deleted file mode 100644 index 8dd8cedbc6..0000000000 --- a/src/codegen/builtins/io/chdir.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Purpose: -//! Emits PHP `chdir` filesystem mutation builtin calls. -//! Passes path and mode/owner arguments to runtime helpers that perform observable OS operations. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the PHP `chdir()` builtin, which changes the current working directory. -/// -/// ## Inputs -/// - `name`: unused (placeholder for dispatcher compatibility). -/// - `args[0]`: the directory path expression; emitted before the runtime call. -/// - `emitter`, `ctx`, `data`: standard codegen state for expression emission and data section. -/// -/// ## Outputs -/// Always returns `Some(PhpType::Bool)` — PHP `chdir()` returns a boolean indicating success. -/// -/// ## Runtime behavior -/// Calls the target-aware `__rt_chdir` runtime helper, which performs an observable OS -/// filesystem mutation. The path argument is evaluated first (with observable side effects -/// in source order), then the runtime helper is invoked. The helper returns 1 (true) on -/// success or 0 (false) on failure, mirroring PHP semantics. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("chdir()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_chdir"); // call the target-aware runtime helper that changes the current working directory - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/chgrp.rs b/src/codegen/builtins/io/chgrp.rs deleted file mode 100644 index 3145ce0e4d..0000000000 --- a/src/codegen/builtins/io/chgrp.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Purpose: -//! Emits PHP `chgrp` filesystem mutation builtin calls. -//! Passes path and mode/owner arguments to runtime helpers that perform observable OS operations. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::path_op_wrapper::{ - emit_owner_group_name_wrapper_dispatch, emit_owner_group_wrapper_dispatch, STREAM_META_GROUP, - STREAM_META_GROUP_NAME, -}; - -/// Emits the `chgrp($path, $group)` builtin call. -/// -/// `args[0]` is the path (string expression) and `args[1]` is the group principal, -/// which may be a string (group name) or integer (GID). -/// -/// On a registered `scheme://` path the call dispatches to the wrapper's -/// `stream_metadata($path, $option, $value)` (vtable slot 14): an integer gid uses -/// `STREAM_META_GROUP` with the gid boxed as `mixed`, a string name uses -/// `STREAM_META_GROUP_NAME` with the name boxed as `mixed`. A non-wrapper path uses -/// libc `__rt_chown(path, -1, gid)` (integer) or `__rt_chgrp_group(path, name)` -/// (string), leaving the owner unchanged. -/// -/// Returns `PhpType::Bool` (true = success, false = failure from runtime). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("chgrp()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve path ptr/len while the group is evaluated - let principal_ty = emit_expr(&args[1], emitter, ctx, data); - if principal_ty == PhpType::Str { - emit_owner_group_name_wrapper_dispatch( - emitter, - ctx, - STREAM_META_GROUP_NAME, - "__rt_chgrp_group", - ); // wrapper stream_metadata(GROUP_NAME) or libc chgrp_group - } else { - emit_owner_group_wrapper_dispatch(emitter, ctx, STREAM_META_GROUP); // wrapper stream_metadata(GROUP) or libc chown - } - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve path ptr/len while the group is evaluated - let principal_ty = emit_expr(&args[1], emitter, ctx, data); - if principal_ty == PhpType::Str { - emit_owner_group_name_wrapper_dispatch( - emitter, - ctx, - STREAM_META_GROUP_NAME, - "__rt_chgrp_group", - ); // wrapper stream_metadata(GROUP_NAME) or libc chgrp_group - } else { - emit_owner_group_wrapper_dispatch(emitter, ctx, STREAM_META_GROUP); // wrapper stream_metadata(GROUP) or libc chown - } - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/chmod.rs b/src/codegen/builtins/io/chmod.rs deleted file mode 100644 index 9e19d826da..0000000000 --- a/src/codegen/builtins/io/chmod.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Purpose: -//! Emits PHP `chmod` filesystem mutation builtin calls. -//! Routes a `scheme://` path matching a registered userspace wrapper to the -//! wrapper's `stream_metadata($path, STREAM_META_ACCESS, $mode)`; all other -//! paths use the libc `__rt_chmod`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. -//! - The wrapper split mirrors `readfile()`: a `__rt_path_is_wrapper` probe picks -//! the wrapper branch (`__rt_user_wrapper_path_op` with the `stream_metadata` -//! vtable slot 14, option `STREAM_META_ACCESS` = 6, value = `$mode`) over the -//! libc filesystem branch. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::path_op_wrapper::emit_box_int_as_mixed; - -/// `stream_metadata` vtable slot index in the per-class user-wrapper vtable. -const STREAM_METADATA_SLOT: usize = 14; - -/// PHP `STREAM_META_ACCESS` option value (`chmod`-style metadata change). -const STREAM_META_ACCESS: usize = 6; - -/// Emits the `chmod` builtin call. -/// -/// Evaluates the path argument, spills it, evaluates the mode argument, spills -/// it, then probes the path scheme with `__rt_path_is_wrapper`. The wrapper -/// branch calls `__rt_user_wrapper_path_op(path, len, slot=14, -/// option=STREAM_META_ACCESS, value=mode)` which invokes the wrapper's -/// `stream_metadata($path, $option, $value)`; the libc branch calls `__rt_chmod` -/// with the path in `x1`/`x2` (`rax`/`rdx`) and the mode in `x3` (`rdi`). -/// -/// Arguments: -/// - `args[0]`: path (string) -/// - `args[1]`: mode (integer octal, e.g. 0o755) -/// -/// Returns: `PhpType::Bool` (true on success, false on failure, matching PHP semantics) -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("chmod()"); - emit_expr(&args[0], emitter, ctx, data); - let wrapper = ctx.next_label("chmod_wrapper"); - let after = ctx.next_label("chmod_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("sub sp, sp, #32"); // scratch: [sp,#0] path ptr, [sp,#8] path len, [sp,#16] mode - emitter.instruction("str x1, [sp, #0]"); // save the path pointer - emitter.instruction("str x2, [sp, #8]"); // save the path length - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("str x0, [sp, #16]"); // save the mode value - emitter.instruction("ldr x0, [sp, #0]"); // path_is_wrapper arg0 = path ptr - emitter.instruction("ldr x1, [sp, #8]"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // x0 = 1 when the scheme matches a registered wrapper - emitter.instruction(&format!("cbnz x0, {}", wrapper)); // registered wrapper scheme → wrapper stream_metadata - emitter.instruction("ldr x1, [sp, #0]"); // libc path ptr → x1 - emitter.instruction("ldr x2, [sp, #8]"); // libc path len → x2 - emitter.instruction("ldr x3, [sp, #16]"); // libc mode → x3 - emitter.instruction("add sp, sp, #32"); // release the scratch frame before the call (libc helper runs at the original sp) - abi::emit_call_label(emitter, "__rt_chmod"); // normal path: libc chmod(path, mode) - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("ldr x0, [sp, #16]"); // reload the mode integer - emit_box_int_as_mixed(emitter); // box $value as mixed → x0 = owned Mixed(int) - emitter.instruction("str x0, [sp, #16]"); // stash the boxed value pointer (mode slot reused) - emitter.instruction("ldr x0, [sp, #0]"); // wrapper path ptr → x0 - emitter.instruction("ldr x1, [sp, #8]"); // wrapper path len → x1 - emitter.instruction(&format!("mov x2, #{}", STREAM_METADATA_SLOT)); // stream_metadata vtable slot - emitter.instruction(&format!("mov x3, #{}", STREAM_META_ACCESS)); // option = STREAM_META_ACCESS - emitter.instruction("ldr x4, [sp, #16]"); // value = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's stream_metadata method - emitter.instruction("str x0, [sp, #0]"); // stash the bool result across the value release - emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed value pointer - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the boxed $value (caller owns; the method borrowed it) - emitter.instruction("ldr x0, [sp, #0]"); // restore the bool result - emitter.instruction("add sp, sp, #32"); // release the scratch frame - emitter.label(&after); - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 32"); // scratch: [rsp+0] path ptr, [rsp+8] path len, [rsp+16] mode - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the path pointer - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the path length - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // save the mode value - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // rax = 1 when the scheme matches a registered wrapper - emitter.instruction("test rax, rax"); // matched a registered wrapper scheme? - emitter.instruction(&format!("jnz {}", wrapper)); // registered wrapper scheme → wrapper stream_metadata - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // libc path ptr → rax - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // libc path len → rdx - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // libc mode → rdi (secondary integer arg) - emitter.instruction("add rsp, 32"); // release the scratch frame before the call (libc helper runs at the original rsp) - abi::emit_call_label(emitter, "__rt_chmod"); // normal path: libc chmod(path, mode) - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the mode integer - emit_box_int_as_mixed(emitter); // box $value as mixed → rax = owned Mixed(int) - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // stash the boxed value pointer (mode slot reused) - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // wrapper path ptr → rdi - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // wrapper path len → rsi - emitter.instruction(&format!("mov rdx, {}", STREAM_METADATA_SLOT)); // stream_metadata vtable slot - emitter.instruction(&format!("mov rcx, {}", STREAM_META_ACCESS)); // option = STREAM_META_ACCESS - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // value = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's stream_metadata method - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // stash the bool result across the value release - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the boxed value pointer - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the boxed $value (caller owns; the method borrowed it) - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore the bool result - emitter.instruction("add rsp, 32"); // release the scratch frame - emitter.label(&after); - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/chown.rs b/src/codegen/builtins/io/chown.rs deleted file mode 100644 index 78435cfb9e..0000000000 --- a/src/codegen/builtins/io/chown.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Purpose: -//! Emits PHP `chown` filesystem mutation builtin calls. -//! Passes path and mode/owner arguments to runtime helpers that perform observable OS operations. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::path_op_wrapper::{ - emit_owner_group_name_wrapper_dispatch, emit_owner_group_wrapper_dispatch, STREAM_META_OWNER, - STREAM_META_OWNER_NAME, -}; - -/// Emits the `chown($path, $owner)` builtin call. -/// -/// `args[0]` is the path (string expression) and `args[1]` is the owner principal, -/// which may be a string (user name) or integer (UID). -/// -/// On a registered `scheme://` path the call dispatches to the wrapper's -/// `stream_metadata($path, $option, $value)` (vtable slot 14): an integer uid uses -/// `STREAM_META_OWNER` with the uid boxed as `mixed`, a string name uses -/// `STREAM_META_OWNER_NAME` with the name boxed as `mixed`. A non-wrapper path uses -/// libc `__rt_chown(path, uid, -1)` (integer) or `__rt_chown_user(path, name)` -/// (string), leaving the group unchanged. -/// -/// Returns `PhpType::Bool` (true = success, false = failure from runtime). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("chown()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve path ptr/len while the owner is evaluated - let principal_ty = emit_expr(&args[1], emitter, ctx, data); - if principal_ty == PhpType::Str { - emit_owner_group_name_wrapper_dispatch( - emitter, - ctx, - STREAM_META_OWNER_NAME, - "__rt_chown_user", - ); // wrapper stream_metadata(OWNER_NAME) or libc chown_user - } else { - emit_owner_group_wrapper_dispatch(emitter, ctx, STREAM_META_OWNER); // wrapper stream_metadata(OWNER) or libc chown - } - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve path ptr/len while the owner is evaluated - let principal_ty = emit_expr(&args[1], emitter, ctx, data); - if principal_ty == PhpType::Str { - emit_owner_group_name_wrapper_dispatch( - emitter, - ctx, - STREAM_META_OWNER_NAME, - "__rt_chown_user", - ); // wrapper stream_metadata(OWNER_NAME) or libc chown_user - } else { - emit_owner_group_wrapper_dispatch(emitter, ctx, STREAM_META_OWNER); // wrapper stream_metadata(OWNER) or libc chown - } - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/clearstatcache.rs b/src/codegen/builtins/io/clearstatcache.rs deleted file mode 100644 index 5754a88631..0000000000 --- a/src/codegen/builtins/io/clearstatcache.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `clearstatcache` calls for filesystem metadata cache state. -//! Provides the codegen hook even when runtime cache behavior is minimal. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The call is effectful from PHP's perspective and should remain ordered with stat-family operations. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `clearstatcache` builtin call. -/// -/// In elefhc this is a no-op: there is no filesystem metadata cache to clear. -/// Arguments are still evaluated (for side effects) and discarded. -/// -/// # Arguments -/// * `_name` — Unused builtin name (passed by the dispatcher). -/// * `args` — Any supplied arguments; each is emitted to consume its side effects. -/// * `emitter` — Assembly emitter. -/// * `ctx` — Codegen context (types, locals, etc.). -/// * `data` — Data section for read-only constants. -/// -/// # Returns -/// Always `PhpType::Void`. The call itself produces no value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("clearstatcache() — no-op (elephc has no stat cache)"); - for arg in args { - emit_expr(arg, emitter, ctx, data); - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/io/closedir.rs b/src/codegen/builtins/io/closedir.rs deleted file mode 100644 index f98a7f236e..0000000000 --- a/src/codegen/builtins/io/closedir.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Purpose: -//! Emits PHP `closedir` calls. -//! Closes a directory handle opened by `opendir()`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The descriptor is unboxed from the stream resource and handed to the -//! `__rt_closedir` runtime helper, which calls libc `closedir`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `closedir()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("closedir()"); - emit_stream_fd_arg("closedir", &args[0], emitter, ctx, data); - // -- dispatch: synthetic wrapper fd -> dir_closedir, else libc closedir -- - let wrapper = ctx.next_label("closedir_wrapper"); - let after = ctx.next_label("closedir_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper)); // dispatch into dir_closedir - abi::emit_call_label(emitter, "__rt_closedir"); // libc closedir - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - abi::emit_call_label(emitter, "__rt_user_wrapper_dir_closedir"); // wrapper dir_closedir + free handle - emitter.label(&after); - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper)); // dispatch into dir_closedir - emitter.instruction("mov rdi, rax"); // descriptor into the runtime-helper argument register - abi::emit_call_label(emitter, "__rt_closedir"); // libc closedir - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rdi, rax"); // descriptor into the runtime-helper argument register - abi::emit_call_label(emitter, "__rt_user_wrapper_dir_closedir"); // wrapper dir_closedir + free handle - emitter.label(&after); - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/io/compress_bzip2_stream.rs b/src/codegen/builtins/io/compress_bzip2_stream.rs deleted file mode 100644 index bf00a58ffb..0000000000 --- a/src/codegen/builtins/io/compress_bzip2_stream.rs +++ /dev/null @@ -1,333 +0,0 @@ -//! Purpose: -//! Lowers `fopen("compress.bzip2://path", ...)` calls. Opens the underlying -//! file read-only, slurps the bzip2-compressed payload, decompresses it via -//! libbz2's one-shot `BZ2_bzBuffToBuffDecompress`, writes the plain bytes -//! to an anonymous temp file, then `dup2`s that fd onto the original -//! descriptor so subsequent fread/fseek/feof see the decompressed bytes -//! transparently. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `compress.bzip2://`. -//! -//! Key details: -//! - The URL must be a string literal; the prefix is stripped at compile -//! time and the underlying path is opened with mode "r". -//! - Slurp cap = 64 KiB (`_stream_filter_buf`), output cap = 256x the -//! compressed size (min 64 KiB) — matches the `zlib.inflate` budget. -//! - libbz2 is referenced only from this builtin's USER asm, so programs -//! that don't use `compress.bzip2://` neither link against nor reference -//! libbz2. The checker emits `require_builtin_library("bz2")` for -//! programs that do. -//! - On decompress failure (non-zero return) we skip the dup2 and let the -//! source fd stay positioned at end-of-file — fread returns empty bytes, -//! matching how the `zlib.inflate` filter degrades on broken input. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -const FILTER_BUF_SIZE: i64 = 65536; - -/// Emits a `fopen("compress.bzip2://...", ...)` call. The path is known to -/// be a string literal beginning with `compress.bzip2://`. -pub fn emit( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() compress.bzip2:// stream"); - let underlying = match &args[0].kind { - ExprKind::StringLiteral(path) => path.strip_prefix("compress.bzip2://").map(str::to_string), - _ => None, - }; - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - let underlying = match underlying { - Some(p) if !p.is_empty() => p, - _ => { - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // negative fd sentinel for PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // negative fd sentinel for PHP false - } - super::fopen::box_fopen_result(emitter, ctx); - return Some(PhpType::Mixed); - } - }; - - let (path_sym, path_len) = data.add_string(underlying.as_bytes()); - let (mode_sym, mode_len) = data.add_string(b"r"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", &path_sym); - emitter.instruction(&format!("mov x2, #{}", path_len)); // path length - abi::emit_symbol_address(emitter, "x3", &mode_sym); - emitter.instruction(&format!("mov x4, #{}", mode_len)); // mode length - abi::emit_call_label(emitter, "__rt_fopen"); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rax", &path_sym); - emitter.instruction(&format!("mov rdx, {}", path_len)); // path length - abi::emit_symbol_address(emitter, "rdi", &mode_sym); - emitter.instruction(&format!("mov rsi, {}", mode_len)); // mode length - abi::emit_call_label(emitter, "__rt_fopen"); - } - } - - let false_label = ctx.next_label("cbz2_false"); - let done_label = ctx.next_label("cbz2_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // negative fd = source open failed - emitter.instruction(&format!("b.lt {}", false_label)); // box false when the source open failed - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // negative fd = source open failed - emitter.instruction(&format!("js {}", false_label)); // sign bit set = negative fd - } - } - - match emitter.target.arch { - Arch::AArch64 => emit_arm64(emitter, |prefix| ctx.next_label(prefix)), - Arch::X86_64 => emit_x86_64(emitter, |prefix| ctx.next_label(prefix)), - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", done_label)), // skip false boxing after bzip2 setup - Arch::X86_64 => emitter.instruction(&format!("jmp {}", done_label)), // skip false boxing after bzip2 setup - } - emitter.label(&false_label); - super::fopen::box_fopen_result(emitter, ctx); - emitter.label(&done_label); - Some(PhpType::Mixed) -} - -/// ARM64: 96-byte stack frame. -/// Layout: -/// [sp, 0..8) source fd -/// [sp, 8..16) slurp offset / compressed length -/// [sp, 16..24) decompressed buffer pointer -/// [sp, 24..32) decompressed length (after BZ2 call) -/// [sp, 32..40) temp fd -/// [sp, 40..48) write offset -/// [sp, 48..56) destLen u32 spill (in: capacity, out: bytes written) -/// [sp, 56..64) padding -/// [sp, 64..72) saved x29 -/// [sp, 72..80) saved x30 -pub(super) fn emit_arm64(emitter: &mut Emitter, mut next_label: F) -where - F: FnMut(&str) -> String, -{ - let slurp = next_label("bz2_slurp"); - let slurp_done = next_label("bz2_slurped"); - let write = next_label("bz2_write"); - let write_done = next_label("bz2_written"); - let decompress_fail = next_label("bz2_decompress_fail"); - let common_done = next_label("bz2_done_arm"); - - emitter.instruction("sub sp, sp, #96"); // reserve the bzip2 scratch frame - emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #64"); // establish the helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save source fd - - // Slurp every compressed byte from the descriptor into _stream_filter_buf. - emitter.instruction("str xzr, [sp, #8]"); // slurp offset = 0 - emitter.label(&slurp); - emitter.instruction("ldr x0, [sp, #0]"); // fd to read from - abi::emit_symbol_address(emitter, "x1", "_stream_filter_buf"); - emitter.instruction("ldr x9, [sp, #8]"); // current compressed-byte count - emitter.instruction("add x1, x1, x9"); // write ptr = buf + offset - emitter.instruction(&format!("mov x2, #{}", FILTER_BUF_SIZE)); // total slurp buffer capacity - emitter.instruction("sub x2, x2, x9"); // remaining capacity - emitter.syscall(3); // read - emitter.instruction("cmp x0, #0"); // did read return EOF or an error? - emitter.instruction(&format!("b.le {}", slurp_done)); // EOF or error - emitter.instruction("ldr x9, [sp, #8]"); // reload compressed-byte count - emitter.instruction("add x9, x9, x0"); // include the bytes just read - emitter.instruction("str x9, [sp, #8]"); // persist the compressed-byte count - emitter.instruction(&format!("mov x10, #{}", FILTER_BUF_SIZE)); // slurp buffer capacity - emitter.instruction("cmp x9, x10"); // is there still room to read more? - emitter.instruction(&format!("b.lt {}", slurp)); // continue slurping until buffer is full or EOF - emitter.label(&slurp_done); - - // Size + allocate output buffer (256x input, min 64 KiB). - emitter.instruction("ldr x9, [sp, #8]"); // compressed input length - emitter.instruction("lsl x9, x9, #8"); // 256x compressed - emitter.instruction(&format!("mov x10, #{}", FILTER_BUF_SIZE)); // minimum output capacity - emitter.instruction("cmp x9, x10"); // compare computed capacity with minimum - emitter.instruction("csel x9, x9, x10, gt"); // max(256x, 64KiB) - emitter.instruction("str w9, [sp, #48]"); // destLen = capacity (u32) - emitter.instruction("mov x0, x9"); // allocation size for decompressed output - emitter.instruction("bl __rt_heap_alloc"); // allocate output buffer - emitter.instruction("mov x9, #1"); // heap kind 1 = persisted string - emitter.instruction("str x9, [x0, #-8]"); // stamp the heap kind for the output buffer - emitter.instruction("str x0, [sp, #16]"); // save output buffer ptr - - // BZ2_bzBuffToBuffDecompress(dest, &destLen, source, sourceLen, 0, 0). - emitter.instruction("ldr x0, [sp, #16]"); // dest - emitter.instruction("add x1, sp, #48"); // &destLen - abi::emit_symbol_address(emitter, "x2", "_stream_filter_buf"); // source - emitter.instruction("ldr x3, [sp, #8]"); // sourceLen (passed as w3 below) - emitter.instruction("mov w4, #0"); // small = 0 - emitter.instruction("mov w5, #0"); // verbosity = 0 - emitter.bl_c("BZ2_bzBuffToBuffDecompress"); // libbz2 one-shot decompress - emitter.instruction("cmp w0, #0"); // did libbz2 report an error? - emitter.instruction(&format!("b.ne {}", decompress_fail)); // non-zero = error → skip dup2 - - emitter.instruction("ldr w9, [sp, #48]"); // destLen now holds bytes written - emitter.instruction("str x9, [sp, #24]"); // save decompressed length - - // Back the descriptor with an anonymous temp file of the plain bytes. - emitter.instruction("bl __rt_tmpfile"); // x0 = temp fd - emitter.instruction("str x0, [sp, #32]"); // save temp fd - - // Write loop. - emitter.instruction("str xzr, [sp, #40]"); // write offset = 0 - emitter.label(&write); - emitter.instruction("ldr x10, [sp, #24]"); // total decompressed length - emitter.instruction("ldr x9, [sp, #40]"); // write offset - emitter.instruction("cmp x9, x10"); // has every decompressed byte been written? - emitter.instruction(&format!("b.ge {}", write_done)); // finish when output is fully written - emitter.instruction("ldr x0, [sp, #32]"); // temp fd - emitter.instruction("ldr x1, [sp, #16]"); // decompressed output buffer - emitter.instruction("add x1, x1, x9"); // src = buf + offset - emitter.instruction("sub x2, x10, x9"); // remaining bytes - emitter.syscall(4); // write - emitter.instruction("cmp x0, #0"); // did write make progress? - emitter.instruction(&format!("b.le {}", write_done)); // bail on error or short write - emitter.instruction("ldr x9, [sp, #40]"); // reload write offset - emitter.instruction("add x9, x9, x0"); // advance by bytes written - emitter.instruction("str x9, [sp, #40]"); // persist write offset - emitter.instruction(&format!("b {}", write)); // continue writing decompressed bytes - emitter.label(&write_done); - - // lseek(temp_fd, 0, SEEK_SET) — rewind so reads start at byte 0. - emitter.instruction("ldr x0, [sp, #32]"); // temp fd to rewind - emitter.instruction("mov x1, #0"); // offset - emitter.instruction("mov x2, #0"); // whence = SEEK_SET - emitter.syscall(199); // lseek - - // dup2(temp_fd, source_fd) so subsequent reads see decompressed bytes. - emitter.instruction("ldr x0, [sp, #32]"); // oldfd = temp fd - emitter.instruction("ldr x1, [sp, #0]"); // newfd = source fd - emitter.bl_c("dup2"); // libc dup2 - emitter.instruction("ldr x0, [sp, #32]"); // close temp fd - emitter.syscall(6); // close - - emitter.label(&decompress_fail); - emitter.label(&common_done); - emitter.instruction("ldr x0, [sp, #0]"); // return source fd - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #96"); // release the bzip2 scratch frame - emitter.instruction("mov x1, x0"); // resource payload = fd - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); -} - -/// x86_64: same shape as ARM64. Frame is rbp-relative (-88 bytes). -pub(super) fn emit_x86_64(emitter: &mut Emitter, mut next_label: F) -where - F: FnMut(&str) -> String, -{ - let slurp = next_label("bz2_slurp_x"); - let slurp_done = next_label("bz2_slurped_x"); - let write = next_label("bz2_write_x"); - let write_done = next_label("bz2_written_x"); - let decompress_fail = next_label("bz2_decompress_fail_x"); - let common_done = next_label("bz2_done_x"); - - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 88"); // reserve frame; 88≡8 mod 16 so rsp is 16-aligned at libc calls (push rbp made it 8) - emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save source fd - - // Slurp. - emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // slurp offset - emitter.label(&slurp); - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // fd - abi::emit_symbol_address(emitter, "rsi", "_stream_filter_buf"); // slurp buffer base - emitter.instruction("add rsi, QWORD PTR [rbp - 16]"); // ptr = buf + offset - emitter.instruction(&format!("mov rdx, {}", FILTER_BUF_SIZE)); // total slurp buffer capacity - emitter.instruction("sub rdx, QWORD PTR [rbp - 16]"); // remaining - emitter.instruction("call read"); // read - emitter.instruction("cmp rax, 0"); // did read return EOF or an error? - emitter.instruction(&format!("jle {}", slurp_done)); // stop slurping on EOF or error - emitter.instruction("add QWORD PTR [rbp - 16], rax"); // bump offset - emitter.instruction(&format!("cmp QWORD PTR [rbp - 16], {}", FILTER_BUF_SIZE)); // is there still room to read more? - emitter.instruction(&format!("jl {}", slurp)); // continue slurping until buffer is full or EOF - emitter.label(&slurp_done); - - // Size + allocate output buffer. - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // compressed len - emitter.instruction("shl rax, 8"); // 256x - emitter.instruction(&format!("mov rcx, {}", FILTER_BUF_SIZE)); // minimum output capacity - emitter.instruction("cmp rax, rcx"); // compare computed capacity with minimum - emitter.instruction("cmovl rax, rcx"); // max(256x, 64KiB) - emitter.instruction("mov DWORD PTR [rbp - 48], eax"); // destLen u32 = capacity - emitter.instruction("mov rdi, rax"); // allocation size for decompressed output - emitter.instruction("call __rt_heap_alloc"); // allocate output buffer - emitter.instruction("mov QWORD PTR [rax - 8], 1"); // heap kind = string - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save output buffer ptr - - // BZ2_bzBuffToBuffDecompress(dest, &destLen, source, sourceLen, 0, 0). - emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // dest - emitter.instruction("lea rsi, [rbp - 48]"); // &destLen - abi::emit_symbol_address(emitter, "rdx", "_stream_filter_buf"); // source - emitter.instruction("mov ecx, DWORD PTR [rbp - 16]"); // sourceLen u32 (compressed len) - emitter.instruction("xor r8d, r8d"); // small = 0 - emitter.instruction("xor r9d, r9d"); // verbosity = 0 - emitter.bl_c("BZ2_bzBuffToBuffDecompress"); // libbz2 one-shot decompress - emitter.instruction("test eax, eax"); // did libbz2 report an error? - emitter.instruction(&format!("jnz {}", decompress_fail)); // non-zero = error - - emitter.instruction("mov eax, DWORD PTR [rbp - 48]"); // destLen now holds decompressed length - emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save decompressed length - - // Temp file backing. - emitter.instruction("call __rt_tmpfile"); // rax = temp fd - emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save temp fd - - // Write loop. - emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // write offset - emitter.label(&write); - emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // total - emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // offset - emitter.instruction("cmp rax, rcx"); // has every decompressed byte been written? - emitter.instruction(&format!("jge {}", write_done)); // finish when output is fully written - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // temp fd - emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // decompressed output buffer - emitter.instruction("add rsi, rax"); // src = buf + offset - emitter.instruction("mov rdx, rcx"); // total decompressed length - emitter.instruction("sub rdx, rax"); // remaining bytes - emitter.instruction("call write"); // copy decompressed bytes into the temp fd - emitter.instruction("cmp rax, 0"); // did write make progress? - emitter.instruction(&format!("jle {}", write_done)); // bail on error or short write - emitter.instruction("add QWORD PTR [rbp - 56], rax"); // advance by bytes written - emitter.instruction(&format!("jmp {}", write)); // continue writing decompressed bytes - emitter.label(&write_done); - - // lseek + dup2 + close. - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // temp fd to rewind - emitter.instruction("xor esi, esi"); // offset = 0 - emitter.instruction("xor edx, edx"); // whence = SEEK_SET - emitter.instruction("call lseek"); // libc lseek - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // oldfd = temp fd - emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // newfd = source fd - emitter.instruction("call dup2"); // replace source fd with temp fd contents - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // temp fd to close after dup2 - emitter.instruction("call close"); // close the temporary descriptor - - emitter.label(&decompress_fail); - emitter.label(&common_done); - emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return source fd - emitter.instruction("add rsp, 88"); // release the 88-byte frame (matches the aligned sub rsp, 88) - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("mov rdi, rax"); // resource payload = fd - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); -} diff --git a/src/codegen/builtins/io/compress_zlib_stream.rs b/src/codegen/builtins/io/compress_zlib_stream.rs deleted file mode 100644 index 2a778a8a78..0000000000 --- a/src/codegen/builtins/io/compress_zlib_stream.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Purpose: -//! Lowers `fopen()` calls whose path is a `compress.zlib://` URL. -//! Opens the underlying file in read-only mode and immediately attaches the -//! `zlib.inflate` filter logic so subsequent reads see decompressed bytes. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `compress.zlib://`. -//! -//! Key details: -//! - The URL must be a string literal; the prefix is stripped at compile time -//! and the underlying path is opened with mode "r" through `__rt_fopen`. -//! - On `fopen` failure (`fd < 0`) the wrapper short-circuits to PHP `false` -//! without invoking the inflate logic, matching `compress.zlib://`'s behavior -//! when the underlying file is missing or unreadable. -//! - The inflate emitter ends with the filtered descriptor already re-boxed as -//! a resource Mixed cell, so the wrapper does not call `box_fopen_result` -//! again on the success path. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits a `fopen("compress.zlib://...", ...)` call. The path is known to be a -/// string literal beginning with `compress.zlib://`. -pub fn emit( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() compress.zlib:// stream"); - let underlying = match &args[0].kind { - ExprKind::StringLiteral(path) => path.strip_prefix("compress.zlib://").map(str::to_string), - _ => None, - }; - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - let underlying = match underlying { - Some(p) if !p.is_empty() => p, - _ => { - // Unparseable or empty path lowers to PHP false. - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // negative fd sentinel for PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // negative fd sentinel for PHP false - } - super::fopen::box_fopen_result(emitter, ctx); - return Some(PhpType::Mixed); - } - }; - - // Materialize the stripped path and "r" mode into the runtime's string-arg - // registers before calling __rt_fopen. - let (path_sym, path_len) = data.add_string(underlying.as_bytes()); - let (mode_sym, mode_len) = data.add_string(b"r"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", &path_sym); - emitter.instruction(&format!("mov x2, #{}", path_len)); // path length - abi::emit_symbol_address(emitter, "x3", &mode_sym); - emitter.instruction(&format!("mov x4, #{}", mode_len)); // mode length - abi::emit_call_label(emitter, "__rt_fopen"); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rax", &path_sym); - emitter.instruction(&format!("mov rdx, {}", path_len)); // path length - abi::emit_symbol_address(emitter, "rdi", &mode_sym); - emitter.instruction(&format!("mov rsi, {}", mode_len)); // mode length - abi::emit_call_label(emitter, "__rt_fopen"); - } - } - - // Branch on fopen failure: negative fd → box false, skip inflate. - let false_label = ctx.next_label("czlib_false"); - let done_label = ctx.next_label("czlib_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // negative fd = open failed - emitter.instruction(&format!("b.lt {}", false_label)); // box false when the source open failed - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // negative fd = open failed - emitter.instruction(&format!("js {}", false_label)); // sign bit set = negative fd - } - } - // Attach inflate; this returns x0/rax = Mixed-boxed resource. - match emitter.target.arch { - Arch::AArch64 => super::stream_filter_inflate::emit_arm64(emitter, |prefix| ctx.next_label(prefix)), - Arch::X86_64 => super::stream_filter_inflate::emit_x86_64(emitter, |prefix| ctx.next_label(prefix)), - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", done_label)), // skip false boxing after attaching inflate - Arch::X86_64 => emitter.instruction(&format!("jmp {}", done_label)), // skip false boxing after attaching inflate - } - emitter.label(&false_label); - super::fopen::box_fopen_result(emitter, ctx); // boxes false (fd < 0) - emitter.label(&done_label); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/copy.rs b/src/codegen/builtins/io/copy.rs deleted file mode 100644 index c02f8acd9d..0000000000 --- a/src/codegen/builtins/io/copy.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Emits PHP `copy` filesystem mutation builtin calls. -//! Passes path and mode/owner arguments to runtime helpers that perform observable OS operations. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the PHP `copy($source, $dest)` builtin call. -/// -/// Evaluates the source path expression (`args[0]`) and destination path expression -/// (`args[1]`), preserving PHP evaluation order (source first, destination second). -/// Saves the source string pointer/length pair across the destination evaluation, -/// then loads both into the appropriate ABI string slots before calling `__rt_copy`. -/// -/// # Arguments -/// - `args[0]`: source path (string) -/// - `args[1]`: destination path (string) -/// -/// # Returns -/// `PhpType::Bool` — PHP `copy()` returns `false` on failure, `true` on success. -/// -/// # ABI Details -/// - AArch64: source pointer/length in `x1`/`x2`, dest pointer/length moved to `x3`/`x4` -/// - X86_64: source pointer/length in `rax`/`rdx`, dest pointer/length moved to `rdi`/`rsi` -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("copy()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // save the source path pointer and length while the destination expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the destination path pointer into the third string-argument slot - emitter.instruction("mov x4, x2"); // move the destination path length into the fourth string-argument slot - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the source path pointer and length after evaluating the destination expression - abi::emit_call_label(emitter, "__rt_copy"); // call the target-aware runtime helper that copies the file-system path - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the source path pointer and length while the destination expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the destination path pointer into the third x86_64 string-argument slot - emitter.instruction("mov rsi, rdx"); // move the destination path length into the fourth x86_64 string-argument slot - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the source path pointer and length after evaluating the destination expression - abi::emit_call_label(emitter, "__rt_copy"); // call the target-aware runtime helper that copies the file-system path - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/data_stream.rs b/src/codegen/builtins/io/data_stream.rs deleted file mode 100644 index 660ff70d2d..0000000000 --- a/src/codegen/builtins/io/data_stream.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Purpose: -//! Lowers `fopen()` calls whose path is a `data://` URI (RFC 2397). -//! Decodes the payload at compile time and materializes it as a readable -//! stream descriptor through the `__rt_data_stream` runtime helper. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `data://`. -//! -//! Key details: -//! - The URI must be a string literal; the `;base64` payload is base64-decoded -//! and any other payload is percent-decoded, both entirely at compile time. -//! - An unparseable URI lowers to PHP `false`, matching a failed `fopen()`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits a `fopen("data://...", ...)` call. The path is known to be a -/// string literal beginning with `data://`. -pub fn emit( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() data:// stream"); - let decoded = match &args[0].kind { - ExprKind::StringLiteral(path) => decode_data_uri(path), - _ => None, - }; - // The mode and optional fopen args are evaluated for side effects; - // data:// streams are read-only regardless of the requested mode. - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - match decoded { - Some(bytes) => { - let (symbol, len) = data.add_string(&bytes); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x0", &symbol); - emitter.instruction(&format!("mov x1, #{}", len)); // decoded payload length - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &symbol); - emitter.instruction(&format!("mov rsi, {}", len)); // decoded payload length - } - } - abi::emit_call_label(emitter, "__rt_data_stream"); // build the readable data:// descriptor - } - None => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // unparseable data:// URI lowers to PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // unparseable data:// URI lowers to PHP false - }, - } - super::fopen::box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Decodes a `data://[][;base64],` URI into its raw bytes. -/// Returns `None` when the URI lacks the mandatory comma or carries an invalid -/// base64 payload. -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)) - } -} - -/// Decodes a base64 payload, tolerating embedded whitespace and stopping at the -/// first `=` padding character. Returns `None` on an invalid alphabet byte. -fn base64_decode(input: &str) -> Option> { - /// Converts one base64 byte into its six-bit value for data:// decoding. - fn sextet(c: u8) -> Option { - match c { - b'A'..=b'Z' => Some((c - b'A') as u32), - b'a'..=b'z' => Some((c - b'a') as u32 + 26), - b'0'..=b'9' => Some((c - b'0') as u32 + 52), - b'+' => Some(62), - b'/' => Some(63), - _ => None, - } - } - let mut out = Vec::new(); - let mut acc = 0u32; - let mut bits = 0u32; - for &c in input.as_bytes() { - if c == b'=' { - break; - } - if c.is_ascii_whitespace() { - continue; - } - acc = (acc << 6) | sextet(c)?; - bits += 6; - if bits >= 8 { - bits -= 8; - out.push((acc >> bits) as u8); - } - } - Some(out) -} - -/// Percent-decodes a `data://` payload: `%HH` escapes become their byte value -/// and `+` becomes a space, matching PHP's `data://` wrapper. -fn percent_decode(input: &str) -> Vec { - let bytes = input.as_bytes(); - let mut out = Vec::with_capacity(bytes.len()); - let mut i = 0; - while i < bytes.len() { - match bytes[i] { - b'%' if i + 2 < bytes.len() => { - let hi = (bytes[i + 1] as char).to_digit(16); - let lo = (bytes[i + 2] as char).to_digit(16); - match (hi, lo) { - (Some(hi), Some(lo)) => { - out.push((hi * 16 + lo) as u8); - i += 3; - } - _ => { - out.push(b'%'); - i += 1; - } - } - } - b'+' => { - out.push(b' '); - i += 1; - } - other => { - out.push(other); - i += 1; - } - } - } - out -} diff --git a/src/codegen/builtins/io/dirname.rs b/src/codegen/builtins/io/dirname.rs deleted file mode 100644 index 6ab3844a41..0000000000 --- a/src/codegen/builtins/io/dirname.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Purpose: -//! Emits PHP `dirname` path-oriented builtin calls. -//! Marshals path strings into runtime helpers that normalize, split, or enumerate filesystem paths. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returned strings and arrays must use runtime allocation/layout compatible with PHP false-on-failure behavior. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `dirname(path)` or `dirname(path, levels)` builtin call. -/// -/// For the single-argument form, calls `__rt_dirname` which strips the final path component. -/// For the two-argument form, calls `__rt_dirname_levels` which applies `dirname()` iteratively -/// `levels` times. -/// -/// # Arguments -/// * `args[0]` - the path string (passed in ABI registers for the path pointer/length) -/// * `args[1]` - optional recursion depth (only present for 2-argument call) -/// -/// # Output -/// * Returns `PhpType::Str` on success; the runtime helper returns null on failure which is -/// handled by the false-on-failure semantics in the caller. -/// -/// # ABI details -/// * AArch64: path in `x1`/`x2`, levels in `x3` -/// * x86_64: path in `rax`/`rdx`, levels in `rdi` -/// * Preserves path registers across the levels expression evaluation. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("dirname()"); - emit_expr(&args[0], emitter, ctx, data); - if args.len() == 1 { - abi::emit_call_label(emitter, "__rt_dirname"); // call the target-aware runtime helper that returns the parent-directory portion - return Some(PhpType::Str); - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the path ptr/len while the levels expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x0"); // move the requested parent depth into the runtime levels register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the path ptr/len after evaluating the levels expression - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the path ptr/len while the levels expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the requested parent depth into the x86_64 runtime levels register - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the path ptr/len after evaluating the levels expression - } - } - abi::emit_call_label(emitter, "__rt_dirname_levels"); // call the target-aware runtime helper that applies dirname() repeatedly - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/disk_space.rs b/src/codegen/builtins/io/disk_space.rs deleted file mode 100644 index e6198467ae..0000000000 --- a/src/codegen/builtins/io/disk_space.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Emits PHP `disk_free_space` and `disk_total_space` calls. -//! Reports the available or total byte capacity of a filesystem. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Both delegate to the `__rt_disk_space` runtime helper, passing a mode -//! selector; the helper returns a double (0.0 when `statfs` fails). - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `disk_space()` stream and I/O builtin calls. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}()", name)); - emit_expr(&args[0], emitter, ctx, data); - let mode = i64::from(name == "disk_total_space"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", mode)); // mode: 0 = free space, 1 = total space - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // path pointer into the helper argument register - emitter.instruction(&format!("mov edi, {}", mode)); // mode: 0 = free space, 1 = total space - } - } - abi::emit_call_label(emitter, "__rt_disk_space"); - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/io/fclose.rs b/src/codegen/builtins/io/fclose.rs deleted file mode 100644 index 6a9814fea4..0000000000 --- a/src/codegen/builtins/io/fclose.rs +++ /dev/null @@ -1,297 +0,0 @@ -//! Purpose: -//! Emits PHP `fclose` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits PHP `fclose(stream)` by extracting the file descriptor from the stream -/// resource, closing it via the target syscall/libc, and returning a bool indicating -/// success (true) or failure (false). Consumes the stream resource in `args[0]`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fclose()"); - emit_stream_fd_arg("fclose", &args[0], emitter, ctx, data); - let success = ctx.next_label("fclose_ok"); - let done = ctx.next_label("fclose_done"); - let user_wrapper_label = ctx.next_label("fclose_user_wrapper"); - let after_dispatch = ctx.next_label("fclose_after_dispatch"); - let phar_label = ctx.next_label("fclose_phar"); - - // -- phar:// write stream synthetic fd (exact 0x50000000): finalize the - // buffered archive to disk instead of the normal close path. -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x5000"); // low half of the phar-write descriptor 0x50000000 - emitter.instruction("lsl w9, w9, #16"); // form the full 0x50000000 phar-write descriptor - emitter.instruction("cmp x0, x9"); // is this the phar-write synthetic descriptor? - emitter.instruction(&format!("b.eq {}", phar_label)); // finalize the buffered phar archive to disk - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x50000000"); // the phar-write synthetic descriptor - emitter.instruction("cmp rax, r9"); // is this the phar-write synthetic descriptor? - emitter.instruction(&format!("je {}", phar_label)); // finalize the buffered phar archive to disk - } - } - - // -- user-wrapper synthetic fd path (Phase 10 step 4) -- - // fopen() returns descriptors >= 0x40000000 for user-defined wrappers - // so the inline _stream_*_filters/_zstream_handles tables (indexed by - // fd) and the close syscall do not apply to those handles. - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", user_wrapper_label)); // branch into the wrapper-aware close helper - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", user_wrapper_label)); // branch into the wrapper-aware close helper - } - } - - emit_zlib_flush_on_close(emitter, ctx); - emit_bz2_flush_on_close(emitter, ctx); - emit_iconv_flush_on_close(emitter, ctx); - emit_tls_session_teardown(emitter, ctx); - emit_user_filter_on_close(emitter, ctx); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", "_stream_read_filters"); - emitter.instruction("strb wzr, [x9, x0]"); // drop any read filter so a reused descriptor starts clean - abi::emit_symbol_address(emitter, "x9", "_stream_write_filters"); - emitter.instruction("strb wzr, [x9, x0]"); // drop any write filter so a reused descriptor starts clean - emitter.syscall(6); // close the requested file descriptor through the platform syscall path - emitter.instruction("cmp x0, #0"); // did the close syscall report success? - emitter.instruction(&format!("b.eq {}", success)); // branch to the success result when the close syscall returns zero - emitter.instruction("mov x0, #0"); // return false when the close syscall reports an error - emitter.instruction(&format!("b {}", done)); // skip the success result write on the error path - emitter.label(&success); - emitter.instruction("mov x0, #1"); // return true when the close syscall succeeds - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", "_stream_read_filters"); // read-filter table base - emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // drop any read filter so a reused descriptor starts clean - abi::emit_symbol_address(emitter, "r9", "_stream_write_filters"); // write-filter table base - emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // drop any write filter so a reused descriptor starts clean - emitter.instruction("mov rdi, rax"); // move the file descriptor into the first SysV libc close() argument register - emitter.instruction("call close"); // close the requested file descriptor through libc close() - emitter.instruction("cmp rax, 0"); // did libc close() report success? - emitter.instruction(&format!("je {}", success)); // branch to the success result when libc close() returns zero - emitter.instruction("xor eax, eax"); // return false when libc close() reports an error - emitter.instruction(&format!("jmp {}", done)); // skip the success result write on the error path - emitter.label(&success); - emitter.instruction("mov rax, 1"); // return true when libc close() succeeds - } - } - emitter.label(&done); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", after_dispatch)), // skip the user-wrapper close path on the normal-fd success/failure - Arch::X86_64 => emitter.instruction(&format!("jmp {}", after_dispatch)), // skip the user-wrapper close path on the normal-fd success/failure - } - - // -- user-wrapper dispatch: call __rt_user_wrapper_fclose with fd -- - emitter.label(&user_wrapper_label); - match emitter.target.arch { - Arch::AArch64 => { - // x0 already holds the synthetic fd; matches the helper's first arg. - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the synthetic fd into the first SysV arg register for the wrapper helper - } - } - abi::emit_call_label(emitter, "__rt_user_wrapper_fclose"); // dispatch into the wrapper's stream_close and free the handle slot - - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", after_dispatch)), // skip the phar finalize block on the user-wrapper path - Arch::X86_64 => emitter.instruction(&format!("jmp {}", after_dispatch)), // skip the phar finalize block on the user-wrapper path - } - - // -- phar:// write finalize: flush the buffered archive to disk -- - emitter.label(&phar_label); - abi::emit_call_label(emitter, "__rt_phar_write_finalize"); - - emitter.label(&after_dispatch); - Some(PhpType::Bool) -} - -/// Calls `onClose()` on any user-filter instance attached to this fd -/// (read or write direction) before the fd is closed, then clears the -/// instances-table slots so a reused descriptor starts clean. The -/// runtime helper `__rt_user_filter_release_fd` carries the same logic -/// for `stream_filter_remove`. -fn emit_user_filter_on_close(emitter: &mut Emitter, _ctx: &mut Context) { - // The helper takes fd in x0/rdi (its SysV first arg). After - // emit_stream_fd_arg the fd lives in x0/rax (the standard - // int-result register), so on x86_64 it must be moved into rdi. - if matches!(emitter.target.arch, Arch::X86_64) { - emitter.instruction("mov rdi, rax"); // fd → SysV first arg for the helper - } - abi::emit_call_label(emitter, "__rt_user_filter_release_fd"); -} - -/// Closes the TLS session attached to `fd` (if any), sending `close_notify` -/// via `_elephc_tls_close_fn` and zeroing `_tls_sessions[fd]` so the descriptor -/// can be reused for a plain TCP connection. `fd` must already be in the -/// int-result register; the helper is a no-op when no session is attached. -/// Shared by `fclose()` and `stream_socket_enable_crypto($s, false)` (the -/// mid-stream crypto-shutdown path). -pub(super) fn emit_tls_session_teardown(emitter: &mut Emitter, ctx: &mut Context) { - let skip = ctx.next_label("fclose_tls_skip"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", "_tls_sessions"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // _tls_sessions[fd] handle - emitter.instruction(&format!("cbz x10, {}", skip)); // no TLS attached → nothing to close - abi::emit_push_reg(emitter, "x0"); // preserve fd across the close call - emitter.instruction("mov x0, x10"); // handle as the close helper's first arg - abi::emit_symbol_address(emitter, "x9", "_elephc_tls_close_fn"); - emitter.instruction("ldr x9, [x9]"); // load runtime value - emitter.instruction("blr x9"); // send close_notify, drop the session - abi::emit_pop_reg(emitter, "x0"); // restore fd - abi::emit_symbol_address(emitter, "x9", "_tls_sessions"); - emitter.instruction("str xzr, [x9, x0, lsl #3]"); // clear the slot so the fd is reusable - emitter.label(&skip); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", "_tls_sessions"); // load runtime data address - emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // _tls_sessions[fd] handle - emitter.instruction("test r10, r10"); // check whether the runtime value is zero - emitter.instruction(&format!("je {}", skip)); // no TLS attached → skip - abi::emit_push_reg(emitter, "rax"); // preserve fd across the close call - emitter.instruction("mov rdi, r10"); // handle as first arg - abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_close_fn", 0); // prepare SysV call argument - emitter.instruction("call r9"); // call selected function pointer - abi::emit_pop_reg(emitter, "rax"); // restore fd - abi::emit_symbol_address(emitter, "r9", "_tls_sessions"); // load runtime data address - emitter.instruction("mov QWORD PTR [r9 + rax*8], 0"); // clear the slot - emitter.label(&skip); - } - } -} - -/// Flushes a `zlib.deflate` write filter before the descriptor is closed. -/// When `_zstream_handles[fd]` is non-zero the descriptor has an attached -/// deflate stream, so the compressed tail is flushed through the per-program -/// `_zlib_close_fn` helper. The descriptor is preserved across the call so the -/// caller's close logic still runs. Only an indirect call is emitted here — no -/// libz symbol is named, so non-zlib programs stay free of `-lz`. -fn emit_zlib_flush_on_close(emitter: &mut Emitter, ctx: &mut Context) { - let skip = ctx.next_label("fclose_zlib_skip"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", "_zstream_handles"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // load this descriptor's deflate stream handle - emitter.instruction(&format!("cbz x10, {}", skip)); // no zlib filter attached: nothing to flush - abi::emit_push_reg(emitter, "x0"); // preserve the descriptor across the flush helper - abi::emit_symbol_address(emitter, "x9", "_zlib_close_fn"); - emitter.instruction("ldr x9, [x9]"); // load the deflate close helper pointer - emitter.instruction("blr x9"); // flush the compressed tail and end the stream - abi::emit_pop_reg(emitter, "x0"); // restore the descriptor for the close path - emitter.label(&skip); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", "_zstream_handles"); // deflate stream handle table base - emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // load this descriptor's deflate stream handle - emitter.instruction("test r10, r10"); // is a zlib deflate filter attached? - emitter.instruction(&format!("je {}", skip)); // no zlib filter attached: nothing to flush - abi::emit_push_reg(emitter, "rax"); // preserve the descriptor across the flush helper - emitter.instruction("mov rdi, rax"); // fd argument for the deflate close helper - abi::emit_load_symbol_to_reg(emitter, "r9", "_zlib_close_fn", 0); // load the deflate close helper pointer - emitter.instruction("call r9"); // flush the compressed tail and end the stream - abi::emit_pop_reg(emitter, "rax"); // restore the descriptor for the close path - emitter.label(&skip); - } - } -} - -/// Closes a `convert.iconv` WRITE filter before the descriptor is closed. When -/// `_iconv_handles[fd]` is non-zero the descriptor has an attached iconv -/// transcoder, so the per-program `_iconv_close_fn` helper `iconv_close`s it and -/// clears the handle. The descriptor is preserved across the call. Only an -/// indirect call is emitted here — no iconv symbol is named, so non-iconv -/// programs stay free of the macOS `-liconv` dependency. -fn emit_iconv_flush_on_close(emitter: &mut Emitter, ctx: &mut Context) { - let skip = ctx.next_label("fclose_iconv_skip"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", "_iconv_handles"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // load this descriptor's iconv transcoder handle - emitter.instruction(&format!("cbz x10, {}", skip)); // no iconv write filter attached: nothing to close - abi::emit_push_reg(emitter, "x0"); // preserve the descriptor across the close helper - abi::emit_symbol_address(emitter, "x9", "_iconv_close_fn"); - emitter.instruction("ldr x9, [x9]"); // load the iconv close helper pointer - emitter.instruction("blr x9"); // iconv_close the descriptor and clear the handle - abi::emit_pop_reg(emitter, "x0"); // restore the descriptor for the close path - emitter.label(&skip); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", "_iconv_handles"); // iconv transcoder handle table base - emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // load this descriptor's iconv transcoder handle - emitter.instruction("test r10, r10"); // is an iconv write filter attached? - emitter.instruction(&format!("je {}", skip)); // no iconv write filter attached: nothing to close - abi::emit_push_reg(emitter, "rax"); // preserve the descriptor across the close helper - emitter.instruction("mov rdi, rax"); // fd argument for the iconv close helper - abi::emit_load_symbol_to_reg(emitter, "r9", "_iconv_close_fn", 0); // load the iconv close helper pointer - emitter.instruction("call r9"); // iconv_close the descriptor and clear the handle - abi::emit_pop_reg(emitter, "rax"); // restore the descriptor for the close path - emitter.label(&skip); - } - } -} - -/// Flushes a `bzip2.compress` write filter before the descriptor is closed. -/// When `_bzstream_handles[fd]` is non-zero the descriptor has an attached -/// bzip2 compress stream, so the compressed tail is flushed through the -/// per-program `_bz2_close_fn` helper. The descriptor is preserved across the -/// call so the caller's close logic still runs. Only an indirect call is -/// emitted here — no libbz2 symbol is named, so non-bzip2 programs stay free of -/// `-lbz2`. -fn emit_bz2_flush_on_close(emitter: &mut Emitter, ctx: &mut Context) { - let skip = ctx.next_label("fclose_bz2_skip"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", "_bzstream_handles"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // load this descriptor's bzip2 stream handle - emitter.instruction(&format!("cbz x10, {}", skip)); // no bzip2 filter attached: nothing to flush - abi::emit_push_reg(emitter, "x0"); // preserve the descriptor across the flush helper - abi::emit_symbol_address(emitter, "x9", "_bz2_close_fn"); - emitter.instruction("ldr x9, [x9]"); // load the bzip2 close helper pointer - emitter.instruction("blr x9"); // flush the compressed tail and end the stream - abi::emit_pop_reg(emitter, "x0"); // restore the descriptor for the close path - emitter.label(&skip); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", "_bzstream_handles"); // bzip2 stream handle table base - emitter.instruction("mov r10, QWORD PTR [r9 + rax*8]"); // load this descriptor's bzip2 stream handle - emitter.instruction("test r10, r10"); // is a bzip2 compress filter attached? - emitter.instruction(&format!("je {}", skip)); // no bzip2 filter attached: nothing to flush - abi::emit_push_reg(emitter, "rax"); // preserve the descriptor across the flush helper - emitter.instruction("mov rdi, rax"); // fd argument for the bzip2 close helper - abi::emit_load_symbol_to_reg(emitter, "r9", "_bz2_close_fn", 0); // load the bzip2 close helper pointer - emitter.instruction("call r9"); // flush the compressed tail and end the stream - abi::emit_pop_reg(emitter, "rax"); // restore the descriptor for the close path - emitter.label(&skip); - } - } -} diff --git a/src/codegen/builtins/io/fdatasync.rs b/src/codegen/builtins/io/fdatasync.rs deleted file mode 100644 index 0003060caa..0000000000 --- a/src/codegen/builtins/io/fdatasync.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `fdatasync` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits the `fdatasync` builtin call. -/// -/// Unboxes the stream resource in `args[0]` to extract its file descriptor, -/// then calls the runtime helper `__rt_fdatasync`. Returns `PhpType::Bool`. -/// -/// # Arguments -/// - `args[0]`: must be a valid stream resource; failure is fatal like PHP. -/// - `emitter`: instruction emitter for the current function. -/// - `ctx`: codegen context with current function frame and variable layout. -/// - `data`: data section for relocations and constant pools. -/// -/// # Returns -/// Always `Some(PhpType::Bool)` — fdatasync has no failure path in this emitter. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fdatasync()"); - emit_stream_fd_arg("fdatasync", &args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_fdatasync"); // libc fdatasync(fd) — falls back to fsync on Darwin - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/feof.rs b/src/codegen/builtins/io/feof.rs deleted file mode 100644 index ab753081d1..0000000000 --- a/src/codegen/builtins/io/feof.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Purpose: -//! Emits PHP `feof` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits code for the PHP `feof(stream)` builtin. -/// -/// Unboxes the stream resource in `args[0]` to extract a raw file descriptor, -/// then calls the target-aware runtime helper `__rt_feof` via the platform ABI. -/// Returns `PhpType::Bool` indicating end-of-file status. -/// -/// # Arguments -/// - `args[0]`: must be a valid stream expression (validated by type checker). -/// - `emitter`: used for instruction emission and target awareness. -/// - `ctx`: carries variable layout and ownership state. -/// - `data`: used for any runtime data section emission required by stream unboxing. -/// -/// # ABI details -/// - On x86_64: moves the file descriptor from `rax` (returned by stream unboxing) to `rdi` -/// before the call to satisfy the SysV AMD64 ABI first-argument register. -/// - On ARM64: the file descriptor is already in the correct register per the ABI contract. -/// -/// # Return -/// `Some(PhpType::Bool)` — `feof` always returns a boolean in PHP. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("feof()"); - emit_stream_fd_arg("feof", &args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the file descriptor into the first SysV feof helper argument register - } - abi::emit_call_label(emitter, "__rt_feof"); // query the target-aware eof helper for the given file descriptor - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/fflush.rs b/src/codegen/builtins/io/fflush.rs deleted file mode 100644 index ee0c4fedd8..0000000000 --- a/src/codegen/builtins/io/fflush.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Purpose: -//! Emits PHP `fflush` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits the `fflush` builtin call, flushing the output buffer of an open file handle. -/// -/// # Arguments -/// - `_name`: Unused name for dispatch; the builtin is identified by this module. -/// - `args`: Must contain at least one `Expr` identifying the stream resource. -/// - `emitter`: Target assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and stream metadata. -/// - `data`: Data section for constants and relocations. -/// -/// # Behavior -/// Unboxes the stream resource via `emit_stream_fd_arg` to extract the raw file descriptor, -/// then calls `__rt_fflush` (a libc `fsync` wrapper with PHP-side fflush semantics). -/// -/// # Return -/// Always returns `Some(PhpType::Bool)` — `true` on success, `false` on error (e.g., invalid stream). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fflush()"); - emit_stream_fd_arg("fflush", &args[0], emitter, ctx, data); - let user_wrapper_label = ctx.next_label("fflush_user_wrapper"); - let after_dispatch = ctx.next_label("fflush_after_dispatch"); - match emitter.target.arch { - Arch::AArch64 => { - // -- user-wrapper synthetic fd path (Phase 10 step 4) -- - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", user_wrapper_label)); // dispatch into the wrapper's stream_flush instead of fsync - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", user_wrapper_label)); // dispatch into the wrapper's stream_flush instead of fsync - } - } - abi::emit_call_label(emitter, "__rt_fflush"); // libc fsync(fd) wrapper (PHP-side fflush semantics) - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", after_dispatch)), // skip the user-wrapper path on the normal-fd result - Arch::X86_64 => emitter.instruction(&format!("jmp {}", after_dispatch)), // skip the user-wrapper path on the normal-fd result - } - emitter.label(&user_wrapper_label); - if matches!(emitter.target.arch, Arch::X86_64) { - emitter.instruction("mov rdi, rax"); // move the synthetic fd into the first SysV arg register for the wrapper helper - } - abi::emit_call_label(emitter, "__rt_user_wrapper_fflush"); // dispatch into the wrapper's stream_flush - emitter.label(&after_dispatch); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/fgetc.rs b/src/codegen/builtins/io/fgetc.rs deleted file mode 100644 index e15a568c3f..0000000000 --- a/src/codegen/builtins/io/fgetc.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Purpose: -//! Emits PHP `fgetc` stream builtin calls. -//! Reads exactly one byte from a stream resource through the runtime helper. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The runtime helper tail-calls `__rt_fread` with length = 1; length 0 is -//! boxed as PHP `false` so EOF remains distinguishable from a byte string. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits code for the PHP `fgetc` builtin. -/// -/// Reads exactly one byte from a stream resource via the `__rt_fgetc` runtime -/// helper. The result is boxed as `PhpType::Mixed` to accommodate PHP's return -/// type: a one-byte string on success, or `false` on EOF/read failure. -/// -/// # Arguments -/// * `name` — builtin name (unused, reserved for future overload resolution) -/// * `args` — call arguments; `args[0]` must be a stream resource -/// * `emitter` — target for emitted instructions -/// * `ctx` — codegen context (label generation, target info) -/// * `data` — data section for relocations -/// -/// # Returns -/// Always `Some(PhpType::Mixed)`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fgetc()"); - emit_stream_fd_arg("fgetc", &args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the file descriptor into the first SysV fread helper argument register - } - abi::emit_call_label(emitter, "__rt_fgetc"); // call the runtime helper that reads exactly one byte before PHP result boxing - box_fgetc_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the raw `fgetc` result into a `Mixed` runtime value. -/// -/// After `__rt_fgetc` returns (x0/x1 = pointer/length, x2/rdx = byte count), -/// this function branches on whether a byte was read: -/// - **AArch64**: `x2 == 0` means EOF/failure → box `false`. Otherwise box a -/// one-byte string (`tag = 1`) via `__rt_mixed_from_value`. -/// - **x86_64**: `rdx == 0` means EOF/failure → box `false`. Otherwise `rax` -/// holds the pointer and `rdx` holds the length; box as string (`eax = 1`). -/// -/// # Arguments -/// * `emitter` — target for emitted instructions -/// * `ctx` — codegen context (label generation) -fn box_fgetc_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("fgetc_false"); - let done_label = ctx.next_label("fgetc_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x2, #0"); // EOF or read failure has no byte to return - emitter.instruction(&format!("b.le {}", false_label)); // box PHP false for EOF/read failure - emitter.instruction("mov x0, #1"); // runtime tag 1 = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist and box the one-byte string - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for fgetc() EOF/failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible EOF semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rdx, 0"); // EOF or read failure has no byte to return - emitter.instruction(&format!("jle {}", false_label)); // box PHP false for EOF/read failure - emitter.instruction("mov rdi, rax"); // move the one-byte string pointer into the mixed payload low word - emitter.instruction("mov rsi, rdx"); // move the one-byte string length into the mixed payload high word - emitter.instruction("mov eax, 1"); // runtime tag 1 = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist and box the one-byte string - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for fgetc() EOF/failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible EOF semantics - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/fgetcsv.rs b/src/codegen/builtins/io/fgetcsv.rs deleted file mode 100644 index e8ec2938bd..0000000000 --- a/src/codegen/builtins/io/fgetcsv.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Purpose: -//! Emits PHP `fgetcsv` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits code to call the PHP `fgetcsv` builtin, which reads one row from a CSV file into an array of strings. -/// -/// Inputs: -/// - `args[0]`: a PHP stream resource whose file descriptor is extracted and passed to the runtime helper. -/// - `emitter`: target-aware instruction emitter. -/// - `ctx`: codegen context carrying target, layout, and state. -/// - `data`: mutable data section for relocatable labels. -/// -/// Side effects: -/// - Calls `emit_stream_fd_arg` to unbox the stream resource to a raw file descriptor. -/// - On x86_64, moves the descriptor into `rdi` per the SysV ABI. -/// - Calls `__rt_fgetcsv` runtime helper which reads one CSV row and returns a string array. -/// -/// Output: -/// - Returns `Some(PhpType::Array(Box::new(PhpType::Str)))` indicating the result is an array of strings. -/// - Returns `None` only on error path (handled by caller). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fgetcsv()"); - emit_stream_fd_arg("fgetcsv", &args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the file descriptor into the first SysV fgetcsv helper argument register - } - abi::emit_call_label(emitter, "__rt_fgetcsv"); // read one CSV row through the target-aware runtime helper and return the resulting string array - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/io/fgets.rs b/src/codegen/builtins/io/fgets.rs deleted file mode 100644 index c4d7081511..0000000000 --- a/src/codegen/builtins/io/fgets.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Purpose: -//! Emits PHP `fgets` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. -//! - Normal descriptors delegate to `__rt_fgets`. Synthetic user-wrapper -//! descriptors (`>= 0x40000000`) read one line through a COMPILED, feof-gated -//! loop emitted here: it checks `stream_eof` before each 1-byte `stream_read`, -//! so it never makes the EOF read whose empty `substr` result corrupts the -//! caller's resource cell (see `stream_get_contents`). Bytes accumulate into -//! `_user_wrapper_drain_buf`; the line ends at `\n` (kept) or EOF. The boxed -//! result copies the bytes out via `__rt_str_persist`, so reusing the shared -//! buffer is safe. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits a call to the `fgets` builtin. -/// -/// Unboxes the stream resource in `args[0]` to extract a raw file descriptor, -/// then reads one line. Normal descriptors invoke `__rt_fgets`; synthetic -/// user-wrapper descriptors run the feof-gated byte loop emitted below. Both -/// paths converge on `(ptr, len)` and the shared false/string boxing. -/// -/// # Arguments -/// * `args[0]` — must be a valid stream resource; validated by `emit_stream_fd_arg`. -/// * `emitter` — target-aware instruction emitter. -/// * `ctx` — codegen context carrying stream/FD metadata. -/// -/// # Returns -/// `Some(PhpType::Mixed)` — a boxed string on success, boxed `false` at EOF. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fgets()"); - emit_stream_fd_arg("fgets", &args[0], emitter, ctx, data); - let wrapper_label = ctx.next_label("fgets_wrapper"); - let box_label = ctx.next_label("fgets_box"); - let wloop_label = ctx.next_label("fgets_wrap_loop"); - let wlast_label = ctx.next_label("fgets_wrap_last"); - let wrelease_label = ctx.next_label("fgets_wrap_release"); - let wdone_label = ctx.next_label("fgets_wrap_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 in w9 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper_label)); // wrappers read the line via the feof-gated loop below - abi::emit_call_label(emitter, "__rt_fgets"); // normal fd: runtime helper reads one line (x1=ptr, x2=len) - emitter.instruction(&format!("b {}", box_label)); // converge on the shared boxing - - emitter.label(&wrapper_label); - emitter.instruction("sub sp, sp, #16"); // scratch: [sp,#0]=fd, [sp,#8]=line length - emitter.instruction("str x0, [sp, #0]"); // save the synthetic wrapper fd - emitter.instruction("str xzr, [sp, #8]"); // line length = 0 - emitter.label(&wloop_label); - emitter.instruction("ldr x0, [sp, #0]"); // reload the wrapper fd - abi::emit_call_label(emitter, "__rt_feof"); // check stream_eof FIRST (x0 = 1 at EOF) - emitter.instruction(&format!("cbnz x0, {}", wdone_label)); // at EOF: return the bytes gathered so far - emitter.instruction("ldr x0, [sp, #0]"); // reload the wrapper fd - emitter.instruction("mov x1, #1"); // read exactly one byte - abi::emit_call_label(emitter, "__rt_fread"); // x1=chunk ptr, x2=len - emitter.instruction(&format!("cbz x2, {}", wdone_label)); // defensive: empty read also ends the line - emitter.instruction("ldrb w13, [x1]"); // load the read byte - emitter.instruction("ldr x10, [sp, #8]"); // current line length - emitter.instruction("movz x11, #0x10, lsl #16"); // line buffer capacity = 1 MiB - emitter.instruction("cmp x10, x11"); // is the buffer full? - emitter.instruction(&format!("b.ge {}", wrelease_label)); // full: release the chunk and stop - abi::emit_symbol_address(emitter, "x12", "_user_wrapper_drain_buf"); - emitter.instruction("strb w13, [x12, x10]"); // append the byte to the line buffer - emitter.instruction("add x10, x10, #1"); // advance the line length - emitter.instruction("str x10, [sp, #8]"); // store the updated line length - emitter.instruction("cmp w13, #10"); // is the byte a newline? - emitter.instruction("mov x0, x1"); // chunk ptr for release (flags preserved) - emitter.instruction(&format!("b.eq {}", wlast_label)); // newline: release this chunk, then finish the line - abi::emit_call_label(emitter, "__rt_decref_any"); // not newline: release the chunk and keep reading - emitter.instruction(&format!("b {}", wloop_label)); // read the next byte - emitter.label(&wlast_label); - abi::emit_call_label(emitter, "__rt_decref_any"); // release the newline chunk - emitter.instruction(&format!("b {}", wdone_label)); // line complete - emitter.label(&wrelease_label); - emitter.instruction("mov x0, x1"); // chunk ptr for release - abi::emit_call_label(emitter, "__rt_decref_any"); // release the dropped chunk (buffer full) - emitter.label(&wdone_label); - abi::emit_symbol_address(emitter, "x1", "_user_wrapper_drain_buf"); // line pointer - emitter.instruction("ldr x2, [sp, #8]"); // line length - emitter.instruction("add sp, sp, #16"); // release the scratch frame - emitter.label(&box_label); - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper_label)); // wrappers read the line via the feof-gated loop below - emitter.instruction("mov rdi, rax"); // normal fd: pass the descriptor to the helper - abi::emit_call_label(emitter, "__rt_fgets"); // runtime helper reads one line (rax=ptr, rdx=len) - emitter.instruction(&format!("jmp {}", box_label)); // converge on the shared boxing - - emitter.label(&wrapper_label); - emitter.instruction("sub rsp, 16"); // scratch: [rsp+0]=fd, [rsp+8]=line length - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the synthetic wrapper fd - emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // line length = 0 - emitter.label(&wloop_label); - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the wrapper fd - abi::emit_call_label(emitter, "__rt_feof"); // check stream_eof FIRST (rax = 1 at EOF) - emitter.instruction("test rax, rax"); // at EOF? - emitter.instruction(&format!("jnz {}", wdone_label)); // at EOF: return the bytes gathered so far - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the wrapper fd - emitter.instruction("mov rsi, 1"); // read exactly one byte - abi::emit_call_label(emitter, "__rt_fread"); // rax=chunk ptr, rdx=len - emitter.instruction("test rdx, rdx"); // zero-length read? - emitter.instruction(&format!("jz {}", wdone_label)); // defensive: empty read also ends the line - emitter.instruction("movzx r10d, BYTE PTR [rax]"); // load the read byte - emitter.instruction("mov r8, QWORD PTR [rsp + 8]"); // current line length - emitter.instruction("cmp r8, 0x100000"); // is the buffer full (1 MiB)? - emitter.instruction(&format!("jge {}", wrelease_label)); // full: release the chunk and stop - abi::emit_symbol_address(emitter, "r11", "_user_wrapper_drain_buf"); // line buffer base - emitter.instruction("mov BYTE PTR [r11 + r8], r10b"); // append the byte to the line buffer - emitter.instruction("inc r8"); // advance the line length - emitter.instruction("mov QWORD PTR [rsp + 8], r8"); // store the updated line length - emitter.instruction("cmp r10b, 10"); // is the byte a newline? (rax still = chunk ptr) - emitter.instruction(&format!("je {}", wlast_label)); // newline: release this chunk, then finish the line - abi::emit_call_label(emitter, "__rt_decref_any"); // not newline: release the chunk (rax=ptr) and keep reading - emitter.instruction(&format!("jmp {}", wloop_label)); // read the next byte - emitter.label(&wlast_label); - abi::emit_call_label(emitter, "__rt_decref_any"); // release the newline chunk (rax=ptr) - emitter.instruction(&format!("jmp {}", wdone_label)); // line complete - emitter.label(&wrelease_label); - abi::emit_call_label(emitter, "__rt_decref_any"); // release the dropped chunk (rax=ptr, buffer full) - emitter.label(&wdone_label); - abi::emit_symbol_address(emitter, "rax", "_user_wrapper_drain_buf"); // line pointer - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // line length - emitter.instruction("add rsp, 16"); // release the scratch frame - emitter.label(&box_label); - } - } - // The (ptr, len) result is shared by both paths. PHP fgets distinguishes EOF - // (no bytes accumulated) from a successful read: len == 0 means false. Box - // the result as a Mixed cell so `($l = fgets($f)) !== false` actually - // terminates when EOF is reached. - let false_label = ctx.next_label("fgets_false"); - let done_label = ctx.next_label("fgets_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x2, {}", false_label)); // zero-length read → PHP false - emitter.instruction("mov x0, #1"); // runtime tag 1 = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the line as a Mixed string - emitter.instruction(&format!("b {}", done_label)); // continue at target label - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // bool payload = 0 (false) - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box PHP false so `!== false` short-circuits - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rdx, rdx"); // zero-length read → PHP false - emitter.instruction(&format!("jz {}", false_label)); // branch when the checked value is zero or equal - emitter.instruction("mov rdi, rax"); // string ptr → mixed_from_value's payload-lo register - emitter.instruction("mov rsi, rdx"); // string len → mixed_from_value's payload-hi register - emitter.instruction("mov eax, 1"); // runtime tag 1 = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the line as a Mixed string - emitter.instruction(&format!("jmp {}", done_label)); // continue at target label - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // bool payload = 0 (false) - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box PHP false so `!== false` short-circuits - emitter.label(&done_label); - } - } - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/file.rs b/src/codegen/builtins/io/file.rs deleted file mode 100644 index 01bdf03134..0000000000 --- a/src/codegen/builtins/io/file.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `file` file input builtin calls. -//! Coordinates path or stream arguments with runtime helpers that allocate returned strings or arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Failure paths must distinguish PHP false from empty string or empty array results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `file` builtin call. -/// -/// Reads an entire file into an array of lines, each line as a string. -/// Uses `__rt_file` runtime helper which returns a refcounted array of strings. -/// -/// # Arguments -/// * `args[0]` - Path or stream expression to read from -/// -/// # Returns -/// `PhpType::Array(Box::new(PhpType::Str))` on success; runtime helper handles -/// PHP false (file not found/empty) by returning an empty array. -/// -/// # ABI -/// Calls `__rt_file` which materializes the path arg and returns the allocated array. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("file()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_file"); // call the target-aware runtime helper that reads the file into an array of lines - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/io/file_exists.rs b/src/codegen/builtins/io/file_exists.rs deleted file mode 100644 index 5eae56ecc1..0000000000 --- a/src/codegen/builtins/io/file_exists.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Purpose: -//! Emits PHP `file_exists` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `file_exists` filesystem check for a single path argument. -/// -/// Evaluates the path expression (argument 0). A `scheme://...` path whose -/// scheme matches a registered userspace wrapper is routed through -/// `__rt_user_wrapper_url_stat`, which instantiates the wrapper and calls its -/// `url_stat()`; the path exists iff that returns a stat array (not `false`). -/// Any other path falls through to `__rt_file_exists` for a real filesystem -/// stat. Returns `PhpType::Bool`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("file_exists()"); - emit_expr(&args[0], emitter, ctx, data); - let fallback = ctx.next_label("file_exists_fs"); - let done = ctx.next_label("file_exists_done"); - match emitter.target.arch { - Arch::AArch64 => { - // -- path string: x1 = ptr, x2 = len (the elephc string ABI) -- - emitter.instruction("sub sp, sp, #16"); // scratch: [sp,#0] path ptr / result bool, [sp,#8] path len - emitter.instruction("str x1, [sp, #0]"); // save path ptr for the filesystem fallback - emitter.instruction("str x2, [sp, #8]"); // save path len for the filesystem fallback - emitter.instruction("mov x0, x1"); // url_stat helper arg0 = path ptr - emitter.instruction("mov x1, x2"); // url_stat helper arg1 = path len - emitter.instruction("mov x2, #0"); // url_stat helper arg2 = flags (0) - abi::emit_call_label(emitter, "__rt_user_wrapper_url_stat"); // x0 = boxed Mixed when a wrapper scheme matched - abi::emit_symbol_address(emitter, "x9", "_url_stat_matched"); - emitter.instruction("ldrb w9, [x9]"); // did a registered wrapper scheme match? - emitter.instruction(&format!("cbz w9, {}", fallback)); // no → real filesystem stat - emitter.instruction("ldr x10, [x0]"); // boxed Mixed runtime tag (url_stat result) - emitter.instruction("cmp x10, #3"); // tag 3 = bool false (wrapper reported the path absent)? - emitter.instruction("cset x10, ne"); // exists = (tag != 3, i.e. url_stat returned a stat array) - emitter.instruction("str x10, [sp, #0]"); // stash the exists bool across the result release - abi::emit_call_label(emitter, "__rt_decref_any"); // x0 still = boxed Mixed result; release it - emitter.instruction("ldr x0, [sp, #0]"); // reload the exists bool as the builtin result - emitter.instruction(&format!("b {}", done)); // skip the filesystem path - emitter.label(&fallback); - emitter.instruction("ldr x1, [sp, #0]"); // restore path ptr for the filesystem helper - emitter.instruction("ldr x2, [sp, #8]"); // restore path len for the filesystem helper - abi::emit_call_label(emitter, "__rt_file_exists"); // real filesystem existence check - emitter.label(&done); - emitter.instruction("add sp, sp, #16"); // release the scratch frame - } - Arch::X86_64 => { - // -- path string: rax = ptr, rdx = len (the elephc string ABI) -- - emitter.instruction("sub rsp, 16"); // scratch: [rsp+0] path ptr / result bool, [rsp+8] path len - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save path ptr for the filesystem fallback - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save path len for the filesystem fallback - emitter.instruction("mov rdi, rax"); // url_stat helper arg0 = path ptr - emitter.instruction("mov rsi, rdx"); // url_stat helper arg1 = path len - emitter.instruction("xor edx, edx"); // url_stat helper arg2 = flags (0) - abi::emit_call_label(emitter, "__rt_user_wrapper_url_stat"); // rax = boxed Mixed when a wrapper scheme matched - abi::emit_symbol_address(emitter, "r9", "_url_stat_matched"); // load runtime data address - emitter.instruction("movzx r9d, BYTE PTR [r9]"); // did a registered wrapper scheme match? - emitter.instruction("test r9d, r9d"); // matched flag set? - emitter.instruction(&format!("jz {}", fallback)); // no → real filesystem stat - emitter.instruction("mov r10, QWORD PTR [rax]"); // boxed Mixed runtime tag (url_stat result) - emitter.instruction("mov rdi, rax"); // preserve the boxed result pointer for release - emitter.instruction("cmp r10, 3"); // tag 3 = bool false (wrapper reported the path absent)? - emitter.instruction("setne al"); // exists = (tag != 3, i.e. url_stat returned a stat array) - emitter.instruction("movzx eax, al"); // widen the bool into the canonical result register - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // stash the exists bool across the result release - emitter.instruction("mov rax, rdi"); // __rt_decref_any reads the pointer in rax - abi::emit_call_label(emitter, "__rt_decref_any"); // release the boxed Mixed result - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // reload the exists bool as the builtin result - emitter.instruction(&format!("jmp {}", done)); // skip the filesystem path - emitter.label(&fallback); - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore path ptr for the filesystem helper - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore path len for the filesystem helper - abi::emit_call_label(emitter, "__rt_file_exists"); // real filesystem existence check - emitter.label(&done); - emitter.instruction("add rsp, 16"); // release the scratch frame - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/file_get_contents.rs b/src/codegen/builtins/io/file_get_contents.rs deleted file mode 100644 index 4cc69f6f0d..0000000000 --- a/src/codegen/builtins/io/file_get_contents.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! Purpose: -//! Emits PHP `file_get_contents` file input builtin calls. -//! Coordinates filesystem paths, PHAR entries, and built-in URL wrappers with -//! runtime helpers that allocate and box returned string or false results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Literal `http://`, `https://`, `ftp://`, and `ftps://` URLs reuse the same -//! wrapper open helpers as `fopen()`, then slurp and close the descriptor. -//! - Dynamic paths route through a runtime URL dispatcher; when TLS is required, -//! the program entry point publishes TLS entry points before user code runs. -//! - Failure paths must distinguish PHP false from empty string results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits code for the PHP `file_get_contents` builtin. -/// -/// `args[0]` is evaluated and pushed as the filename argument, then `__rt_file_get_contents` -/// is called. On AArch64 the filename is passed in `x0`; on x86_64 in `rdi`. The result is -/// always boxed into `PhpType::Mixed` — a successful read yields a string (tag 1), while -/// failure yields bool false (tag 3). Returns `PhpType::Mixed` unconditionally. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("file_get_contents()"); - // A literal phar:// URL is read and decoded at compile time (uncompressed, - // gzip, or bzip2) and the entry's bytes are embedded as the string result — - // the same compile-time model as fopen("phar://...","r"). A missing archive - // or entry yields PHP false. (A non-literal phar:// path is read at run time - // through fopen + stream_get_contents instead.) - if let crate::parser::ast::ExprKind::StringLiteral(url) = &args[0].kind { - if url.starts_with("phar://") { - match super::phar_stream::extract_phar_entry(url) { - Some(bytes) => { - let (sym, len) = data.add_string(&bytes); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", &sym); - emitter.instruction(&format!("mov x2, #{}", len)); // embedded entry length → string-result length register - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rax", &sym); - emitter.instruction(&format!("mov rdx, {}", len)); // embedded entry length → string-result length register - } - } - } - None => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x1, #0"), // missing archive/entry → null string ptr → boxed false - Arch::X86_64 => emitter.instruction("xor eax, eax"), // missing archive/entry → null string ptr → boxed false - }, - } - box_file_get_contents_result(emitter, ctx); - return Some(PhpType::Mixed); - } - // Literal http/https/ftp/ftps URLs open the wrapper, slurp the whole body - // into an owned string, and box it — the fopen() + stream_get_contents() - // + fclose() model. A failed open boxes PHP false. - if url.starts_with("http://") { - super::http_stream::emit_open_fd(args, emitter, data); - emit_url_slurp_and_box(emitter, ctx); - return Some(PhpType::Mixed); - } - if url.starts_with("https://") { - super::https_stream::emit_open_fd(args, emitter, data); - emit_url_slurp_and_box(emitter, ctx); - return Some(PhpType::Mixed); - } - if url.starts_with("ftps://") { - super::ftps_stream::emit_open_fd(args, emitter, data); - emit_url_slurp_and_box(emitter, ctx); - return Some(PhpType::Mixed); - } - if url.starts_with("ftp://") { - super::ftp_stream::emit_open_fd(args, emitter, data); - emit_url_slurp_and_box(emitter, ctx); - return Some(PhpType::Mixed); - } - } - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_file_get_contents_maybe_url"); // routes dynamic URL/phar paths before the filesystem reader - box_file_get_contents_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the raw string result (pointer + length) into a `PhpType::Mixed` cell. -/// -/// On AArch64 the runtime helper places the string pointer in `x1` and length in `x2`; -/// on x86_64 in `rax` and `rdx`. A null pointer signals failure — this path emits bool -/// false (tag 3) via `__rt_mixed_from_value`. On success, the string pointer/length are -/// stored directly into a heap-allocated mixed cell (tag 1) without copying the buffer. -pub(super) fn box_file_get_contents_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("fgc_false"); - let done_label = ctx.next_label("fgc_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null runtime string pointer means file_get_contents() failed - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the successful file payload while allocating the mixed box - emitter.instruction("mov x0, #24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the mixed result cell for a successful string payload - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocated payload as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag in the mixed result - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the owned file string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words without copying the owned file buffer - emitter.instruction(&format!("b {}", done_label)); // skip the false boxing path after a successful read - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for file_get_contents() failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null runtime string pointer means file_get_contents() failed - emitter.instruction(&format!("jz {}", false_label)); // box false when the runtime helper reports failure - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the successful file payload while allocating the mixed box - emitter.instruction("mov rax, 24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the mixed result cell for a successful string payload - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5)); // materialize the mixed-cell heap kind word with the x86_64 heap marker - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocated payload as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag in the mixed result - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the owned file string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer without copying the owned file buffer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length without copying the owned file buffer - emitter.instruction(&format!("jmp {}", done_label)); // skip the false boxing path after a successful read - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for file_get_contents() failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - } -} - -/// Given an open stream fd in the int-result register (`x0`/`rax`), or `-1` on a -/// failed open, slurps the whole stream into an **owned** string, closes the fd, -/// and boxes the result as `file_get_contents()`'s `PhpType::Mixed` (string on -/// success, bool `false` on a failed open). The slurp uses the TLS-aware -/// `__rt_stream_get_contents` read-all helper and then `__rt_str_persist` so the -/// boxed string owns its bytes and survives later `_concat_buf` reuse. -fn emit_url_slurp_and_box(emitter: &mut Emitter, ctx: &mut Context) { - let fail_label = ctx.next_label("fgc_url_fail"); - let done_label = ctx.next_label("fgc_url_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // a failed wrapper open returns -1 - emitter.instruction(&format!("b.lt {}", fail_label)); // box false when the open failed - emitter.instruction("sub sp, sp, #32"); // [sp,#0]=fd, [sp,#8]=ptr, [sp,#16]=len - emitter.instruction("str x0, [sp, #0]"); // save the fd for the close below - abi::emit_call_label(emitter, "__rt_stream_get_contents"); // (x0=fd) → x1=ptr, x2=len (concat_buf slice) - emitter.instruction("stp x1, x2, [sp, #8]"); // save the slurped ptr/len across the close - emitter.instruction("ldr x0, [sp, #0]"); // reload the fd - super::fclose::emit_tls_session_teardown(emitter, ctx); - emitter.syscall(6); // close(fd) - emitter.instruction("ldp x1, x2, [sp, #8]"); // restore the slurped ptr/len - abi::emit_call_label(emitter, "__rt_str_persist"); // copy to owned heap → x1=ptr, x2=len - emitter.instruction("add sp, sp, #32"); // release the slurp frame - emitter.instruction(&format!("b {}", done_label)); // boxed string payload is ready - emitter.label(&fail_label); - emitter.instruction("mov x1, #0"); // null string ptr → boxed false - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // a failed wrapper open returns -1 - emitter.instruction(&format!("jl {}", fail_label)); // box false when the open failed - emitter.instruction("sub rsp, 32"); // [rsp+0]=fd, [rsp+8]=ptr, [rsp+16]=len - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the fd for the close below - emitter.instruction("mov rdi, rax"); // __rt_stream_get_contents takes the fd in rdi - abi::emit_call_label(emitter, "__rt_stream_get_contents"); // rax=ptr, rdx=len (concat_buf slice) - emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // save the slurped ptr across the close - emitter.instruction("mov QWORD PTR [rsp + 16], rdx"); // save the slurped len across the close - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // reload the fd for TLS teardown - super::fclose::emit_tls_session_teardown(emitter, ctx); - emitter.instruction("mov rdi, rax"); // move the restored fd into close()'s argument register - emitter.instruction("call close"); // close(fd) via libc - emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // restore the slurped ptr - emitter.instruction("mov rdx, QWORD PTR [rsp + 16]"); // restore the slurped len - abi::emit_call_label(emitter, "__rt_str_persist"); // copy to owned heap → rax=ptr, rdx=len - emitter.instruction("add rsp, 32"); // release the slurp frame - emitter.instruction(&format!("jmp {}", done_label)); // boxed string payload is ready - emitter.label(&fail_label); - emitter.instruction("xor eax, eax"); // null string ptr → boxed false - emitter.label(&done_label); - } - } - box_file_get_contents_result(emitter, ctx); -} diff --git a/src/codegen/builtins/io/file_put_contents.rs b/src/codegen/builtins/io/file_put_contents.rs deleted file mode 100644 index a211484be6..0000000000 --- a/src/codegen/builtins/io/file_put_contents.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Purpose: -//! Emits PHP `file_put_contents` filesystem mutation builtin calls. -//! Passes path and mode/owner arguments to runtime helpers that perform observable OS operations. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `file_put_contents` builtin call. -/// -/// Saves the path argument (args[0]) on the stack/caller-saved registers, evaluates -/// the data argument (args[1]) in source order, then materializes all four string-argument -/// registers and calls `__rt_file_put_contents`. Returns `PhpType::Int` (byte count or false). -/// -/// # Arguments -/// - `_name`: ignored (always `file_put_contents`) -/// - `args[0]`: path string -/// - `args[1]`: data string -/// -/// # Side effects -/// - Performs observable filesystem writes via the runtime helper. -/// - Clobbers caller-saved registers (`x0`-`x7`/`rdi`-`rsi` pairs) per ABI. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("file_put_contents()"); - // file_put_contents("phar://archive/entry", $data) assembles a signed - // single-entry phar (same runtime as fopen+fwrite+fclose). A literal phar:// - // URL that resolves to a write target is handled here; anything else (or an - // unresolvable URL) falls through to the normal file write below. - if let crate::parser::ast::ExprKind::StringLiteral(url) = &args[0].kind { - if url.starts_with("phar://") && args.len() >= 2 { - if let Some(ty) = - super::phar_stream::emit_file_put_contents_write(url, &args[1], emitter, ctx, data) - { - return Some(ty); - } - } - } - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push filename ptr and length onto the temporary stack while the data expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the data pointer into the third string-argument register pair slot - emitter.instruction("mov x4, x2"); // move the data length into the fourth string-argument register pair slot - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the filename pointer and length after evaluating the data expression - abi::emit_call_label(emitter, "__rt_file_put_contents"); // call the target-aware runtime helper that writes the string payload to disk - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the filename pointer and length while the data expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the data pointer into the third x86_64 string-argument slot - emitter.instruction("mov rsi, rdx"); // move the data length into the fourth x86_64 string-argument slot - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the filename pointer and length after evaluating the data expression - abi::emit_call_label(emitter, "__rt_file_put_contents"); // call the target-aware runtime helper that writes the string payload to disk - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/fileatime.rs b/src/codegen/builtins/io/fileatime.rs deleted file mode 100644 index e066521eab..0000000000 --- a/src/codegen/builtins/io/fileatime.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Purpose: -//! Emits PHP `fileatime` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_int_or_false_result; - -/// Emits code for the PHP `fileatime()` builtin, which returns the last access time of a file. -/// -/// # Arguments -/// - `_name`: Unused parameter present for dispatcher uniformity. -/// - `args`: Single argument providing the file path expression. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and metadata. -/// - `data`: Data section for relocatable literals. -/// -/// # Returns -/// Always returns `Some(PhpType::Mixed)` because the builtin can return an integer -/// timestamp on success or `false` on failure. -/// -/// # Behavior -/// 1. Emits code to evaluate and push the file path argument. -/// 2. Calls `__rt_fileatime`, the target-aware runtime helper that invokes `stat` and -/// extracts `st_atime`. -/// 3. Boxes the raw integer or `false` sentinel into a PHP `Mixed` value. -/// -/// # Notes -/// Filesystem state is observable; emitters must preserve call order and propagate -/// the `false` sentinel on failure rather than raising a fatal error. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fileatime()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_fileatime"); // call the target-aware runtime helper that loads st_atime - box_stat_int_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/filectime.rs b/src/codegen/builtins/io/filectime.rs deleted file mode 100644 index d6196e1fa5..0000000000 --- a/src/codegen/builtins/io/filectime.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Emits PHP `filectime` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_int_or_false_result; - -/// Emits a call to PHP `filectime($path)`. -/// -/// Input: `args[0]` must be a path expression (string). Emitter evaluates it and -/// passes the resulting string pointer+length to `__rt_filectime` via the ABI. -/// -/// Output: returns `Some(PhpType::Mixed)` — the boxed i64 modification timestamp -/// on success, or boxed PHP `false` on failure (including non-existent paths). -/// -/// Side effects: calls `__rt_filectime` runtime helper; observable filesystem access -/// means call order and error handling must match PHP semantics. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("filectime()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_filectime"); // call the target-aware runtime helper that loads st_ctime - box_stat_int_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/filegroup.rs b/src/codegen/builtins/io/filegroup.rs deleted file mode 100644 index 874fa8ddec..0000000000 --- a/src/codegen/builtins/io/filegroup.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Purpose: -//! Emits PHP `filegroup` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_int_or_false_result; - -/// Emits code for the PHP `filegroup()` builtin. -/// -/// `filegroup()` returns the group ID of the file at `args[0]`, or `false` if -/// the file cannot be stat'd. The path expression is emitted first, then the -/// runtime helper `__rt_filegroup` is called to populate `st_gid` from the OS -/// stat buffer. The integer result (or `false` sentinel) is boxed into a -/// `PhpType::Mixed` and returned. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("filegroup()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_filegroup"); // call the target-aware runtime helper that loads st_gid - box_stat_int_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/fileinode.rs b/src/codegen/builtins/io/fileinode.rs deleted file mode 100644 index c2337c195e..0000000000 --- a/src/codegen/builtins/io/fileinode.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Purpose: -//! Emits PHP `fileinode` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_int_or_false_result; - -/// Emits code for the PHP `fileinode(path)` builtin. -/// -/// Evaluates `path` as the sole argument, calls the target-aware runtime helper -/// `__rt_fileinode` to retrieve the inode number via `stat`, then boxes the result -/// as a PHP `Mixed` value (either an integer inode or `false` on failure). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fileinode()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_fileinode"); // call the target-aware runtime helper that loads st_ino - box_stat_int_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/filemtime.rs b/src/codegen/builtins/io/filemtime.rs deleted file mode 100644 index 6d3c942654..0000000000 --- a/src/codegen/builtins/io/filemtime.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Purpose: -//! Emits PHP `filemtime` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `filemtime` builtin call. -/// -/// Emits the path argument, calls the target-aware runtime helper `__rt_filemtime`, -/// and returns `Some(PhpType::Int)`. The runtime handles the false sentinel on failure -/// (missing file, permission error, etc.) and converts it to a Unix timestamp representation -/// that remains distinguishable from valid timestamps. -/// -/// # Arguments -/// * `_name` - Unused, always "filemtime" (kept for dispatcher signature parity) -/// * `args` - Exactly one argument: the path expression -/// * `emitter` - Target assembly emitter -/// * `ctx` - Codegen context (variable layout, class metadata) -/// * `data` - Data section for relocations and string constants -/// -/// # Returns -/// `Some(PhpType::Int)` — the modification timestamp is always typed as Int, -/// even when the underlying filesystem stat fails; the runtime sentinel preserves -/// this distinction. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("filemtime()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_filemtime"); // call the target-aware runtime helper that returns the Unix modification timestamp - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/fileowner.rs b/src/codegen/builtins/io/fileowner.rs deleted file mode 100644 index eb640d915a..0000000000 --- a/src/codegen/builtins/io/fileowner.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Emits PHP `fileowner` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_int_or_false_result; - -/// Emits the `fileowner` builtin call. -/// -/// Evaluates the path argument, calls the runtime helper that retrieves `st_uid` -/// via the target-aware stat path, boxes the integer UID or `false` into a PHP -/// `Mixed` value, and returns `PhpType::Mixed`. -/// -/// Arguments: -/// - `args[0]` must be a path expression (string). -/// -/// Side effects: filesystem state is observable; call order and failure sentinels are preserved. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fileowner()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_fileowner"); // call the target-aware runtime helper that loads st_uid - box_stat_int_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/fileperms.rs b/src/codegen/builtins/io/fileperms.rs deleted file mode 100644 index 0eaecb2b25..0000000000 --- a/src/codegen/builtins/io/fileperms.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Purpose: -//! Emits PHP `fileperms` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_int_or_false_result; - -/// Emits the `fileperms` builtin call. -/// -/// Evaluates the file path argument, calls the `__rt_fileperms` runtime helper -/// which invokes `stat()` and extracts the `st_mode` field, then boxes the result -/// as `PhpType::Mixed` (integer permission mask on success, PHP false on failure). -/// -/// # Arguments -/// - `_name`: unused, follows the builtin emitter convention -/// - `args[0]`: the file path expression -/// -/// # Returns -/// Always returns `Some(PhpType::Mixed)` — the boxed result is never consumed by a caller -/// that would interpret `None` as an error; the PHP false sentinel handles failure. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fileperms()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_fileperms"); // call the target-aware runtime helper that loads st_mode - box_stat_int_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/filesize.rs b/src/codegen/builtins/io/filesize.rs deleted file mode 100644 index 4bd1df16f6..0000000000 --- a/src/codegen/builtins/io/filesize.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Purpose: -//! Emits PHP `filesize` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `filesize()` call. A `scheme://...` path whose scheme matches a -/// registered userspace wrapper is routed through -/// `__rt_user_wrapper_url_stat_field` (field selector 0 = `'size'`), which calls -/// the wrapper's `url_stat()` and extracts the integer `'size'` entry. Any other -/// path falls through to the platform-aware `__rt_filesize`. Returns -/// `PhpType::Int` (PHP's `false`-on-error is represented via the existing scalar -/// convention, which `__rt_filesize` itself models only approximately). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("filesize()"); - emit_expr(&args[0], emitter, ctx, data); - let fallback = ctx.next_label("filesize_fs"); - let done = ctx.next_label("filesize_done"); - match emitter.target.arch { - Arch::AArch64 => { - // -- path string: x1 = ptr, x2 = len -- - emitter.instruction("sub sp, sp, #16"); // scratch: [sp,#0] path ptr, [sp,#8] path len - emitter.instruction("str x1, [sp, #0]"); // save path ptr for the filesystem fallback - emitter.instruction("str x2, [sp, #8]"); // save path len for the filesystem fallback - emitter.instruction("mov x0, x1"); // field helper arg0 = path ptr - emitter.instruction("mov x1, x2"); // field helper arg1 = path len - emitter.instruction("mov x2, #0"); // field selector 0 = 'size' - abi::emit_call_label(emitter, "__rt_user_wrapper_url_stat_field"); // x0 = wrapper 'size' (or -1) - abi::emit_symbol_address(emitter, "x9", "_url_stat_matched"); - emitter.instruction("ldrb w9, [x9]"); // did a registered wrapper scheme match? - emitter.instruction(&format!("cbz w9, {}", fallback)); // no → real filesystem filesize - emitter.instruction(&format!("b {}", done)); // matched: x0 already holds the wrapper 'size' - emitter.label(&fallback); - emitter.instruction("ldr x1, [sp, #0]"); // restore path ptr for the filesystem helper - emitter.instruction("ldr x2, [sp, #8]"); // restore path len for the filesystem helper - abi::emit_call_label(emitter, "__rt_filesize"); // real filesystem size - emitter.label(&done); - emitter.instruction("add sp, sp, #16"); // release the scratch frame - } - Arch::X86_64 => { - // -- path string: rax = ptr, rdx = len -- - emitter.instruction("sub rsp, 16"); // scratch: [rsp+0] path ptr, [rsp+8] path len - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save path ptr for the filesystem fallback - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save path len for the filesystem fallback - emitter.instruction("mov rdi, rax"); // field helper arg0 = path ptr - emitter.instruction("mov rsi, rdx"); // field helper arg1 = path len - emitter.instruction("xor edx, edx"); // field selector 0 = 'size' - abi::emit_call_label(emitter, "__rt_user_wrapper_url_stat_field"); // rax = wrapper 'size' (or -1) - abi::emit_symbol_address(emitter, "r9", "_url_stat_matched"); // load runtime data address - emitter.instruction("movzx r9d, BYTE PTR [r9]"); // did a registered wrapper scheme match? - emitter.instruction("test r9d, r9d"); // matched flag set? - emitter.instruction(&format!("jz {}", fallback)); // no → real filesystem filesize - emitter.instruction(&format!("jmp {}", done)); // matched: rax already holds the wrapper 'size' - emitter.label(&fallback); - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore path ptr for the filesystem helper - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore path len for the filesystem helper - abi::emit_call_label(emitter, "__rt_filesize"); // real filesystem size - emitter.label(&done); - emitter.instruction("add rsp, 16"); // release the scratch frame - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/filetype.rs b/src/codegen/builtins/io/filetype.rs deleted file mode 100644 index c71bd3ed94..0000000000 --- a/src/codegen/builtins/io/filetype.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Purpose: -//! Emits PHP `filetype` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_string_or_false_result; - -/// Emits code for the PHP `filetype` builtin. -/// -/// Evaluates the path argument, calls `__rt_filetype` to retrieve the filesystem -/// type string (`"file"`, `"dir"`, `"link"`, etc.), boxes the result, and returns -/// `PhpType::Mixed`. On failure (e.g., file not found), emits a PHP `false` sentinel. -/// -/// - **args**: must contain exactly one path expression (checked by caller). -/// - **emitter**: receives the evaluated path load, call to `__rt_filetype`, and box result. -/// - **ctx**: carries variable layout and ownership state through codegen. -/// - **data**: accumulates data-section entries for string literals and metadata. -/// - **Returns**: `Some(PhpType::Mixed)` unconditionally; caller relies on runtime result. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("filetype()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_filetype"); // call the target-aware runtime helper that returns "file"/"dir"/"link"/... - box_stat_string_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/flock.rs b/src/codegen/builtins/io/flock.rs deleted file mode 100644 index 2d83bc1039..0000000000 --- a/src/codegen/builtins/io/flock.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Purpose: -//! Emits PHP `flock` advisory-locking builtin calls over runtime file handles. -//! Validates the stream argument before invoking the libc `flock` wrapper. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The runtime translates the PHP `LOCK_UN` value (3) to the POSIX value (8), -//! preserves `LOCK_NB`, and returns the optional `$would_block` state. - -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits code for the PHP `flock(stream, operation, &$would_block?)` builtin. -/// -/// Validates the stream argument and extracts its file descriptor. Emits the lock -/// operation expression, then places both fd (in x0/rax) and operation (in x1/rdx) -/// into the standard integer argument registers before calling `__rt_flock`. On return, -/// optionally stores the runtime's `$would_block` output into the caller's variable. -/// -/// Returns `PhpType::Bool` unconditionally. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("flock()"); - emit_stream_fd_arg("flock", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the file descriptor while the operation expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the lock operation into the second runtime argument register - abi::emit_pop_reg(emitter, "x0"); // restore the file descriptor into the primary integer register - } - Arch::X86_64 => { - emitter.instruction("mov rdx, rax"); // move the lock operation into the secondary x86_64 integer argument register - abi::emit_pop_reg(emitter, "rax"); // restore the file descriptor into the primary integer register - } - } - // -- user-wrapper synthetic fd path (G1): dispatch into stream_lock -- - // A descriptor >= USER_WRAPPER_FD_BASE is a userspace wrapper handle, so - // flock() must call the wrapper's stream_lock() rather than the libc - // flock() wrapper. PHP does not populate $would_block for userspace - // wrappers, so the wrapper path skips the by-ref store entirely. - let wrapper_label = ctx.next_label("flock_user_wrapper"); - let done_label = ctx.next_label("flock_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper_label)); // dispatch into the wrapper's stream_lock - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper_label)); // dispatch into the wrapper's stream_lock - } - } - abi::emit_call_label(emitter, "__rt_flock"); // call the runtime libc flock(fd, op) wrapper that translates LOCK_UN - if let Some(would_block_arg) = args.get(2) { - emit_store_would_block(would_block_arg, emitter, ctx); - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", done_label)), // skip the wrapper path on the normal-fd result - Arch::X86_64 => emitter.instruction(&format!("jmp {}", done_label)), // skip the wrapper path on the normal-fd result - } - emitter.label(&wrapper_label); - // `__rt_user_wrapper_flock` resolves the wrapper object from the synthetic - // fd and calls stream_lock($operation). Its lookup expects the fd in the - // SysV first-arg register (x0 / rdi) and the operation in the second (x1 / - // rsi). ARM64 already holds fd in x0 and operation in x1; x86_64 left fd in - // rax and operation in rdx (the libc `__rt_flock` convention), so move both - // into the wrapper-call registers first. - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // synthetic fd → wrapper-lookup first-arg register - emitter.instruction("mov rsi, rdx"); // lock operation → wrapper-call second-arg register - } - abi::emit_call_label(emitter, "__rt_user_wrapper_flock"); // call the wrapper's stream_lock($operation) - emitter.label(&done_label); - Some(PhpType::Bool) -} - -/// Emits code to store the `$would_block` output from `__rt_flock` into the variable -/// represented by `arg`. -/// -/// Uses a push/pop cycle to preserve the `flock()` return value across the store. -/// On ARM64 the runtime writes `would_block` to x1; on x86_64 it writes to rdx. -/// In both cases the value is moved to the standard scalar result register (x0/rax) -/// before calling `emit_store_would_block_result`. -fn emit_store_would_block(arg: &Expr, emitter: &mut Emitter, ctx: &mut Context) { - let ExprKind::Variable(name) = &arg.kind else { - return; - }; - - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, "x0"); // preserve flock() return while storing the by-ref would_block output - emitter.instruction("mov x0, x1"); // move would_block into the standard scalar result register for storage - emit_store_would_block_result(name, emitter, ctx); - abi::emit_pop_reg(emitter, "x0"); // restore flock() return after updating would_block - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve flock() return while storing the by-ref would_block output - emitter.instruction("mov rax, rdx"); // move would_block into the standard scalar result register for storage - emit_store_would_block_result(name, emitter, ctx); - abi::emit_pop_reg(emitter, "rax"); // restore flock() return after updating would_block - } - } -} - -/// Stores the `would_block` boolean result into the variable identified by `name`. -/// -/// Resolves the variable's storage location: -/// - **Global variable**: uses `__rt_flock`'s page-relative `would_block` output via a global symbol -/// - **Ref parameter** (passed by reference): loads the parameter's stack address and stores through it -/// - **Local stack variable**: stores directly at the variable's stack offset -/// -/// Updates the variable's type to `PhpType::Int` (0 or 1) and marks it `NonHeap`. -/// Panics if a ref param lacks a stack slot. -fn emit_store_would_block_result(name: &str, emitter: &mut Emitter, ctx: &mut Context) { - if ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)) { - let label = format!("_gvar_{}", name); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", &label); // resolve global would_block storage address - emitter.instruction("str x0, [x9]"); // store would_block into the global slot - } - Arch::X86_64 => { - abi::emit_store_reg_to_symbol(emitter, "rax", &label, 0); // store would_block into the global slot - } - } - } else if ctx.ref_params.contains(name) { - let offset = ctx - .variables - .get(name) - .expect("codegen bug: missing ref-param slot for flock() would_block") - .stack_offset; - match emitter.target.arch { - Arch::AArch64 => { - abi::load_at_offset(emitter, "x9", offset); // load referenced would_block storage address - emitter.instruction("str x0, [x9]"); // store would_block through the referenced slot - } - Arch::X86_64 => { - abi::load_at_offset(emitter, "r11", offset); // load referenced would_block storage address - abi::emit_store_to_address(emitter, "rax", "r11", 0); // store would_block through the referenced slot - } - } - } else if let Some(offset) = ctx.variables.get(name).map(|var| var.stack_offset) { - match emitter.target.arch { - Arch::AArch64 => { - abi::store_at_offset(emitter, "x0", offset); // store would_block in the local variable slot - } - Arch::X86_64 => { - abi::store_at_offset(emitter, "rax", offset); // store would_block in the local variable slot - } - } - ctx.update_var_type_and_ownership(name, PhpType::Int, HeapOwnership::NonHeap); - } -} diff --git a/src/codegen/builtins/io/fnmatch.rs b/src/codegen/builtins/io/fnmatch.rs deleted file mode 100644 index ce39ccd74c..0000000000 --- a/src/codegen/builtins/io/fnmatch.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Purpose: -//! Emits PHP `fnmatch` I/O builtin calls. -//! Marshals PHP values into runtime helpers that interact with files, paths, streams, or stdout. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - I/O helpers are effectful and their false/null failure conventions are part of PHP compatibility. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `fnmatch` PHP builtin call. -/// -/// Evaluates three arguments in source order (pattern, filename, flags), marshaling -/// each as a string pointer/length pair. On ARM64 uses `x1`/`x2` for the pattern and -/// `x3`/`x4` for the filename; on x86_64 uses `rax`/`rdx` and `rdi`/`rsi`. Arguments -/// are preserved on the stack during evaluation to allow correct ordering. Calls the -/// target-aware runtime helper `__rt_fnmatch` and returns `PhpType::Bool`. -/// -/// # Arguments -/// * `_name` — unused, matches the dispatcher signature -/// * `args[0]` — pattern (string) -/// * `args[1]` — filename (string) -/// * `args[2]` — optional flags; defaults to 0 if absent -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fnmatch()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the pattern ptr/len while the filename expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the filename ptr/len while the flags expression is evaluated - if let Some(flags) = args.get(2) { - emit_expr(flags, emitter, ctx, data); - emitter.instruction("mov x5, x0"); // move the runtime flags into the fnmatch helper flag register - } else { - emitter.instruction("mov x5, #0"); // default flags = 0 - } - emitter.instruction("ldp x3, x4, [sp], #16"); // restore the filename ptr/len into the secondary runtime string-argument pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the pattern ptr/len after evaluating the filename expression - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the pattern ptr/len while the filename expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the filename ptr/len while the flags expression is evaluated - if let Some(flags) = args.get(2) { - emit_expr(flags, emitter, ctx, data); - emitter.instruction("mov rcx, rax"); // move the runtime flags into the fnmatch helper flag register - } else { - emitter.instruction("xor ecx, ecx"); // default flags = 0 - } - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the filename ptr/len into the secondary runtime string-argument slots - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the pattern ptr/len after evaluating the filename expression - } - } - abi::emit_call_label(emitter, "__rt_fnmatch"); // call the target-aware runtime helper that performs shell-glob matching - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/fopen.rs b/src/codegen/builtins/io/fopen.rs deleted file mode 100644 index 9df5cfa8d7..0000000000 --- a/src/codegen/builtins/io/fopen.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Purpose: -//! Emits PHP `fopen` file input builtin calls. -//! Coordinates path or stream arguments with runtime helpers that allocate returned strings or arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Failure paths must distinguish PHP false from empty string or empty array results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits the `fopen` builtin call, evaluating arguments in source order before -/// materializing filename and mode in ABI order for `__rt_fopen`. -/// -/// On success, boxes the native file descriptor as a PHP resource (tag 9). On -/// failure (negative descriptor), boxes PHP false (tag 3) to distinguish from -/// empty string or empty array. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen()"); - // The php:// wrapper exposes the standard streams. A statically-known - // php://stdin|stdout|stderr|input|output path resolves to its descriptor - // without touching the filesystem; the mode is still evaluated for effects. - if let ExprKind::StringLiteral(path) = &args[0].kind { - if let Some(fd) = php_standard_stream_fd(path) { - emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - emit_standard_stream_resource(fd, emitter); - return Some(PhpType::Mixed); - } - if let Some(fd) = php_fd_stream(path) { - // php://fd/N opens descriptor N directly. Useful for forwarding - // pre-opened descriptors into the PHP stream layer. - emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - emit_standard_stream_resource(fd, emitter); - return Some(PhpType::Mixed); - } - if is_php_memory_stream(path) { - // php://memory and php://temp are backed by an anonymous temp - // file: a real descriptor, so every fd-based stream builtin - // operates on them unchanged. The mode is evaluated for effects. - emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_tmpfile"); // create the anonymous backing descriptor - box_fopen_result(emitter, ctx); - return Some(PhpType::Mixed); - } - if path.starts_with("php://filter/") { - // php://filter/[read=|write=]/resource= opens the - // underlying resource and attaches a built-in filter to it. - return super::php_filter_stream::emit(_name, args, emitter, ctx, data); - } - if path.starts_with("data://") { - // A data:// URI is decoded at compile time and lowered to a - // readable stream over its payload. - return super::data_stream::emit(args, emitter, ctx, data); - } - if path.starts_with("phar://") { - // A read-mode phar:// entry is read from the archive at compile - // time and lowered to a readable stream over its decoded bytes; - // write modes route to the PHAR write bridge. - return super::phar_stream::emit(args, emitter, ctx, data); - } - if path.starts_with("ftp://") { - // An ftp:// URL is opened through the FTP handshake runtime. - return super::ftp_stream::emit(args, emitter, ctx, data); - } - if path.starts_with("ftps://") { - // ftps:// (RFC 4217 explicit FTP-over-TLS) reuses __rt_ftp_open - // with the _ftp_use_tls flag set; needs elephc-tls at link time. - return super::ftps_stream::emit(args, emitter, ctx, data); - } - if path.starts_with("http://") { - // An http:// URL is opened through the HTTP request runtime. - return super::http_stream::emit(args, emitter, ctx, data); - } - if path.starts_with("https://") { - // An https:// URL is opened through the TLS-secured HTTP runtime; - // the checker has already flagged the program as needing - // -lelephc_tls so the elephc-tls staticlib is linked in. - return super::https_stream::emit(args, emitter, ctx, data); - } - if path.starts_with("compress.zlib://") { - // compress.zlib:// wraps the underlying file with the zlib.inflate - // read filter so reads see decompressed bytes. - return super::compress_zlib_stream::emit(args, emitter, ctx, data); - } - if path.starts_with("compress.bzip2://") { - // compress.bzip2:// slurps + bz2-decompresses the underlying file - // through libbz2's BZ2_bzBuffToBuffDecompress, then dup2's a temp - // fd carrying the decompressed bytes onto the original fd. - return super::compress_bzip2_stream::emit(args, emitter, ctx, data); - } - } - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push filename ptr/len while the mode expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the mode pointer into the secondary runtime string-argument pair - emitter.instruction("mov x4, x2"); // move the mode length into the secondary runtime string-argument pair - emitter.instruction("stp x3, x4, [sp, #-16]!"); // preserve the mode ptr/len while optional args are evaluated - emit_ignored_optional_args(args, emitter, ctx, data); - emitter.instruction("ldp x3, x4, [sp], #16"); // restore the mode ptr/len after optional args - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the filename ptr/len after evaluating later arguments - abi::emit_call_label(emitter, "__rt_fopen_maybe_phar"); // open the file (routes a non-literal phar:// read URL to the runtime phar reader, else __rt_fopen) - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the filename ptr/len while the mode expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the mode pointer into the x86_64 secondary runtime string-argument slot - emitter.instruction("mov rsi, rdx"); // move the mode length into the x86_64 secondary runtime string-argument slot - abi::emit_push_reg_pair(emitter, "rdi", "rsi"); // preserve the mode ptr/len while optional args are evaluated - emit_ignored_optional_args(args, emitter, ctx, data); - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the mode ptr/len after optional args - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the filename ptr/len after evaluating later arguments - abi::emit_call_label(emitter, "__rt_fopen_maybe_phar"); // open the file (routes a non-literal phar:// read URL to the runtime phar reader, else __rt_fopen) - } - } - box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Evaluates the mode argument and any currently-ignored optional fopen arguments. -pub(super) fn emit_mode_and_ignored_optional_args( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_expr(&args[1], emitter, ctx, data); - emit_ignored_optional_args(args, emitter, ctx, data); -} - -/// Evaluates fopen's optional 3rd/4th arguments in source order for side effects. -pub(super) fn emit_ignored_optional_args( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - for arg in &args[2..] { - emit_expr(arg, emitter, ctx, data); - } -} - -/// Maps a `php://` standard-stream URL to its file descriptor. The `php://memory` -/// and `php://temp` streams are handled by [`is_php_memory_stream`]; `php://filter` -/// is handled elsewhere. -fn php_standard_stream_fd(path: &str) -> Option { - match path { - "php://stdin" | "php://input" => Some(0), - "php://stdout" | "php://output" => Some(1), - "php://stderr" => Some(2), - _ => None, - } -} - -/// Recognizes the `php://memory` and `php://temp` in-memory stream URLs. -/// `php://temp` accepts an optional `/maxmemory:N` suffix, which elephc ignores -/// because the stream is always backed by an anonymous temp file. -fn is_php_memory_stream(path: &str) -> bool { - path == "php://memory" || path == "php://temp" || path.starts_with("php://temp/") -} - -/// Recognizes `php://fd/N` URLs and returns the embedded descriptor N. -/// The descriptor is treated as already open — elephc trusts the caller -/// to have prepared it through whatever side channel (e.g. an inherited -/// file descriptor from a parent process or a previously-opened -/// `dup`/`pipe` pair). -fn php_fd_stream(path: &str) -> Option { - let suffix = path.strip_prefix("php://fd/")?; - suffix.parse::().ok() -} - -/// Boxes a well-known descriptor as a PHP stream `resource`. -fn emit_standard_stream_resource(fd: i64, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x1, #{}", fd)); // payload = the standard-stream descriptor - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - } - Arch::X86_64 => { - emitter.instruction(&format!("mov edi, {}", fd)); // payload = the standard-stream descriptor - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - } - } -} - -/// Boxes the fopen result: if `x0`/`rax` is negative, emits PHP false (tag 3, payload 0); -/// otherwise emits a PHP resource (tag 9, descriptor in low word). Uses `__rt_mixed_from_value` -/// via ABI calling convention. -pub(super) fn box_fopen_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("fopen_false"); - let done_label = ctx.next_label("fopen_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did fopen() return a negative descriptor for failure? - emitter.instruction(&format!("b.lt {}", false_label)); // box PHP false when opening the stream failed - emitter.instruction("mov x1, x0"); // move the native stream descriptor into the mixed payload low word - emitter.instruction("mov x2, #0"); // resource mixed payloads do not use a high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the successful stream resource result - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path after a successful open - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for fopen() failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible fopen() failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did fopen() return a negative descriptor for failure? - emitter.instruction(&format!("js {}", false_label)); // box PHP false when opening the stream failed - emitter.instruction("mov rdi, rax"); // move the native stream descriptor into the mixed payload low word - emitter.instruction("xor esi, esi"); // resource mixed payloads do not use a high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the successful stream resource result - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path after a successful open - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for fopen() failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible fopen() failure semantics - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/fpassthru.rs b/src/codegen/builtins/io/fpassthru.rs deleted file mode 100644 index 71bc7263e6..0000000000 --- a/src/codegen/builtins/io/fpassthru.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Purpose: -//! Emits PHP `fpassthru` stream builtin calls over runtime file handles. -//! Validates the stream argument before streaming remaining bytes to stdout. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returns the total number of bytes copied to stdout, or -1 on read failure. -//! - Normal descriptors delegate to `__rt_fpassthru`. Synthetic user-wrapper -//! descriptors (`>= 0x40000000`) use a compiled, feof-gated loop emitted here -//! that reads each chunk through `__rt_fread`, writes it to stdout, and -//! releases it. feof is checked BEFORE each read so the loop never makes the -//! EOF read whose empty `substr` result corrupts the caller's resource cell -//! (see `stream_get_contents` for the full rationale). - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `fpassthru()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fpassthru()"); - emit_stream_fd_arg("fpassthru", &args[0], emitter, ctx, data); - let wrapper_label = ctx.next_label("fpt_wrapper"); - let loop_label = ctx.next_label("fpt_loop"); - let release_eof_label = ctx.next_label("fpt_release_eof"); - let wdone_label = ctx.next_label("fpt_done"); - let done_label = ctx.next_label("fpt_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 in w9 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper_label)); // wrappers stream via the feof-gated loop below - abi::emit_call_label(emitter, "__rt_fpassthru"); // normal fd: runtime helper streams the rest to stdout - emitter.instruction(&format!("b {}", done_label)); // skip the wrapper loop on the normal path - - emitter.label(&wrapper_label); - emitter.instruction("sub sp, sp, #32"); // scratch: [sp,#0]=fd, [sp,#8]=total, [sp,#16]=chunk ptr - emitter.instruction("str x0, [sp, #0]"); // save the synthetic wrapper fd - emitter.instruction("str xzr, [sp, #8]"); // bytes-copied total = 0 - emitter.label(&loop_label); - emitter.instruction("ldr x0, [sp, #0]"); // reload the wrapper fd - abi::emit_call_label(emitter, "__rt_feof"); // check stream_eof FIRST (x0 = 1 at EOF) - emitter.instruction(&format!("cbnz x0, {}", wdone_label)); // at EOF: stop without reading - emitter.instruction("ldr x0, [sp, #0]"); // reload the wrapper fd - emitter.instruction("mov x1, #4096"); // request up to 4096 bytes - abi::emit_call_label(emitter, "__rt_fread"); // x1=chunk ptr, x2=len - emitter.instruction(&format!("cbz x2, {}", release_eof_label)); // defensive: empty read also stops - emitter.instruction("str x1, [sp, #16]"); // save the chunk ptr for the later release - emitter.instruction("ldr x9, [sp, #8]"); // current total - emitter.instruction("add x9, x9, x2"); // add this chunk's length - emitter.instruction("str x9, [sp, #8]"); // store the updated total - emitter.instruction("mov x0, #1"); // fd = stdout (x1=ptr, x2=len already in place) - emitter.syscall(4); // write(1, chunk, len) - emitter.instruction("ldr x0, [sp, #16]"); // reload the chunk ptr - abi::emit_call_label(emitter, "__rt_decref_any"); // release the owned chunk, then loop - emitter.instruction(&format!("b {}", loop_label)); // stream the next chunk - emitter.label(&release_eof_label); - emitter.instruction("mov x0, x1"); // the final (empty/uncopied) owned chunk - abi::emit_call_label(emitter, "__rt_decref_any"); // release it (heap freed; non-heap skipped) - emitter.label(&wdone_label); - emitter.instruction("ldr x0, [sp, #8]"); // return the total bytes copied to stdout - emitter.instruction("add sp, sp, #32"); // release the scratch frame - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper_label)); // wrappers stream via the feof-gated loop below - emitter.instruction("mov rdi, rax"); // normal fd: pass the descriptor to the helper - abi::emit_call_label(emitter, "__rt_fpassthru"); // runtime helper streams the rest to stdout - emitter.instruction(&format!("jmp {}", done_label)); // skip the wrapper loop on the normal path - - emitter.label(&wrapper_label); - emitter.instruction("sub rsp, 32"); // scratch: [rsp+0]=fd, [rsp+8]=total, [rsp+16]=chunk ptr - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the synthetic wrapper fd - emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // bytes-copied total = 0 - emitter.label(&loop_label); - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the wrapper fd - abi::emit_call_label(emitter, "__rt_feof"); // check stream_eof FIRST (rax = 1 at EOF) - emitter.instruction("test rax, rax"); // at EOF? - emitter.instruction(&format!("jnz {}", wdone_label)); // at EOF: stop without reading - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the wrapper fd - emitter.instruction("mov rsi, 4096"); // request up to 4096 bytes - abi::emit_call_label(emitter, "__rt_fread"); // rax=chunk ptr, rdx=len - emitter.instruction("test rdx, rdx"); // zero-length read? - emitter.instruction(&format!("jz {}", release_eof_label)); // defensive: empty read also stops - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // save the chunk ptr for the later release - emitter.instruction("mov r8, QWORD PTR [rsp + 8]"); // current total - emitter.instruction("add r8, rdx"); // add this chunk's length - emitter.instruction("mov QWORD PTR [rsp + 8], r8"); // store the updated total - emitter.instruction("mov rsi, rax"); // buffer = chunk ptr - emitter.instruction("mov edi, 1"); // fd = stdout (rdx=len already in place) - abi::emit_call_label(emitter, "write"); // write(1, chunk, len) via libc - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the chunk ptr - abi::emit_call_label(emitter, "__rt_decref_any"); // release the owned chunk, then loop - emitter.instruction(&format!("jmp {}", loop_label)); // stream the next chunk - emitter.label(&release_eof_label); - abi::emit_call_label(emitter, "__rt_decref_any"); // release the final (empty/uncopied) chunk (rax=ptr) - emitter.label(&wdone_label); - emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // return the total bytes copied to stdout - emitter.instruction("add rsp, 32"); // release the scratch frame - emitter.label(&done_label); - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/fprintf.rs b/src/codegen/builtins/io/fprintf.rs deleted file mode 100644 index f27d441130..0000000000 --- a/src/codegen/builtins/io/fprintf.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! Purpose: -//! Emits PHP `fprintf` calls: formats like `sprintf` and writes the result to a -//! stream descriptor, returning the number of bytes written. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - `fprintf($stream, $format, ...$values)` = `sprintf($format, ...$values)` + -//! `fwrite($stream, $result)`. The values are pushed as 16-byte tagged records -//! (identical to `sprintf`/`printf`) and `__rt_sprintf` pops them on return. -//! - The descriptor is stashed on the stack BELOW the variadic records so it -//! survives `__rt_sprintf`'s record cleanup, then the formatted bytes are sent -//! through `__rt_fwrite` (which applies write filters and dispatches user -//! wrappers, exactly like `fwrite`). - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `fprintf()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fprintf()"); - // args[0] = stream, args[1] = format, args[2..] = values. - let arg_count = args.len() - 2; - - // -- evaluate the stream descriptor and stash it below the variadic records -- - emit_stream_fd_arg("fprintf", &args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("str x0, [sp, #-16]!"), // push the descriptor (survives __rt_sprintf cleanup) - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve a 16-byte slot for the descriptor - emitter.instruction("mov QWORD PTR [rsp], rax"); // stash the descriptor below the variadic records - } - } - - // -- push the format values in reverse as 16-byte tagged records -- - for i in (2..args.len()).rev() { - let ty = emit_expr(&args[i], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => match ty { - PhpType::Int => { - emitter.instruction("str x0, [sp, #-16]!"); // push int value - emitter.instruction("str xzr, [sp, #8]"); // type tag 0 = int - } - PhpType::Float => { - emitter.instruction("fmov x0, d0"); // move float bits to int register - emitter.instruction("str x0, [sp, #-16]!"); // push float bits - emitter.instruction("mov x0, #2"); // type tag 2 = float - emitter.instruction("str x0, [sp, #8]"); // store type tag - } - PhpType::Bool => { - emitter.instruction("str x0, [sp, #-16]!"); // push bool value - emitter.instruction("mov x0, #3"); // type tag 3 = bool - emitter.instruction("str x0, [sp, #8]"); // store type tag - } - PhpType::Str => { - emitter.instruction("str x1, [sp, #-16]!"); // push string pointer - emitter.instruction("lsl x0, x2, #8"); // shift length left by 8 - emitter.instruction("orr x0, x0, #1"); // set type tag bit 0 = str - emitter.instruction("str x0, [sp, #8]"); // store tag|length - } - _ => { - emitter.instruction("str xzr, [sp, #-16]!"); // push zero - emitter.instruction("str xzr, [sp, #8]"); // type tag 0 - } - }, - Arch::X86_64 => match ty { - PhpType::Int => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged record for the integer operand - emitter.instruction("mov QWORD PTR [rsp], rax"); // store the integer payload in the low half - emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // tag the record as an integer operand - } - PhpType::Float => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged record for the floating operand - emitter.instruction("movsd QWORD PTR [rsp], xmm0"); // store the floating bits in the low half - emitter.instruction("mov QWORD PTR [rsp + 8], 2"); // tag the record as a floating operand - } - PhpType::Bool => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged record for the boolean operand - emitter.instruction("mov QWORD PTR [rsp], rax"); // store the boolean payload in the low half - emitter.instruction("mov QWORD PTR [rsp + 8], 3"); // tag the record as a boolean operand - } - PhpType::Str => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged record for the string operand - emitter.instruction("mov QWORD PTR [rsp], rax"); // store the string pointer in the low half - emitter.instruction("mov rcx, rdx"); // copy the string length before packing it - emitter.instruction("shl rcx, 8"); // shift the length into the upper metadata bits - emitter.instruction("or rcx, 1"); // tag the record as a string operand - emitter.instruction("mov QWORD PTR [rsp + 8], rcx"); // store the packed string metadata word - } - _ => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged record for unsupported operands - emitter.instruction("mov QWORD PTR [rsp], 0"); // store a zero payload - emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // tag the record as an integer zero fallback - } - }, - } - } - - // -- evaluate the format string and format through the sprintf runtime -- - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("mov x0, #{}", arg_count)), // number of packed variadic records - Arch::X86_64 => abi::emit_load_int_immediate(emitter, "rdi", arg_count as i64), // number of packed variadic records - } - abi::emit_call_label(emitter, "__rt_sprintf"); // format → ptr+len; pops the caller's packed records - - // -- write the formatted bytes to the stashed descriptor via __rt_fwrite -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp], #16"); // pop the stashed descriptor into the fwrite fd argument (x1=ptr, x2=len) - abi::emit_call_label(emitter, "__rt_fwrite"); // write the payload, applying any attached write filter - } - Arch::X86_64 => { - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // pop the stashed descriptor into the fwrite fd argument - emitter.instruction("add rsp, 16"); // release the descriptor slot - emitter.instruction("mov rsi, rax"); // formatted string pointer → second fwrite argument (rdx=len already in place) - abi::emit_call_label(emitter, "__rt_fwrite"); // write the payload, applying any attached write filter - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/fputcsv.rs b/src/codegen/builtins/io/fputcsv.rs deleted file mode 100644 index b8f7a60155..0000000000 --- a/src/codegen/builtins/io/fputcsv.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Purpose: -//! Emits PHP `fputcsv` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits a PHP `fputcsv(stream, fields, separator, enclosure, escape)` builtin call. -/// -/// Validates `stream` via `emit_stream_fd_arg` to extract a raw file descriptor, -/// then preserves it on the stack while `fields` (args[1]) is evaluated as a -/// string-array expression. After evaluation, the array pointer is moved into the -/// second ABI argument register and the file descriptor is restored to the first -/// register before calling `__rt_fputcsv`. Returns `PhpType::Int` (bytes written -/// or false on failure). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fputcsv()"); - emit_stream_fd_arg("fputcsv", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the file descriptor while the string-array expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the string-array pointer into the second runtime helper argument register - abi::emit_pop_reg(emitter, "x0"); // restore the file descriptor into the first runtime helper argument register - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the string-array pointer into the second SysV fputcsv helper argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the file descriptor into the first SysV fputcsv helper argument register - } - } - abi::emit_call_label(emitter, "__rt_fputcsv"); // write the string array as a CSV line through the target-aware runtime helper - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/fread.rs b/src/codegen/builtins/io/fread.rs deleted file mode 100644 index 5258b34ce6..0000000000 --- a/src/codegen/builtins/io/fread.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Purpose: -//! Emits PHP `fread` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Lowers the PHP `fread` builtin call to target assembly. -/// -/// Arguments: -/// - `args[0]`: stream resource (validated via `emit_stream_fd_arg`) -/// - `args[1]`: byte count expression -/// -/// Emits: -/// - Stream unboxing and fd preservation on stack while length is evaluated -/// - ABI-aligned argument materialization for `__rt_fread` (fd in arg0, length in arg1) -/// - Tail call to `__rt_fread`, which returns an owned PHP string in x0/x1 or x0=0 on error -/// -/// Returns `Some(PhpType::Str)` unconditionally; caller must handle false/null from runtime. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fread()"); - emit_stream_fd_arg("fread", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the file descriptor while the length expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the requested byte count into the fread helper length register - abi::emit_pop_reg(emitter, "x0"); // restore the file descriptor into the fread helper fd register - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the requested byte count into the second SysV fread helper argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the file descriptor into the first SysV fread helper argument register - } - } - abi::emit_call_label(emitter, "__rt_fread"); // read bytes through the target-aware runtime helper and return an elephc string - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/fscanf.rs b/src/codegen/builtins/io/fscanf.rs deleted file mode 100644 index 7dd044547e..0000000000 --- a/src/codegen/builtins/io/fscanf.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Emits PHP `fscanf` calls: reads one line from a stream and parses it with the -//! `sscanf` runtime, returning an array of matched fields. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - `fscanf($stream, $format)` = read a line via `__rt_fgets`, then -//! `__rt_sscanf($line, $format)`. v1 implements the 2-argument array-returning -//! form (the by-ref output-variable form is not supported, mirroring `sscanf`). -//! - `__rt_fgets` returns the line as (ptr, len) in the runtime string registers, -//! exactly the position `__rt_sscanf` expects for its input string, so this -//! reuses `sscanf`'s argument marshaling. -//! - At EOF `__rt_fgets` yields a zero-length line, so `fscanf` returns an empty -//! array rather than PHP's `false`/`-1` (documented v1 divergence). Because the -//! line read goes through `__rt_fgets`, which dispatches synthetic -//! userspace-wrapper descriptors into the wrapper's `stream_read`, `fscanf` works -//! on registered userspace-wrapper handles as well as real descriptors. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `fscanf()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fscanf()"); - // -- read one line from the stream descriptor -- - emit_stream_fd_arg("fscanf", &args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_call_label(emitter, "__rt_fgets"); // read one line: x1=ptr, x2=len - // The line is now the sscanf input string (x1/x2). Marshal it and - // the format string exactly like sscanf(). - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the line while the format string is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // format pointer → secondary runtime string-argument pair - emitter.instruction("mov x4, x2"); // format length → secondary runtime string-argument pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the line into the primary string-argument pair - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the descriptor into the first fgets argument register - abi::emit_call_label(emitter, "__rt_fgets"); // read one line: rax=ptr, rdx=len - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push the line while the format string is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // format pointer → secondary string-argument pair - emitter.instruction("mov rsi, rdx"); // format length → secondary string-argument pair - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the line into the primary string-argument pair - } - } - abi::emit_call_label(emitter, "__rt_sscanf"); // parse the line per the format string into an array of fields - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/io/fseek.rs b/src/codegen/builtins/io/fseek.rs deleted file mode 100644 index 4f2be10732..0000000000 --- a/src/codegen/builtins/io/fseek.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Purpose: -//! Emits PHP `fseek` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits the `fseek(stream, offset, whence)` builtin call. -/// -/// Validates the stream resource and unboxes it to a raw file descriptor, -/// evaluates `offset` and optional `whence` arguments (defaulting to SEEK_SET=0), -/// then calls the platform lseek syscall. On success the stream's EOF flag is -/// cleared before returning 0. On failure returns -1. -/// -/// # Arguments -/// - `_name`: builtin name (unused, always "fseek") -/// - `args`: [stream, offset, whence?] — whence is optional, defaults to 0 (SEEK_SET) -/// - `emitter`: target for emitted assembly -/// - `ctx`: codegen context (labels, target, platform) -/// - `data`: data section for symbols (eof_flags table) -/// -/// # Returns -/// Always `Some(PhpType::Int)` — PHP semantics: 0 on success, -1 on failure. -/// -/// # Side effects -/// - Clobbers caller-saved registers used for syscall argument passing. -/// - Stack: pushes two registers before evaluating offset/whence, pops on completion. -/// - On success: clears the per-fd EOF flag via the `_eof_flags` runtime symbol. -/// -/// # ABI constraints -/// - AArch64: lseek via syscall 199, args in x0 (fd), x1 (offset), x2 (whence). -/// - x86_64: lseek via libc call, args in rdi (fd), rsi (offset), rdx (whence). -/// - Preserves fd on the stack across expression evaluation to handle errors safely. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fseek()"); - emit_stream_fd_arg("fseek", &args[0], emitter, ctx, data); - let success_label = ctx.next_label("fseek_success"); - let done_label = ctx.next_label("fseek_done"); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the file descriptor while the seek offset expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the seek offset while the optional whence expression is evaluated - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - } else { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // default whence = SEEK_SET for the AArch64 lseek path - } - Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // default whence = SEEK_SET for the x86_64 lseek path - } - } - } - let user_wrapper_label = ctx.next_label("fseek_user_wrapper"); - let after_dispatch = ctx.next_label("fseek_after_dispatch"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x2, x0"); // move the whence selector into the third lseek syscall argument register - abi::emit_pop_reg(emitter, "x1"); // restore the seek offset into the second lseek syscall argument register - abi::emit_pop_reg(emitter, "x0"); // restore the file descriptor into the first lseek syscall argument register - // -- user-wrapper synthetic fd path (Phase 10 step 4) -- - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", user_wrapper_label)); // dispatch into the wrapper's stream_seek instead of lseek - abi::emit_push_reg(emitter, "x0"); // preserve fd so successful fseek() can clear its EOF flag - emitter.syscall(199); // reposition the file offset through the platform syscall path - if emitter.platform.needs_cmp_before_error_branch() { - emitter.instruction("cmp x0, #0"); // Linux: negative lseek result means fseek() failed - } - emitter.instruction(&emitter.platform.branch_on_syscall_success(&success_label)); // continue only when lseek succeeded - abi::emit_pop_reg(emitter, "x9"); // discard preserved fd on the fseek() failure path - emitter.instruction("mov x0, #-1"); // fseek() returns -1 on failure - emitter.instruction(&format!("b {}", done_label)); // skip EOF reset after a failed seek - emitter.label(&success_label); - abi::emit_pop_reg(emitter, "x9"); // restore fd for EOF-flag reset after a successful seek - abi::emit_symbol_address(emitter, "x10", "_eof_flags"); - emitter.instruction("strb wzr, [x10, x9]"); // clear EOF because fseek() repositioned the stream - emitter.instruction("mov x0, #0"); // fseek() returns 0 on success - emitter.label(&done_label); - emitter.instruction(&format!("b {}", after_dispatch)); // skip the user-wrapper path on the normal-fd success/failure - emitter.label(&user_wrapper_label); - abi::emit_call_label(emitter, "__rt_user_wrapper_fseek"); // dispatch into the wrapper's stream_seek - emitter.label(&after_dispatch); - } - Arch::X86_64 => { - emitter.instruction("mov rdx, rax"); // move the whence selector into the third SysV lseek() argument register - abi::emit_pop_reg(emitter, "rsi"); // restore the seek offset into the second SysV lseek() argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the file descriptor into the first SysV lseek() argument register - // -- user-wrapper synthetic fd path (Phase 10 step 4) -- - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rdi, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", user_wrapper_label)); // dispatch into the wrapper's stream_seek instead of lseek - abi::emit_push_reg(emitter, "rdi"); // preserve fd so successful fseek() can clear its EOF flag - emitter.instruction("call lseek"); // reposition the file offset through libc lseek() on linux-x86_64 - emitter.instruction("cmp rax, 0"); // did libc lseek() succeed with a non-negative resulting file offset? - emitter.instruction(&format!("jge {}", success_label)); // continue only when fseek() succeeded - abi::emit_pop_reg(emitter, "r10"); // discard preserved fd on the fseek() failure path - emitter.instruction("mov rax, -1"); // fseek() returns -1 on failure - emitter.instruction(&format!("jmp {}", done_label)); // skip EOF reset after a failed seek - emitter.label(&success_label); - abi::emit_pop_reg(emitter, "r10"); // restore fd for EOF-flag reset after a successful seek - abi::emit_symbol_address(emitter, "r11", "_eof_flags"); // materialize the eof-flag table for fseek() - emitter.instruction("mov BYTE PTR [r11 + r10], 0"); // clear EOF because fseek() repositioned the stream - emitter.instruction("xor eax, eax"); // fseek() returns 0 on success - emitter.label(&done_label); - emitter.instruction(&format!("jmp {}", after_dispatch)); // skip the user-wrapper path on the normal-fd success/failure - emitter.label(&user_wrapper_label); - abi::emit_call_label(emitter, "__rt_user_wrapper_fseek"); // dispatch into the wrapper's stream_seek - emitter.label(&after_dispatch); - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/fsockopen.rs b/src/codegen/builtins/io/fsockopen.rs deleted file mode 100644 index 28d6f76ad4..0000000000 --- a/src/codegen/builtins/io/fsockopen.rs +++ /dev/null @@ -1,229 +0,0 @@ -//! Purpose: -//! Emits PHP `fsockopen` calls. -//! Opens a connected TCP socket to a host/port pair and yields it as a PHP -//! stream resource, writing the optional by-reference error outputs. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Signature `fsockopen(hostname, port, &error_code, &error_message, timeout)`. -//! The hostname string and port integer are handed to `__rt_fsockopen`, which -//! builds the `tcp://host:port` address and connects through -//! `__rt_stream_socket_client`. -//! - On success `&$error_code` is set to 0 and `&$error_message` to the empty -//! string; on failure they are set to a generic connection error. The stores -//! dispatch on the variable's storage class (global / by-ref param / local), -//! matching the `stream_socket_recvfrom` write-back pattern. -//! - v1's documented limitation: the `$timeout` argument is evaluated for its -//! side effects but the connection uses the OS default connect timeout. - -use crate::codegen::builtins::io::stream_socket_server::box_socket_result; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits codegen for PHP `fsockopen()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fsockopen()"); - // PHP evaluates the value arguments left to right: hostname, port, then the - // timeout. The error-code/message arguments are by-reference write targets, - // so they are not evaluated as values here. - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the hostname string - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, "x0"); // preserve the port - if args.len() >= 5 { - emit_expr(&args[4], emitter, ctx, data); - } - abi::emit_pop_reg(emitter, "x9"); // restore the port - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the hostname - emitter.instruction("mov x0, x1"); // arg 0 = hostname pointer - emitter.instruction("mov x1, x2"); // arg 1 = hostname length - emitter.instruction("mov x2, x9"); // arg 2 = port - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the hostname string - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, "rax"); // preserve the port - if args.len() >= 5 { - emit_expr(&args[4], emitter, ctx, data); - } - abi::emit_pop_reg(emitter, "r8"); // restore the port - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the hostname - emitter.instruction("mov rdx, r8"); // arg 2 = port - } - } - abi::emit_call_label(emitter, "__rt_fsockopen"); - emit_error_outputs(args, emitter, ctx, data); - box_socket_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Writes the by-reference `&$error_code` / `&$error_message` outputs from the -/// connection result held in the integer result register. On success the code -/// is 0 and the message empty; on failure a generic connection error is -/// reported. The result register is preserved across the stores. -fn emit_error_outputs(args: &[Expr], emitter: &mut Emitter, ctx: &mut Context, data: &mut DataSection) { - let errno_var = variable_name(args.get(2)); - let errstr_var = variable_name(args.get(3)); - if errno_var.is_none() && errstr_var.is_none() { - return; - } - let (empty_sym, _) = data.add_string(b""); - let (msg_sym, msg_len) = data.add_string(b"Connection refused"); - // The connect failure is not classified; report the platform's - // ECONNREFUSED generically (the common cause). - let econnrefused = emitter.platform.econnrefused(); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, "x0"); // preserve the connection result - emitter.instruction("cmp x0, #0"); // did the connection succeed (fd >= 0)? - emitter.instruction("mov x9, #0"); // success error code = 0 - emitter.instruction(&format!("mov x10, #{}", econnrefused)); // failure error code = ECONNREFUSED - emitter.instruction("csel x9, x9, x10, ge"); // x9 = error code for the outcome - abi::emit_symbol_address(emitter, "x10", &msg_sym); - abi::emit_symbol_address(emitter, "x11", &empty_sym); - emitter.instruction("csel x10, x11, x10, ge"); // x10 = error-message pointer - emitter.instruction("mov x11, #0"); // success error-message length = 0 - emitter.instruction(&format!("mov x12, #{}", msg_len)); // failure error-message length - emitter.instruction("csel x11, x11, x12, ge"); // x11 = error-message length - if let Some(name) = errno_var { - store_int(name, "x9", emitter, ctx); - } - if let Some(name) = errstr_var { - store_str(name, "x10", "x11", emitter, ctx); - } - abi::emit_pop_reg(emitter, "x0"); // restore the connection result - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the connection result - emitter.instruction("cmp rax, 0"); // did the connection succeed (fd >= 0)? - emitter.instruction(&format!("mov r9, {}", econnrefused)); // failure error code = ECONNREFUSED - emitter.instruction("mov r10, 0"); // success error code = 0 - emitter.instruction("cmovge r9, r10"); // r9 = error code for the outcome - abi::emit_symbol_address(emitter, "r10", &msg_sym); // failure error-message pointer - abi::emit_symbol_address(emitter, "r11", &empty_sym); // success error-message pointer - emitter.instruction("cmovge r10, r11"); // r10 = error-message pointer - emitter.instruction(&format!("mov r11, {}", msg_len)); // failure error-message length - emitter.instruction("mov rcx, 0"); // success error-message length = 0 - emitter.instruction("cmovge r11, rcx"); // r11 = error-message length - if let Some(name) = errno_var { - store_int(name, "r9", emitter, ctx); - } - if let Some(name) = errstr_var { - store_str(name, "r10", "r11", emitter, ctx); - } - abi::emit_pop_reg(emitter, "rax"); // restore the connection result - } - } -} - -/// Returns the variable name when `arg` is a plain `$variable` expression. -fn variable_name(arg: Option<&Expr>) -> Option<&str> { - match arg.map(|a| &a.kind) { - Some(ExprKind::Variable(name)) => Some(name.as_str()), - _ => None, - } -} - -/// Stores a scalar register into a variable's 8-byte slot, dispatching on the -/// variable's storage class. -fn store_int(name: &str, value_reg: &str, emitter: &mut Emitter, ctx: &Context) { - let is_global = - ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)); - if is_global { - let label = format!("_gvar_{}", name); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x13", &label); // load page of the error-code variable - emitter.instruction(&format!("str {}, [x13]", value_reg)); // store the error code - } - Arch::X86_64 => { - abi::emit_store_reg_to_symbol(emitter, value_reg, &label, 0); // store the error code - } - } - return; - } - if ctx.ref_params.contains(name) { - let offset = ctx - .variables - .get(name) - .expect("codegen bug: missing ref-param slot for fsockopen error output") - .stack_offset; - match emitter.target.arch { - Arch::AArch64 => { - abi::load_at_offset(emitter, "x13", offset); // load the referenced error-code storage - emitter.instruction(&format!("str {}, [x13]", value_reg)); // store the error code - } - Arch::X86_64 => { - abi::load_at_offset(emitter, "r13", offset); // load the referenced error-code storage - abi::emit_store_to_address(emitter, value_reg, "r13", 0); // store the error code - } - } - return; - } - if let Some(offset) = ctx.variables.get(name).map(|var| var.stack_offset) { - abi::store_at_offset(emitter, value_reg, offset); // store the error code into the local slot - } -} - -/// Stores a string pointer/length pair into a variable's 16-byte string slot, -/// dispatching on the variable's storage class. -fn store_str(name: &str, ptr_reg: &str, len_reg: &str, emitter: &mut Emitter, ctx: &Context) { - let is_global = - ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)); - if is_global { - let label = format!("_gvar_{}", name); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x13", &label); // load page of the error-message variable - emitter.instruction(&format!("str {}, [x13]", ptr_reg)); // store the error-message pointer - emitter.instruction(&format!("str {}, [x13, #8]", len_reg)); // store the error-message length - } - Arch::X86_64 => { - abi::emit_store_reg_to_symbol(emitter, ptr_reg, &label, 0); // store the error-message pointer - abi::emit_store_reg_to_symbol(emitter, len_reg, &label, 8); // store the error-message length - } - } - return; - } - if ctx.ref_params.contains(name) { - let offset = ctx - .variables - .get(name) - .expect("codegen bug: missing ref-param slot for fsockopen error output") - .stack_offset; - match emitter.target.arch { - Arch::AArch64 => { - abi::load_at_offset(emitter, "x13", offset); // load the referenced error-message storage - emitter.instruction(&format!("str {}, [x13]", ptr_reg)); // store the error-message pointer - emitter.instruction(&format!("str {}, [x13, #8]", len_reg)); // store the error-message length - } - Arch::X86_64 => { - abi::load_at_offset(emitter, "r13", offset); // load the referenced error-message storage - abi::emit_store_to_address(emitter, ptr_reg, "r13", 0); // store the error-message pointer - abi::emit_store_to_address(emitter, len_reg, "r13", 8); // store the error-message length - } - } - return; - } - if let Some(offset) = ctx.variables.get(name).map(|var| var.stack_offset) { - // A local string slot keeps the pointer at `offset` and the length at - // `offset - 8`, matching `abi::emit_store`/`emit_load` for `PhpType::Str`. - abi::store_at_offset(emitter, ptr_reg, offset); // store the error-message pointer - abi::store_at_offset(emitter, len_reg, offset - 8); // store the error-message length - } -} diff --git a/src/codegen/builtins/io/fstat.rs b/src/codegen/builtins/io/fstat.rs deleted file mode 100644 index b505b23cf7..0000000000 --- a/src/codegen/builtins/io/fstat.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Purpose: -//! Emits PHP `fstat` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_array_or_false_result; -use super::stream_arg::emit_stream_fd_arg; - -/// Emits the `fstat` builtin call. -/// -/// Unboxes the stream resource in `args[0]` to extract the raw file descriptor. -/// A synthetic user-wrapper fd (`>= 0x40000000`) dispatches into -/// `__rt_user_wrapper_fstat`, which invokes the wrapper's `stream_stat()` and -/// returns its boxed Mixed stat array (or boxed `false`) directly. A normal fd -/// calls `__rt_fstat_array` to build a PHP-compatible fstat array and boxes the -/// result. Either path leaves a boxed Mixed in the int-result register, so the -/// builtin returns `PhpType::Mixed` (an array or `false`). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fstat()"); - emit_stream_fd_arg("fstat", &args[0], emitter, ctx, data); - let wrapper_label = ctx.next_label("fstat_user_wrapper"); - let after_dispatch = ctx.next_label("fstat_after_dispatch"); - match emitter.target.arch { - Arch::AArch64 => { - // -- user-wrapper synthetic fd path -- - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper_label)); // dispatch into the wrapper's stream_stat instead of fstat - abi::emit_call_label(emitter, "__rt_fstat_array"); // normal fd: build the PHP-compatible fstat array from the platform stat - box_stat_array_or_false_result(emitter, ctx); // box the raw stat array (or false) into a Mixed cell - emitter.instruction(&format!("b {}", after_dispatch)); // skip the user-wrapper path for normal fds - emitter.label(&wrapper_label); - abi::emit_call_label(emitter, "__rt_user_wrapper_fstat"); // wrapper fd: dispatch stream_stat, result already a boxed Mixed - emitter.label(&after_dispatch); - } - Arch::X86_64 => { - // -- user-wrapper synthetic fd path -- - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper_label)); // dispatch into the wrapper's stream_stat instead of fstat - abi::emit_call_label(emitter, "__rt_fstat_array"); // normal fd: build the PHP-compatible fstat array from the platform stat - box_stat_array_or_false_result(emitter, ctx); // box the raw stat array (or false) into a Mixed cell - emitter.instruction(&format!("jmp {}", after_dispatch)); // skip the user-wrapper path for normal fds - emitter.label(&wrapper_label); - emitter.instruction("mov rdi, rax"); // the wrapper helper's handle lookup expects the synthetic fd in rdi - abi::emit_call_label(emitter, "__rt_user_wrapper_fstat"); // wrapper fd: dispatch stream_stat, result already a boxed Mixed - emitter.label(&after_dispatch); - } - } - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/fsync.rs b/src/codegen/builtins/io/fsync.rs deleted file mode 100644 index 5befc1d956..0000000000 --- a/src/codegen/builtins/io/fsync.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Purpose: -//! Emits PHP `fsync` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits a call to the `fsync` runtime helper for the given file stream. -/// -/// Unboxes the stream resource via `emit_stream_fd_arg` to extract its raw file -/// descriptor, then emits a call to `__rt_fsync` which wraps the libc `fsync(fd)` -/// call. Returns `PhpType::Bool` to reflect PHP's synchronous operation semantics. -/// -/// # Arguments -/// - `args[0]` must be a valid stream resource; the function indexes without bounds -/// checking and relies on the type checker to validate argument count. -/// - `ctx` carries variable layout and ownership state; `emitter` receives the -/// generated assembly; `data` holds runtime data section entries. -/// -/// # Returns -/// `Some(PhpType::Bool)` — fsync always returns a boolean in PHP. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fsync()"); - emit_stream_fd_arg("fsync", &args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_fsync"); // libc fsync(fd) wrapper - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/ftell.rs b/src/codegen/builtins/io/ftell.rs deleted file mode 100644 index 84a1bed1ad..0000000000 --- a/src/codegen/builtins/io/ftell.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Purpose: -//! Emits PHP `ftell` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits PHP `ftell()` which returns the current file position offset. -/// -/// # Arguments -/// - `args[0]`: PHP stream resource to query. -/// -/// # Behavior -/// Unboxes the stream resource to extract its file descriptor, then issues a -/// `lseek(fd, 0, SEEK_CUR)` syscall/libc call to retrieve the current position. -/// Returns `i64` (PhpType::Int) representing bytes from the start of the file. -/// -/// # ABI (ARM64) -/// - `emit_stream_fd_arg` places the fd in `x0`. -/// - `lseek` syscall (#199) uses `x0`=fd, `x1`=offset(0), `x2`=whence(SEEK_CUR=1). -/// - Result returned in `x0`. -/// -/// # ABI (x86_64) -/// - `emit_stream_fd_arg` leaves fd in `rax` after stream unboxing. -/// - Libc `lseek(rdi, rsi, rdx)` uses `rdi`=fd, `rsi`=offset(0), `rdx`=whence(SEEK_CUR=1). -/// - Result returned in `rax`. -/// -/// # PHP semantics -/// - Returns `false` on error (invalid stream, not seekable). Codegen does not -/// model PHP error/false propagation here — caller handles type/warnings. -/// - Position is a non-negative integer; -1 indicates error. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ftell()"); - emit_stream_fd_arg("ftell", &args[0], emitter, ctx, data); - let user_wrapper_label = ctx.next_label("ftell_user_wrapper"); - let after_dispatch = ctx.next_label("ftell_after_dispatch"); - match emitter.target.arch { - Arch::AArch64 => { - // -- user-wrapper synthetic fd path (Phase 10 step 4) -- - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", user_wrapper_label)); // dispatch into the wrapper's stream_tell instead of lseek - emitter.instruction("mov x1, #0"); // offset = 0 for the AArch64 ftell() lseek syscall - emitter.instruction("mov x2, #1"); // whence = SEEK_CUR for the AArch64 ftell() lseek syscall - emitter.syscall(199); // ask the kernel for the current file position through lseek() - emitter.instruction(&format!("b {}", after_dispatch)); // skip the user-wrapper path on the normal-fd success/failure - emitter.label(&user_wrapper_label); - abi::emit_call_label(emitter, "__rt_user_wrapper_ftell"); // dispatch into the wrapper's stream_tell - emitter.label(&after_dispatch); - } - Arch::X86_64 => { - // -- user-wrapper synthetic fd path (Phase 10 step 4) -- - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", user_wrapper_label)); // dispatch into the wrapper's stream_tell instead of lseek - emitter.instruction("mov rdi, rax"); // move the file descriptor into the first SysV lseek() argument register - emitter.instruction("xor esi, esi"); // offset = 0 for the linux-x86_64 ftell() lseek() call - emitter.instruction("mov edx, 1"); // whence = SEEK_CUR for the linux-x86_64 ftell() lseek() call - emitter.instruction("call lseek"); // ask libc lseek() for the current file position on linux-x86_64 - emitter.instruction(&format!("jmp {}", after_dispatch)); // skip the user-wrapper path on the normal-fd success/failure - emitter.label(&user_wrapper_label); - emitter.instruction("mov rdi, rax"); // move the synthetic fd into the first SysV arg register for the wrapper helper - abi::emit_call_label(emitter, "__rt_user_wrapper_ftell"); // dispatch into the wrapper's stream_tell - emitter.label(&after_dispatch); - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/ftp_stream.rs b/src/codegen/builtins/io/ftp_stream.rs deleted file mode 100644 index 3a744850b6..0000000000 --- a/src/codegen/builtins/io/ftp_stream.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Purpose: -//! Lowers `fopen()` calls whose path is an `ftp://` URL. -//! Parses the URL at compile time and opens the file through the -//! `__rt_ftp_open` runtime helper, which performs the FTP handshake. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `ftp://`. -//! -//! Key details: -//! - The URL must be a string literal: `ftp://[user[:pass]@]host[:port]/path`. -//! v1 logs in anonymously and reads in binary (`TYPE I`) passive mode, so any -//! `user:pass@` credentials in the URL are ignored. -//! - The control address (`tcp://host:port`) and the `RETR` command line are -//! built at compile time and handed to `__rt_ftp_open`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits a `fopen("ftp://...", ...)` call. The path is known to be a string -/// literal beginning with `ftp://`. -pub fn emit( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() ftp:// stream"); - // The mode and optional fopen args are evaluated for side effects; - // ftp:// streams are read-only. - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - emit_open_fd(args, emitter, data); - super::fopen::box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Emits the `ftp://` open, leaving the data-connection fd (or -1 on an -/// unparseable URL) in the int-result register. Does NOT evaluate a mode -/// argument or box the result — shared by `fopen()` and `file_get_contents()`. -pub(super) fn emit_open_fd(args: &[Expr], emitter: &mut Emitter, data: &mut DataSection) { - let parsed = match &args[0].kind { - ExprKind::StringLiteral(url) => parse_ftp_url(url), - _ => None, - }; - match parsed { - Some((ctrl_addr, retr_cmd)) => { - let (ctrl_sym, ctrl_len) = data.add_string(ctrl_addr.as_bytes()); - let (retr_sym, retr_len) = data.add_string(retr_cmd.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x0", &ctrl_sym); - emitter.instruction(&format!("mov x1, #{}", ctrl_len)); // control address length - abi::emit_symbol_address(emitter, "x2", &retr_sym); - emitter.instruction(&format!("mov x3, #{}", retr_len)); // RETR command length - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &ctrl_sym); - emitter.instruction(&format!("mov rsi, {}", ctrl_len)); // control address length - abi::emit_symbol_address(emitter, "rdx", &retr_sym); - emitter.instruction(&format!("mov rcx, {}", retr_len)); // RETR command length - } - } - abi::emit_call_label(emitter, "__rt_ftp_open"); // run the FTP handshake and open the file - } - None => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // unparseable ftp:// URL lowers to PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // unparseable ftp:// URL lowers to PHP false - }, - } -} - -/// Parses an `ftp://[user[:pass]@]host[:port]/path` URL into the control -/// address (`tcp://host:port`) and the `RETR ` command line. Returns -/// `None` when the URL has no path component. -fn parse_ftp_url(url: &str) -> Option<(String, String)> { - let rest = url.strip_prefix("ftp://")?; - // v1 logs in anonymously; drop any user:pass@ userinfo before the host. - let after_userinfo = match rest.find('@') { - Some(at) => &rest[at + 1..], - None => rest, - }; - let slash = after_userinfo.find('/')?; - let authority = &after_userinfo[..slash]; - let path = &after_userinfo[slash..]; - if authority.is_empty() || path.len() < 2 { - return None; - } - let (host, port) = match authority.rfind(':') { - Some(colon) => (&authority[..colon], &authority[colon + 1..]), - None => (authority, "21"), - }; - if host.is_empty() || port.is_empty() || !port.bytes().all(|b| b.is_ascii_digit()) { - return None; - } - Some(( - format!("tcp://{}:{}", host, port), - format!("RETR {}\r\n", path), - )) -} diff --git a/src/codegen/builtins/io/ftps_stream.rs b/src/codegen/builtins/io/ftps_stream.rs deleted file mode 100644 index 0f2ae2260e..0000000000 --- a/src/codegen/builtins/io/ftps_stream.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Purpose: -//! Lowers `fopen()` calls whose path is an `ftps://` URL (RFC 4217 explicit -//! FTP over TLS). Parses the URL at compile time, sets `_ftp_use_tls = 1` -//! so the runtime helper performs the AUTH TLS handshake, then dispatches -//! into the standard `__rt_ftp_open` flow. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `ftps://`. -//! -//! Key details: -//! - The URL must be a string literal: `ftps://[user[:pass]@]host[:port]/path`. -//! v1 logs in anonymously (binary, passive). The default port is 21 — the -//! "explicit" RFC 4217 mode where the client connects in cleartext and -//! upgrades via `AUTH TLS`. (PHP also accepts implicit ftps on port 990, -//! but elephc doesn't implement that v1.) -//! - The runtime helper attaches elephc-tls to both the control fd (after -//! `AUTH TLS`) and the PASV data fd, so subsequent `fread` automatically -//! routes through TLS via the `_tls_sessions` table. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits codegen for PHP `ftps_stream()` stream and I/O builtin calls. -pub fn emit( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() ftps:// stream"); - // The mode and optional fopen args are evaluated for side effects; - // ftps:// streams are read-only. - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - emit_open_fd(args, emitter, data); - super::fopen::box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Emits the `ftps://` open (publishing the TLS fn-pointers and flagging the -/// AUTH-TLS handshake), leaving the data-connection fd (or -1 on an unparseable -/// URL) in the int-result register. Does NOT evaluate a mode argument or box the -/// result — shared by `fopen()` and `file_get_contents()`. -pub(super) fn emit_open_fd(args: &[Expr], emitter: &mut Emitter, data: &mut DataSection) { - let parsed = match &args[0].kind { - ExprKind::StringLiteral(url) => parse_ftps_url(url), - _ => None, - }; - match parsed { - Some((ctrl_addr, retr_cmd)) => { - let (ctrl_sym, ctrl_len) = data.add_string(ctrl_addr.as_bytes()); - let (retr_sym, retr_len) = data.add_string(retr_cmd.as_bytes()); - // Publish the elephc-tls C entries into their runtime slots so the - // ftp helper's AUTH-TLS path can route through them. - super::https_stream::publish_tls_function_pointers(emitter); - match emitter.target.arch { - Arch::AArch64 => { - // Set _ftp_use_tls = 1 so __rt_ftp_open does the AUTH TLS - // dance, PBSZ 0 / PROT P, and TLS-attaches both channels. - abi::emit_symbol_address(emitter, "x9", "_ftp_use_tls"); - emitter.instruction("mov x10, #1"); // flag the next FTP open as AUTH-TLS - emitter.instruction("str x10, [x9]"); // publish the AUTH-TLS flag for __rt_ftp_open - abi::emit_symbol_address(emitter, "x0", &ctrl_sym); - emitter.instruction(&format!("mov x1, #{}", ctrl_len)); // control address length - abi::emit_symbol_address(emitter, "x2", &retr_sym); - emitter.instruction(&format!("mov x3, #{}", retr_len)); // RETR command length - } - Arch::X86_64 => { - abi::emit_store_imm_to_symbol(emitter, "_ftp_use_tls", 0, 1); // publish the AUTH-TLS flag for __rt_ftp_open - abi::emit_symbol_address(emitter, "rdi", &ctrl_sym); - emitter.instruction(&format!("mov rsi, {}", ctrl_len)); // control address length - abi::emit_symbol_address(emitter, "rdx", &retr_sym); - emitter.instruction(&format!("mov rcx, {}", retr_len)); // RETR command length - } - } - abi::emit_call_label(emitter, "__rt_ftp_open"); // run the FTP+TLS handshake - } - None => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // unparseable ftps:// URL lowers to PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // unparseable ftps:// URL lowers to PHP false - }, - } -} - -/// Parses an `ftps://[user[:pass]@]host[:port]/path` URL. Same shape as the -/// plain `ftp://` parser; only the prefix and default port behaviour differ. -fn parse_ftps_url(url: &str) -> Option<(String, String)> { - let rest = url.strip_prefix("ftps://")?; - let after_userinfo = match rest.find('@') { - Some(at) => &rest[at + 1..], - None => rest, - }; - let slash = after_userinfo.find('/')?; - let authority = &after_userinfo[..slash]; - let path = &after_userinfo[slash..]; - if authority.is_empty() || path.len() < 2 { - return None; - } - let (host, port) = match authority.rfind(':') { - Some(colon) => (&authority[..colon], &authority[colon + 1..]), - None => (authority, "21"), - }; - if host.is_empty() || port.is_empty() || !port.bytes().all(|b| b.is_ascii_digit()) { - return None; - } - Some(( - format!("tcp://{}:{}", host, port), - format!("RETR {}\r\n", path), - )) -} diff --git a/src/codegen/builtins/io/ftruncate.rs b/src/codegen/builtins/io/ftruncate.rs deleted file mode 100644 index fe1527ff3e..0000000000 --- a/src/codegen/builtins/io/ftruncate.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Purpose: -//! Emits PHP `ftruncate` builtin calls that resize an open file handle. -//! Validates the stream argument and forwards to the libc `ftruncate` runtime -//! helper, or to the userspace wrapper's `stream_truncate()` for synthetic fds. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Truncation length is materialized into the second integer argument register -//! following the platform ABI before the runtime call. -//! - A descriptor `>= USER_WRAPPER_FD_BASE` (0x40000000) is a userspace wrapper -//! handle, so the call is routed to `__rt_user_wrapper_ftruncate` instead. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits code for the PHP `ftruncate(stream, size)` builtin. -/// -/// Validates the stream argument and extracts its file descriptor into the primary -/// integer register. Evaluates the size expression and moves it into the second -/// integer argument register per the platform ABI. A normal fd calls the libc -/// `__rt_ftruncate`; a synthetic userspace-wrapper fd (`>= 0x40000000`) is routed -/// to `__rt_user_wrapper_ftruncate`, which invokes the wrapper's `stream_truncate`. -/// -/// Returns `PhpType::Bool` unconditionally. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ftruncate()"); - emit_stream_fd_arg("ftruncate", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the file descriptor while the size expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the truncation length into the second runtime argument register - abi::emit_pop_reg(emitter, "x0"); // restore the file descriptor into the primary integer register - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the truncation length into the second runtime argument register - abi::emit_pop_reg(emitter, "rax"); // restore the file descriptor into the primary integer register - } - } - // -- user-wrapper synthetic fd path (G1): dispatch into stream_truncate -- - // A descriptor >= USER_WRAPPER_FD_BASE is a userspace wrapper handle, so - // ftruncate() must call the wrapper's stream_truncate() rather than the - // libc ftruncate() syscall (which would fail on the synthetic fd). - let wrapper_label = ctx.next_label("ftruncate_user_wrapper"); - let done_label = ctx.next_label("ftruncate_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper_label)); // dispatch into the wrapper's stream_truncate - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // load USER_WRAPPER_FD_BASE for the synthetic-fd comparison - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper_label)); // dispatch into the wrapper's stream_truncate - } - } - abi::emit_call_label(emitter, "__rt_ftruncate"); // call the libc ftruncate(fd, size) wrapper on a normal fd - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", done_label)), // skip the wrapper path on the normal-fd result - Arch::X86_64 => emitter.instruction(&format!("jmp {}", done_label)), // skip the wrapper path on the normal-fd result - } - emitter.label(&wrapper_label); - // `__rt_user_wrapper_ftruncate` resolves the wrapper object from the - // synthetic fd and calls stream_truncate($new_size). Its lookup expects the - // fd in the SysV first-arg register (x0 / rdi) and the size in the second - // (x1 / rsi). ARM64 already holds fd in x0 and size in x1; x86_64 left fd in - // rax (the size is already in rsi), so move the fd into rdi first. - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // synthetic fd → wrapper-lookup first-arg register - } - abi::emit_call_label(emitter, "__rt_user_wrapper_ftruncate"); // call the wrapper's stream_truncate($new_size) - emitter.label(&done_label); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/fwrite.rs b/src/codegen/builtins/io/fwrite.rs deleted file mode 100644 index a8e8540a29..0000000000 --- a/src/codegen/builtins/io/fwrite.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Emits PHP `fwrite` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits a PHP `fwrite` call by unboxing the stream resource to a raw file descriptor, -/// evaluating the data string expression, then invoking the platform `write` syscall -/// (ARM64) or libc `write()` function (X86_64). The file descriptor is saved across -/// the data expression evaluation to avoid register conflicts. -/// -/// # Arguments -/// * `_name` — unused, matches the builtin dispatcher signature -/// * `args[0]` — stream resource; must be a valid open file handle -/// * `args[1]` — string data to write -/// * `emitter` — target-specific assembly emitter -/// * `ctx` — codegen context (used by `emit_stream_fd_arg`) -/// * `data` — data section for relocations and constants -/// -/// # Returns -/// Always `Some(PhpType::Int)` (bytes written), matching PHP `fwrite` semantics. -/// -/// # Platform behavior -/// * ARM64: pushes fd to stack, evaluates data into x0, restores fd from stack, invokes syscall 4 -/// * X86_64: preserves fd in rax, evaluates data into rax, moves to rdi/rsi for libc write() -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fwrite()"); - emit_stream_fd_arg("fwrite", &args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // push the file descriptor while the data expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("ldr x0, [sp], #16"); // restore the file descriptor into the first __rt_fwrite argument register - abi::emit_call_label(emitter, "__rt_fwrite"); // write the payload, applying any attached write filter - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the file descriptor while the data expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - abi::emit_pop_reg(emitter, "rdi"); // restore the file descriptor into the first __rt_fwrite argument register - emitter.instruction("mov rsi, rax"); // move the elephc string pointer into the second __rt_fwrite argument register - abi::emit_call_label(emitter, "__rt_fwrite"); // write the payload, applying any attached write filter - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/getcwd.rs b/src/codegen/builtins/io/getcwd.rs deleted file mode 100644 index 41704c9749..0000000000 --- a/src/codegen/builtins/io/getcwd.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Purpose: -//! Emits PHP `getcwd` I/O builtin calls. -//! Marshals PHP values into runtime helpers that interact with files, paths, streams, or stdout. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - I/O helpers are effectful and their false/null failure conventions are part of PHP compatibility. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `__rt_getcwd` runtime helper for PHP's `getcwd()` function. -/// -/// `getcwd()` takes no arguments; the function name and argument list are ignored. -/// Returns `Some(PhpType::Str)` on success, or `None` if the current working directory -/// cannot be determined (the runtime helper handles the false/null failure convention). -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("getcwd()"); - abi::emit_call_label(emitter, "__rt_getcwd"); // call the target-aware runtime helper that returns the current working directory as an owned string - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/gethostbyaddr.rs b/src/codegen/builtins/io/gethostbyaddr.rs deleted file mode 100644 index 56533233dd..0000000000 --- a/src/codegen/builtins/io/gethostbyaddr.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Purpose: -//! Emits PHP `gethostbyaddr` calls. -//! Reverse-resolves an IPv4 address to a host name. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Evaluates the address argument into the string result registers and -//! delegates to `__rt_gethostbyaddr`. The helper returns a null pointer for a -//! malformed address, which is boxed as PHP `false`; a found host name (or -//! the unchanged address when no record exists) is boxed as a string. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `gethostbyaddr()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("gethostbyaddr()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_gethostbyaddr"); - box_string_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("gethostbyaddr_false"); - let done_label = ctx.next_label("gethostbyaddr_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means a malformed address - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means a malformed address - emitter.instruction(&format!("jz {}", false_label)); // box false for a malformed address - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/gethostbyname.rs b/src/codegen/builtins/io/gethostbyname.rs deleted file mode 100644 index d168ee05ab..0000000000 --- a/src/codegen/builtins/io/gethostbyname.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Purpose: -//! Emits PHP `gethostbyname` calls. -//! Resolves a host name to its IPv4 address string. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Evaluates the host-name argument into the string result registers and -//! delegates to `__rt_gethostbyname`, which resolves and renders the address -//! or returns the host name unchanged when resolution fails. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `gethostbyname()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("gethostbyname()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_gethostbyname"); - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/gethostname.rs b/src/codegen/builtins/io/gethostname.rs deleted file mode 100644 index 242d995c99..0000000000 --- a/src/codegen/builtins/io/gethostname.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Purpose: -//! Emits PHP `gethostname` calls. -//! Returns the system host name as an elephc string. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Delegates to the `__rt_gethostname` runtime helper, which leaves the host -//! name in the standard string pointer/length result registers. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `gethostname()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("gethostname()"); - abi::emit_call_label(emitter, "__rt_gethostname"); - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/getprotobyname.rs b/src/codegen/builtins/io/getprotobyname.rs deleted file mode 100644 index 80dbc95c55..0000000000 --- a/src/codegen/builtins/io/getprotobyname.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Purpose: -//! Emits PHP `getprotobyname` calls. -//! Looks up a protocol number by name or alias in `/etc/protocols`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_getprotobyname` helper returns -1 when no entry matches; that -//! case is boxed as PHP false, a valid number as a boxed integer. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `getprotobyname()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("getprotobyname()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // string pointer becomes the first helper argument - emitter.instruction("mov x1, x2"); // string length becomes the second helper argument - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // string pointer becomes the first SysV argument - emitter.instruction("mov rsi, rdx"); // string length becomes the second SysV argument - } - } - abi::emit_call_label(emitter, "__rt_getprotobyname"); - box_protocol_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a -1 sentinel becomes PHP `false`, any other value -/// becomes a boxed integer. -fn box_protocol_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("getprotobyname_false"); - let done_label = ctx.next_label("getprotobyname_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the helper find no matching entry? - emitter.instruction(&format!("b.lt {}", false_label)); // box PHP false on the -1 sentinel - emitter.instruction("mov x1, x0"); // move the protocol number into the mixed payload - emitter.instruction("mov x2, #0"); // integer mixed payloads have no high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = integer - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid lookup - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the helper find no matching entry? - emitter.instruction(&format!("js {}", false_label)); // box PHP false on the -1 sentinel - emitter.instruction("mov rdi, rax"); // move the protocol number into the mixed payload - emitter.instruction("xor esi, esi"); // integer mixed payloads have no high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = integer - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid lookup - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/getprotobynumber.rs b/src/codegen/builtins/io/getprotobynumber.rs deleted file mode 100644 index e115440244..0000000000 --- a/src/codegen/builtins/io/getprotobynumber.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Purpose: -//! Emits PHP `getprotobynumber` calls. -//! Looks up a protocol name by number in `/etc/protocols`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_getprotobynumber` helper returns a null pointer when no entry -//! matches; that case is boxed as PHP false, a found name as a boxed string. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `getprotobynumber()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("getprotobynumber()"); - emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the protocol number into the helper argument register - } - abi::emit_call_label(emitter, "__rt_getprotobynumber"); - box_string_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string without copying the buffer. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("getprotobynumber_false"); - let done_label = ctx.next_label("getprotobynumber_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means no entry matched - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means no entry matched - emitter.instruction(&format!("jz {}", false_label)); // box false when no entry matched - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/getservbyname.rs b/src/codegen/builtins/io/getservbyname.rs deleted file mode 100644 index a4cb56489f..0000000000 --- a/src/codegen/builtins/io/getservbyname.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Purpose: -//! Emits PHP `getservbyname` calls. -//! Looks up an internet service port by service name and protocol. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_getservbyname` helper returns -1 when no entry matches; that -//! case is boxed as PHP false, a valid port as a boxed integer. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `getservbyname()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("getservbyname()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the service string while the protocol string is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // protocol pointer becomes the third helper argument - emitter.instruction("mov x4, x2"); // protocol length becomes the fourth helper argument - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the service string into the first two helper arguments - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the service string while the protocol string is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // protocol length becomes the fourth SysV helper argument - emitter.instruction("mov rdx, rax"); // protocol pointer becomes the third SysV helper argument - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the service string into the first two SysV helper arguments - } - } - abi::emit_call_label(emitter, "__rt_getservbyname"); - box_port_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a -1 sentinel becomes PHP `false`, any other value -/// becomes a boxed integer. -fn box_port_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("getservbyname_false"); - let done_label = ctx.next_label("getservbyname_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the helper find no matching entry? - emitter.instruction(&format!("b.lt {}", false_label)); // box PHP false on the -1 sentinel - emitter.instruction("mov x1, x0"); // move the service port into the mixed payload - emitter.instruction("mov x2, #0"); // integer mixed payloads have no high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = integer - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid lookup - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the helper find no matching entry? - emitter.instruction(&format!("js {}", false_label)); // box PHP false on the -1 sentinel - emitter.instruction("mov rdi, rax"); // move the service port into the mixed payload - emitter.instruction("xor esi, esi"); // integer mixed payloads have no high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = integer - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid lookup - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/getservbyport.rs b/src/codegen/builtins/io/getservbyport.rs deleted file mode 100644 index 778a8ede3a..0000000000 --- a/src/codegen/builtins/io/getservbyport.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Purpose: -//! Emits PHP `getservbyport` calls. -//! Looks up an internet service name by port number and protocol. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_getservbyport` helper returns a null pointer when no entry -//! matches; that case is boxed as PHP false, a found name as a boxed string. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `getservbyport()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("getservbyport()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x0, x0, [sp, #-16]!"); // push the port number while the protocol string is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("ldp x0, x9, [sp], #16"); // restore the port number into the first helper argument - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rax"); // push the port number while the protocol string is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rsi, rax"); // protocol pointer becomes the second SysV helper argument - abi::emit_pop_reg_pair(emitter, "rdi", "rcx"); // restore the port number into the first SysV helper argument - } - } - abi::emit_call_label(emitter, "__rt_getservbyport"); - box_string_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string without copying the buffer. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("getservbyport_false"); - let done_label = ctx.next_label("getservbyport_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means no entry matched - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means no entry matched - emitter.instruction(&format!("jz {}", false_label)); // box false when no entry matched - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/glob_fn.rs b/src/codegen/builtins/io/glob_fn.rs deleted file mode 100644 index 6c2687a575..0000000000 --- a/src/codegen/builtins/io/glob_fn.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Purpose: -//! Emits PHP `glob` path-oriented builtin calls. -//! Marshals path strings into runtime helpers that normalize, split, or enumerate filesystem paths. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returned strings and arrays must use runtime allocation/layout compatible with PHP false-on-failure behavior. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for the PHP `glob()` builtin. -/// -/// Evaluates the pattern argument, then calls `__rt_glob` to expand the glob pattern -/// into an array of matching file paths. Returns `Array` on success, or `false` -/// on failure (handled by the runtime helper's false-on-failure return convention). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("glob()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_glob"); // call the target-aware runtime helper that expands the glob pattern into a string array - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/io/hash_file.rs b/src/codegen/builtins/io/hash_file.rs deleted file mode 100644 index 20d3d005a3..0000000000 --- a/src/codegen/builtins/io/hash_file.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Purpose: -//! Emits PHP `hash_file($algo, $filename, $binary = false)` calls. Reads the file -//! through the shared file-read runtime, then hashes the bytes through the same -//! elephc-crypto path as `hash()`, boxing the result as `string|false`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returns PHP `false` (a boxed Mixed cell) when the file cannot be read, matching -//! `file_get_contents()` failure semantics; on success returns the hex (or raw, -//! when `$binary`) digest string. An unknown algorithm throws a catchable -//! `\ValueError` from `__rt_hash`. - -use super::file_get_contents::box_file_get_contents_result; -use crate::codegen::builtins::strings::hash_crypto; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_string, coerce_to_truthiness, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `hash_file($algo, $filename, $binary = false)` builtin call. -/// -/// Evaluates and preserves the algorithm name and the optional `$binary` flag, -/// reads `$filename` via `__rt_file_get_contents_maybe_url`, and — on a successful -/// read — feeds the file bytes to the shared `__rt_hash` dispatcher (persisting the -/// digest so the boxed string owns its bytes). A failed read boxes PHP `false`. -/// Returns `PhpType::Mixed` (the boxed `string|false` runtime representation). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hash_file()"); - let fail = ctx.next_label("hash_file_fail"); - let done = ctx.next_label("hash_file_box"); - emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the algorithm string (evaluated first) - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the filename string (PHP evaluates $filename before $binary) - emit_binary_flag(args, emitter, ctx, data); - emitter.instruction("str x0, [sp, #-16]!"); // preserve the binary flag; all three args are now evaluated in source order - emitter.instruction("ldp x1, x2, [sp, #16]"); // reload the filename into the reader's string registers - abi::emit_call_label(emitter, "__rt_file_get_contents_maybe_url"); // read the file → x1=ptr, x2=len (null on failure) - emitter.instruction(&format!("cbz x1, {}", fail)); // a null pointer means the file could not be read → PHP false - emitter.instruction("mov x3, x1"); // move the file bytes pointer into the hash data register pair - emitter.instruction("mov x4, x2"); // move the file bytes length into the hash data register pair - emitter.instruction("ldr x5, [sp]"); // restore the binary flag into its hash argument register - emitter.instruction("ldp x1, x2, [sp, #32]"); // restore the algorithm string into the algorithm register pair - emitter.instruction("add sp, sp, #48"); // discard the preserved algorithm, filename, and binary slots - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_hash"); // hash the file bytes → x1=ptr, x2=len of the digest string - abi::emit_call_label(emitter, "__rt_str_persist"); // copy the digest to owned heap so the boxed string survives buffer reuse - emitter.instruction(&format!("b {}", done)); // the digest string is ready to box - emitter.label(&fail); - emitter.instruction("add sp, sp, #48"); // discard the preserved algorithm, filename, and binary slots - emitter.instruction("mov x1, #0"); // null string pointer → boxed PHP false - emitter.label(&done); - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the algorithm string (evaluated first) - emit_string_arg(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the filename string (PHP evaluates $filename before $binary) - emit_binary_flag(args, emitter, ctx, data); - abi::emit_push_reg(emitter, "rax"); // preserve the binary flag; all three args are now evaluated in source order - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the filename pointer into the reader's string register - emitter.instruction("mov rdx, QWORD PTR [rsp + 24]"); // reload the filename length into the reader's string register - abi::emit_call_label(emitter, "__rt_file_get_contents_maybe_url"); // read the file → rax=ptr, rdx=len (null on failure) - emitter.instruction("test rax, rax"); // a null pointer means the file could not be read → PHP false - emitter.instruction(&format!("jz {}", fail)); // box false when the read failed - emitter.instruction("mov rdi, rax"); // move the file bytes pointer into the hash data register - emitter.instruction("mov rsi, rdx"); // move the file bytes length into the hash data register - emitter.instruction("mov r10, QWORD PTR [rsp]"); // restore the binary flag into its hash argument register - emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // restore the algorithm string pointer - emitter.instruction("mov rdx, QWORD PTR [rsp + 40]"); // restore the algorithm string length - emitter.instruction("add rsp, 48"); // discard the preserved algorithm, filename, and binary slots - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_hash"); // hash the file bytes → rax=ptr, rdx=len of the digest string - abi::emit_call_label(emitter, "__rt_str_persist"); // copy the digest to owned heap so the boxed string survives buffer reuse - emitter.instruction(&format!("jmp {}", done)); // the digest string is ready to box - emitter.label(&fail); - emitter.instruction("add rsp, 48"); // discard the preserved algorithm, filename, and binary slots - emitter.instruction("xor eax, eax"); // null string pointer → boxed PHP false - emitter.label(&done); - } - } - box_file_get_contents_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Evaluates `arg` and coerces it into the string ABI register pair (mirrors -/// `builtins::strings::args::emit_string_arg`, which is private to the strings -/// module): a Mixed value is cast through `__rt_mixed_cast_string` instead of -/// leaving a boxed cell in the result register with stale string registers. -fn emit_string_arg(arg: &Expr, emitter: &mut Emitter, ctx: &mut Context, data: &mut DataSection) { - let ty = emit_expr(arg, emitter, ctx, data); - coerce_to_string(emitter, ctx, data, &ty); -} - -/// Materialises the optional `$binary` flag (arg index 2) as a 0/1 integer in the -/// int-result register, defaulting to `0` (PHP `false`/hex output) when omitted. -fn emit_binary_flag(args: &[Expr], emitter: &mut Emitter, ctx: &mut Context, data: &mut DataSection) { - if args.len() > 2 { - let ty = emit_expr(&args[2], emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &ty); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); // default $binary to false (hex output) when omitted - } -} diff --git a/src/codegen/builtins/io/http_stream.rs b/src/codegen/builtins/io/http_stream.rs deleted file mode 100644 index 65c621e806..0000000000 --- a/src/codegen/builtins/io/http_stream.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Purpose: -//! Lowers `fopen()` calls whose path is an `http://` URL. -//! Parses the URL at compile time and opens the body through the -//! `__rt_http_open` runtime helper after building the request line at -//! runtime so that the active `stream_context_create(['http' => ...])` -//! options can override the method. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `http://`. -//! -//! Key details: -//! - The URL must be a string literal: `http://[user@]host[:port]/path`. -//! Any `user@` userinfo is dropped. -//! - The host and path are emitted as compile-time rodata literals and -//! handed to `__rt_http_build_request`, which writes the full request -//! into `_http_req_scratch` (consulting `_stream_context_options` -//! along the way). `__rt_http_open` then sends that buffer. -//! - When the context has no `[http][method]` override, the runtime -//! build falls back to `GET`, producing the same wire bytes as the -//! previous static-only path. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits a `fopen("http://...", ...)` call. The path is known to be a string -/// literal beginning with `http://`. -pub fn emit( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() http:// stream"); - // The mode and optional fopen args are evaluated for side effects; - // http:// streams are read-only. - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - emit_open_fd(args, emitter, data); - super::fopen::box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Emits the `http://` open, leaving the response-body fd (or -1 on an -/// unparseable URL) in the int-result register. Does NOT evaluate a mode -/// argument or box the result — shared by `fopen()` and `file_get_contents()`. -pub(super) fn emit_open_fd(args: &[Expr], emitter: &mut Emitter, data: &mut DataSection) { - let parsed = match &args[0].kind { - ExprKind::StringLiteral(url) => parse_http_url(url), - _ => None, - }; - match parsed { - Some(parsed) => { - let (addr_sym, addr_len) = data.add_string(parsed.addr.as_bytes()); - let (host_sym, host_len) = data.add_string(parsed.host.as_bytes()); - let (path_sym, path_len) = data.add_string(parsed.path.as_bytes()); - - match emitter.target.arch { - Arch::AArch64 => { - // -- build the request at runtime so context [http][method] overrides apply -- - abi::emit_symbol_address(emitter, "x0", &host_sym); - emitter.instruction(&format!("mov x1, #{}", host_len)); // host length - abi::emit_symbol_address(emitter, "x2", &path_sym); - emitter.instruction(&format!("mov x3, #{}", path_len)); // path length - abi::emit_call_label(emitter, "__rt_http_build_request"); // x0 = total request length - abi::emit_push_reg(emitter, "x0"); // preserve the request length across the addr setup - abi::emit_symbol_address(emitter, "x0", &addr_sym); - emitter.instruction(&format!("mov x1, #{}", addr_len)); // TCP address length - abi::emit_symbol_address(emitter, "x2", "_http_req_scratch"); // request payload pointer - abi::emit_pop_reg(emitter, "x3"); // request length - abi::emit_call_label(emitter, "__rt_http_open"); // send the HTTP request and open the response body - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &host_sym); - emitter.instruction(&format!("mov rsi, {}", host_len)); // host length - abi::emit_symbol_address(emitter, "rdx", &path_sym); - emitter.instruction(&format!("mov rcx, {}", path_len)); // path length - abi::emit_call_label(emitter, "__rt_http_build_request"); // rax = total request length - abi::emit_push_reg(emitter, "rax"); // preserve the request length across the addr setup - abi::emit_symbol_address(emitter, "rdi", &addr_sym); - emitter.instruction(&format!("mov rsi, {}", addr_len)); // TCP address length - abi::emit_symbol_address(emitter, "rdx", "_http_req_scratch"); // request payload pointer - abi::emit_pop_reg(emitter, "rcx"); // request length - abi::emit_call_label(emitter, "__rt_http_open"); // send the HTTP request and open the response body - } - } - } - None => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // unparseable http:// URL lowers to PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // unparseable http:// URL lowers to PHP false - }, - } -} - -struct ParsedHttpUrl { - addr: String, - host: String, - path: String, -} - -/// Parses an `http://[user@]host[:port]/path` URL into the TCP address -/// (`tcp://host:port`), the host (for the Host: header), and the request -/// path. Returns `None` when the authority is missing or the port is -/// non-numeric. -fn parse_http_url(url: &str) -> Option { - let rest = url.strip_prefix("http://")?; - // Drop any `user@` userinfo before the host — v1 ignores credentials. - let after_userinfo = match rest.find('@') { - Some(at) => &rest[at + 1..], - None => rest, - }; - // Split the authority from the path; a missing path defaults to "/". - let (authority, path) = match after_userinfo.find('/') { - Some(slash) => (&after_userinfo[..slash], &after_userinfo[slash..]), - None => (after_userinfo, "/"), - }; - if authority.is_empty() { - return None; - } - let (host, port) = match authority.rfind(':') { - Some(colon) => (&authority[..colon], &authority[colon + 1..]), - None => (authority, "80"), - }; - if host.is_empty() || port.is_empty() || !port.bytes().all(|b| b.is_ascii_digit()) { - return None; - } - Some(ParsedHttpUrl { - addr: format!("tcp://{}:{}", host, port), - // The Host: header (and the request_fulluri absolute URI, which is - // built as "http://" + host + path) include the port for non-default - // ports, matching PHP — e.g. "127.0.0.1:8080", but bare "host" on :80. - host: if port == "80" { - host.to_string() - } else { - format!("{}:{}", host, port) - }, - path: path.to_string(), - }) -} diff --git a/src/codegen/builtins/io/https_stream.rs b/src/codegen/builtins/io/https_stream.rs deleted file mode 100644 index 0533a9b584..0000000000 --- a/src/codegen/builtins/io/https_stream.rs +++ /dev/null @@ -1,263 +0,0 @@ -//! Purpose: -//! Lowers `fopen()` calls whose path is an `https://` URL. -//! Parses the URL at compile time, publishes the elephc-tls C entry points -//! into the runtime function-pointer slots, and opens the response body -//! through `__rt_https_open`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `https://`. -//! -//! Key details: -//! - The URL must be a string literal: `https://[user@]host[:port]/path`. -//! v1 issues a plain HTTP/1.0 `GET` and ignores any `user@` userinfo. -//! - Indirect function pointers (`_elephc_tls_*_fn`) keep the shared runtime -//! free of any direct elephc-tls reference, so only programs that actually -//! open https URLs trigger `-lelephc_tls` linkage. The wrapper publishes -//! the 4 entry points (`connect`, `write`, `read`, `close`) into BSS slots -//! right before the call, idempotently overwriting them on every fopen. -//! - The host string and HTTP request body are materialised in `.rodata`; -//! the port is passed as an immediate integer. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits a `fopen("https://...", ...)` call. The path is known to be a string -/// literal beginning with `https://`. -pub fn emit( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() https:// stream"); - // The mode and optional fopen args are evaluated for side effects; - // https:// streams are read-only. - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - emit_open_fd(args, emitter, data); - super::fopen::box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Emits the `https://` open (publishing the TLS fn-pointers), leaving the -/// response-body fd (or -1 on an unparseable URL) in the int-result register. -/// Does NOT evaluate a mode argument or box the result — shared by `fopen()` -/// and `file_get_contents()`. -pub(super) fn emit_open_fd(args: &[Expr], emitter: &mut Emitter, data: &mut DataSection) { - let parsed = match &args[0].kind { - ExprKind::StringLiteral(url) => parse_https_url(url), - _ => None, - }; - match parsed { - Some(parts) => { - let (host_sym, host_len) = data.add_string(parts.host.as_bytes()); - let (req_sym, req_len) = data.add_string(parts.request.as_bytes()); - publish_tls_function_pointers(emitter); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x0", &host_sym); - emitter.instruction(&format!("mov x1, #{}", host_len)); // hostname length - emitter.instruction(&format!("mov x2, #{}", parts.port)); // TCP port for the TLS handshake - abi::emit_symbol_address(emitter, "x3", &req_sym); - emitter.instruction(&format!("mov x4, #{}", req_len)); // HTTP request length - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &host_sym); - emitter.instruction(&format!("mov rsi, {}", host_len)); // hostname length - emitter.instruction(&format!("mov rdx, {}", parts.port)); // TCP port for the TLS handshake - abi::emit_symbol_address(emitter, "rcx", &req_sym); - emitter.instruction(&format!("mov r8, {}", req_len)); // HTTP request length - } - } - abi::emit_call_label(emitter, "__rt_https_open"); // run the TLS-secured request and open the body - } - None => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // unparseable https:// URL lowers to PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // unparseable https:// URL lowers to PHP false - }, - } -} - -/// Stores the addresses of the elephc-tls C entry points into the -/// `_elephc_tls_*_fn` runtime slots so `__rt_https_open` and -/// `stream_socket_enable_crypto` can call through them. -pub(crate) fn publish_tls_function_pointers(emitter: &mut Emitter) { - const ENTRIES: &[(&str, &str)] = &[ - ("elephc_tls_connect", "_elephc_tls_connect_fn"), - ("elephc_tls_connect_insecure", "_elephc_tls_connect_insecure_fn"), - ("elephc_tls_connect_cafile", "_elephc_tls_connect_cafile_fn"), - ("elephc_tls_connect_capath", "_elephc_tls_connect_capath_fn"), - ("elephc_tls_connect_peer_name", "_elephc_tls_connect_peer_name_fn"), - ("elephc_tls_write", "_elephc_tls_write_fn"), - ("elephc_tls_read", "_elephc_tls_read_fn"), - ("elephc_tls_close", "_elephc_tls_close_fn"), - ("elephc_tls_attach_fd", "_elephc_tls_attach_fd_fn"), - ( - "elephc_tls_attach_fd_client_cert", - "_elephc_tls_attach_fd_client_cert_fn", - ), - ( - "elephc_tls_connect_client_cert", - "_elephc_tls_connect_client_cert_fn", - ), - ]; - match emitter.target.arch { - Arch::AArch64 => { - for (c_name, slot) in ENTRIES { - let extern_sym = emitter.target.extern_symbol(c_name); - abi::emit_extern_symbol_address(emitter, "x9", &extern_sym); - abi::emit_symbol_address(emitter, "x10", slot); - emitter.instruction("str x9, [x10]"); // publish the elephc-tls entry into its runtime slot - } - } - Arch::X86_64 => { - for (c_name, slot) in ENTRIES { - let extern_sym = emitter.target.extern_symbol(c_name); - abi::emit_extern_symbol_address(emitter, "r9", &extern_sym); - abi::emit_store_reg_to_symbol(emitter, "r9", slot, 0); // publish the elephc-tls entry into its runtime slot - } - } - } -} - -struct HttpsUrl { - host: String, - port: u16, - request: String, -} - -/// Parses an `https://[user@]host[:port]/path` URL into the hostname, the TCP -/// port (defaulting to 443), and the HTTP/1.0 request text. Returns `None` -/// when the authority is missing or the port is non-numeric. -/// -/// IPv6 hosts use the bracket-literal form (`[::1]`, `[2001:db8::1]:8443`); -/// the brackets are stripped from the value passed to `elephc_tls_connect` -/// but preserved in the `Host:` header per RFC 7230 §5.4. -fn parse_https_url(url: &str) -> Option { - let rest = url.strip_prefix("https://")?; - let after_userinfo = match rest.find('@') { - Some(at) => &rest[at + 1..], - None => rest, - }; - let (authority, path) = match after_userinfo.find('/') { - Some(slash) => (&after_userinfo[..slash], &after_userinfo[slash..]), - None => (after_userinfo, "/"), - }; - if authority.is_empty() { - return None; - } - let (connect_host, host_header, port_str) = split_authority(authority)?; - let port: u16 = port_str.parse().ok()?; - Some(HttpsUrl { - host: connect_host, - port, - request: format!( - "GET {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n", - path, host_header - ), - }) -} - -/// Splits `authority` into `(connect_host, host_header, port_str)`. -/// -/// For an IPv6 literal `[::1]:8443`, `connect_host` is `::1` (the bytes -/// `elephc_tls_connect` will actually resolve) while `host_header` keeps the -/// brackets so the HTTP `Host:` line stays RFC-compliant. -fn split_authority(authority: &str) -> Option<(String, String, &str)> { - if let Some(rest) = authority.strip_prefix('[') { - // IPv6 literal: walk to the closing bracket, then look for ':port'. - let close = rest.find(']')?; - let v6 = &rest[..close]; - if v6.is_empty() { - return None; - } - let after = &rest[close + 1..]; - let port_str = if after.is_empty() { - "443" - } else { - after.strip_prefix(':').filter(|p| !p.is_empty())? - }; - Some((v6.to_string(), format!("[{}]", v6), port_str)) - } else { - let (host, port_str) = match authority.rfind(':') { - Some(colon) => (&authority[..colon], &authority[colon + 1..]), - None => (authority, "443"), - }; - if host.is_empty() || port_str.is_empty() { - return None; - } - Some((host.to_string(), host.to_string(), port_str)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Parses an HTTPS URL into host, port, and path pieces for unit assertions. - fn first_line(url: &str) -> Option<(String, u16, String)> { - let parsed = parse_https_url(url)?; - let request_line = parsed.request.lines().next()?.to_string(); - Some((parsed.host, parsed.port, request_line)) - } - - /// Verifies HTTPS URL parsing with the default port. - #[test] - fn parses_default_port() { - let (host, port, line) = first_line("https://example.com/").unwrap(); - assert_eq!(host, "example.com"); - assert_eq!(port, 443); - assert_eq!(line, "GET / HTTP/1.0"); - } - - /// Verifies HTTPS URL parsing with an explicit port and path. - #[test] - fn parses_explicit_port_and_path() { - let (host, port, line) = first_line("https://example.com:8443/api?id=1").unwrap(); - assert_eq!(host, "example.com"); - assert_eq!(port, 8443); - assert_eq!(line, "GET /api?id=1 HTTP/1.0"); - } - - /// Verifies HTTPS URL parsing with an IPv6 literal and default port. - #[test] - fn parses_ipv6_literal_default_port() { - let parsed = parse_https_url("https://[::1]/").unwrap(); - assert_eq!(parsed.host, "::1"); - assert_eq!(parsed.port, 443); - assert!(parsed.request.contains("\r\nHost: [::1]\r\n")); - } - - /// Verifies HTTPS URL parsing with an IPv6 literal and explicit port. - #[test] - fn parses_ipv6_literal_with_port() { - let parsed = parse_https_url("https://[2001:db8::1]:8443/path").unwrap(); - assert_eq!(parsed.host, "2001:db8::1"); - assert_eq!(parsed.port, 8443); - assert!(parsed.request.contains("\r\nHost: [2001:db8::1]\r\n")); - assert!(parsed.request.starts_with("GET /path HTTP/1.0")); - } - - /// Verifies HTTPS URL rejection with a missing authority. - #[test] - fn rejects_missing_authority() { - assert!(parse_https_url("https://").is_none()); - assert!(parse_https_url("https:///path").is_none()); - } - - /// Verifies HTTPS URL rejection with an unclosed IPv6 literal. - #[test] - fn rejects_unclosed_ipv6() { - assert!(parse_https_url("https://[::1/").is_none()); - } - - /// Verifies HTTPS URL rejection with an empty port. - #[test] - fn rejects_empty_port() { - assert!(parse_https_url("https://example.com:/").is_none()); - } -} diff --git a/src/codegen/builtins/io/is_dir.rs b/src/codegen/builtins/io/is_dir.rs deleted file mode 100644 index c7feb10a62..0000000000 --- a/src/codegen/builtins/io/is_dir.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Purpose: -//! Emits PHP `is_dir` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits x86_64 / ARM64 codegen for PHP's `is_dir(path)` builtin. -/// -/// Evaluates `path` (a string expression), calls the runtime helper -/// `__rt_is_dir`, and returns a `bool`. The runtime helper performs -/// a target-aware stat call and signals failure via a sentinel value -/// rather than panicking, preserving PHP semantics. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_dir()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_is_dir"); // call the target-aware runtime helper that checks whether the path is a directory - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/is_executable.rs b/src/codegen/builtins/io/is_executable.rs deleted file mode 100644 index 659a717796..0000000000 --- a/src/codegen/builtins/io/is_executable.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Purpose: -//! Emits PHP `is_executable` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits `is_executable($path)` by evaluating the path argument, calling the -/// `__rt_is_executable` runtime helper, and returning `PhpType::Bool`. -/// -/// Expects exactly one argument (the path expression). The runtime helper -/// performs `access(path, X_OK)` on the target platform. Filesystem state is -/// observable, so call order and failure sentinels must be preserved. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_executable()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_is_executable"); // call the target-aware runtime helper that runs access(path, X_OK) - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/is_file.rs b/src/codegen/builtins/io/is_file.rs deleted file mode 100644 index 3cb2e757a4..0000000000 --- a/src/codegen/builtins/io/is_file.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Purpose: -//! Emits PHP `is_file` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `is_file` builtin. -/// -/// A `scheme://...` path whose scheme matches a registered userspace wrapper is -/// routed through `__rt_user_wrapper_url_stat_field` (field selector 1 = -/// `'mode'`): the path is a regular file when the wrapper's `url_stat()` reports -/// a `'mode'` whose `S_IFMT` bits equal `S_IFREG` (0o100000). Any other path -/// falls through to the platform-aware `__rt_is_file`. Returns `PhpType::Bool`. -/// -/// # Arguments -/// - `args[0]`: the path expression to check -/// - `_name`: unused; matches the dispatcher signature -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_file()"); - emit_expr(&args[0], emitter, ctx, data); - let fallback = ctx.next_label("is_file_fs"); - let done = ctx.next_label("is_file_done"); - match emitter.target.arch { - Arch::AArch64 => { - // -- path string: x1 = ptr, x2 = len -- - emitter.instruction("sub sp, sp, #16"); // scratch: [sp,#0] path ptr, [sp,#8] path len - emitter.instruction("str x1, [sp, #0]"); // save path ptr for the filesystem fallback - emitter.instruction("str x2, [sp, #8]"); // save path len for the filesystem fallback - emitter.instruction("mov x0, x1"); // field helper arg0 = path ptr - emitter.instruction("mov x1, x2"); // field helper arg1 = path len - emitter.instruction("mov x2, #1"); // field selector 1 = 'mode' - abi::emit_call_label(emitter, "__rt_user_wrapper_url_stat_field"); // x0 = wrapper 'mode' (or -1) - abi::emit_symbol_address(emitter, "x9", "_url_stat_matched"); - emitter.instruction("ldrb w9, [x9]"); // did a registered wrapper scheme match? - emitter.instruction(&format!("cbz w9, {}", fallback)); // no → real filesystem is_file - emitter.instruction("and x0, x0, #0xF000"); // isolate the S_IFMT file-type bits of the mode - emitter.instruction("mov x9, #0x8000"); // S_IFREG = 0o100000 (regular file) - emitter.instruction("cmp x0, x9"); // is it a regular file? - emitter.instruction("cset x0, eq"); // is_file = (S_IFMT == S_IFREG) - emitter.instruction(&format!("b {}", done)); // skip the filesystem path - emitter.label(&fallback); - emitter.instruction("ldr x1, [sp, #0]"); // restore path ptr for the filesystem helper - emitter.instruction("ldr x2, [sp, #8]"); // restore path len for the filesystem helper - abi::emit_call_label(emitter, "__rt_is_file"); // real filesystem regular-file check - emitter.label(&done); - emitter.instruction("add sp, sp, #16"); // release the scratch frame - } - Arch::X86_64 => { - // -- path string: rax = ptr, rdx = len -- - emitter.instruction("sub rsp, 16"); // scratch: [rsp+0] path ptr, [rsp+8] path len - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save path ptr for the filesystem fallback - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save path len for the filesystem fallback - emitter.instruction("mov rdi, rax"); // field helper arg0 = path ptr - emitter.instruction("mov rsi, rdx"); // field helper arg1 = path len - emitter.instruction("mov edx, 1"); // field selector 1 = 'mode' - abi::emit_call_label(emitter, "__rt_user_wrapper_url_stat_field"); // rax = wrapper 'mode' (or -1) - abi::emit_symbol_address(emitter, "r9", "_url_stat_matched"); // load runtime data address - emitter.instruction("movzx r9d, BYTE PTR [r9]"); // did a registered wrapper scheme match? - emitter.instruction("test r9d, r9d"); // matched flag set? - emitter.instruction(&format!("jz {}", fallback)); // no → real filesystem is_file - emitter.instruction("and eax, 0xF000"); // isolate the S_IFMT file-type bits of the mode - emitter.instruction("cmp eax, 0x8000"); // S_IFREG = 0o100000 (regular file)? - emitter.instruction("sete al"); // is_file = (S_IFMT == S_IFREG) - emitter.instruction("movzx eax, al"); // widen the bool into the canonical result register - emitter.instruction(&format!("jmp {}", done)); // skip the filesystem path - emitter.label(&fallback); - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore path ptr for the filesystem helper - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore path len for the filesystem helper - abi::emit_call_label(emitter, "__rt_is_file"); // real filesystem regular-file check - emitter.label(&done); - emitter.instruction("add rsp, 16"); // release the scratch frame - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/is_link.rs b/src/codegen/builtins/io/is_link.rs deleted file mode 100644 index b94c45fef0..0000000000 --- a/src/codegen/builtins/io/is_link.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits PHP `is_link` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `is_link` builtin call for a single path argument. -/// -/// # Arguments -/// - `_name`: Unused; always `is_link` (case-insensitive lookup handled by catalog). -/// - `args`: Exactly one expression yielding a path string. -/// - `emitter`: Assembly emitter for the current function. -/// - `ctx`: Codegen context (variable layout, class metadata). -/// - `data`: Data section for relocations and static data. -/// -/// # Behavior -/// Evaluates `args[0]` (path argument), then calls `__rt_is_link` which invokes -/// `lstat()` and checks `S_ISLNK`. Returns `PhpType::Bool` (PHP `is_link` is always bool). -/// -/// # Safety & invariants -/// - Filesystem state is observable; call order must match source evaluation order. -/// - The runtime helper handles platform-specific `S_ISLNK` detection. -/// - Result is always `Some(PhpType::Bool)`; no failure sentinel — PHP false on error. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_link()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_is_link"); // call the target-aware runtime helper that runs lstat() and checks S_ISLNK - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/is_readable.rs b/src/codegen/builtins/io/is_readable.rs deleted file mode 100644 index ac0c873766..0000000000 --- a/src/codegen/builtins/io/is_readable.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Purpose: -//! Emits PHP `is_readable` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to PHP's `is_readable()` builtin. -/// -/// Evaluates `args[0]` as a filesystem path expression and calls `__rt_is_readable` -/// to check whether the path is readable by the current process. -/// -/// # Arguments -/// - `args[0]`: path expression to check -/// -/// # Returns -/// `Some(PhpType::Bool)` — the result of the readability check -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_readable()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_is_readable"); // call the target-aware runtime helper that checks whether the path is readable - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/is_writable.rs b/src/codegen/builtins/io/is_writable.rs deleted file mode 100644 index 3bc954173a..0000000000 --- a/src/codegen/builtins/io/is_writable.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Purpose: -//! Emits PHP `is_writable` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `is_writable` builtin call. -/// -/// # Arguments -/// - `_name`: Unused name matching the builtin catalog entry. -/// - `args`: Single argument supplying the filesystem path to check. -/// - `emitter`: Target assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and scope. -/// - `data`: Data section for relocations and static data. -/// -/// # Returns -/// Always returns `Some(PhpType::Bool)` since `is_writable` is a predicate. -/// -/// # Codegen behavior -/// Emits the path argument expression, then calls `__rt_is_writable` to perform -/// the platform-specific stat operation. The result is a PHP boolean. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_writable()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_is_writable"); // call the target-aware runtime helper that checks whether the path is writable - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/link.rs b/src/codegen/builtins/io/link.rs deleted file mode 100644 index dfb02a44e1..0000000000 --- a/src/codegen/builtins/io/link.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Purpose: -//! Emits PHP `link` (hard link) builtin calls. -//! Marshals old / new path arguments and invokes the libc wrapper runtime. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returns `true` on success, `false` on failure. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Lowers PHP `link(oldpath, newpath)` into a `__rt_link` libc call. -/// -/// Marshals two string arguments (old path, new path) according to target ABI. -/// On both ARM64 and x86_64 the evaluation order preserves source semantics: -/// the old path is evaluated and preserved on the stack or in callee-saved registers -/// before the new path is evaluated, then both are loaded into argument registers -/// in the order `link(oldpath, newpath)` expects before the runtime wrapper is called. -/// -/// # Arguments -/// - `_name`: Unused — the builtin name is not needed at emission time. -/// - `args`: Exactly two expressions: `args[0]` = old path, `args[1]` = new path. -/// - `emitter`: Assembly emitter. -/// - `ctx`: Codegen context (used by `emit_expr`). -/// - `data`: Data section for string literals. -/// -/// # Return -/// Always returns `PhpType::Bool` (the call result is a libc `int` converted to bool). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("link()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve old path while new path is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move new path pointer - emitter.instruction("mov x4, x2"); // move new path length - emitter.instruction("ldp x1, x2, [sp], #16"); // restore old path - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve old path - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // new path pointer - emitter.instruction("mov rsi, rdx"); // new path length - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore old path - } - } - abi::emit_call_label(emitter, "__rt_link"); // libc link(oldpath, newpath) wrapper - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/linkinfo.rs b/src/codegen/builtins/io/linkinfo.rs deleted file mode 100644 index 88ccb25c9f..0000000000 --- a/src/codegen/builtins/io/linkinfo.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `linkinfo` builtin calls. -//! Returns the `st_dev` field of the link (or -1 on failure). -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The runtime helper invokes libc `lstat()` and returns the platform `st_dev` -//! field on success, or PHP's `-1` failure sentinel. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `linkinfo()` builtin. -/// -/// `linkinfo()` calls the runtime helper `__rt_linkinfo`, which wraps libc -/// `lstat()` and returns the `st_dev` field of the link as an integer. -/// Returns `-1` on failure (e.g., if the path does not exist or is not a symlink). -/// -/// # Arguments -/// * `_name` - Unused; present for dispatcher uniformity. -/// * `args` - Must contain exactly one argument: the path as a string expression. -/// * `emitter` - Target assembly emitter. -/// * `ctx` - Codegen context (variable layout, ownership). -/// * `data` - Data section for string literals and metadata. -/// -/// # Returns -/// Always returns `PhpType::Int` (the device ID or -1 on failure). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("linkinfo()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_linkinfo"); // libc lstat() wrapper that returns the device id - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/lstat.rs b/src/codegen/builtins/io/lstat.rs deleted file mode 100644 index c80e949842..0000000000 --- a/src/codegen/builtins/io/lstat.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `lstat` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_array_or_false_result; - -/// Emits code for the PHP `lstat()` builtin, which returns filesystem metadata -/// for a file without following symlinks. -/// -/// # Arguments -/// - `args[0]`: the file path (string expression) -/// - `emitter`: assembly emitter -/// - `ctx`: codegen context (for variable layout, ownership state) -/// - `data`: data section (for string/array literals) -/// -/// # Returns -/// `Some(PhpType::Mixed)` — `lstat` always produces a value (array on success, `false` on failure). -/// -/// # Runtime behavior -/// Calls `__rt_lstat_array` to build a PHP-compatible metadata array or emit `false`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("lstat()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_lstat_array"); // call the target-aware runtime helper that builds the PHP-compatible lstat array - box_stat_array_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/mkdir.rs b/src/codegen/builtins/io/mkdir.rs deleted file mode 100644 index 9e2ae37e75..0000000000 --- a/src/codegen/builtins/io/mkdir.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Purpose: -//! Emits PHP `mkdir` filesystem mutation builtin calls. -//! Routes `scheme://` paths matching a registered userspace wrapper to the -//! wrapper's `mkdir()` method; all other paths use the libc `__rt_mkdir`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. -//! - The wrapper split mirrors `readfile()`: a `__rt_path_is_wrapper` probe picks -//! the wrapper branch (`__rt_user_wrapper_path_op` with the `mkdir` vtable slot -//! 17) over the libc filesystem branch. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::path_op_wrapper::emit_single_path_wrapper_dispatch; - -/// `mkdir` vtable slot index in the per-class user-wrapper vtable. -const MKDIR_SLOT: usize = 17; - -/// Emits code for the PHP `mkdir(path, ...)` builtin. -/// -/// Arguments: -/// - `args[0]`: path expression, emitted via `emit_expr` before the dispatch. -/// - `_name`: unused; preserved for dispatcher signature parity. -/// - `ctx`, `data`: carried through to `emit_expr` for path materialization. -/// -/// Returns: `Some(PhpType::Bool)` — PHP `mkdir` returns `bool` on success/failure. -/// -/// Runtime contract: a registered `scheme://` path dispatches to the wrapper's -/// `mkdir()` (vtable slot 17) via `__rt_user_wrapper_path_op`; any other path -/// calls the libc `__rt_mkdir`. (v1: `$mode`/`$recursive` are not threaded — the -/// libc path uses mode `0755`; the wrapper receives zeroed extra arguments.) -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("mkdir()"); - emit_expr(&args[0], emitter, ctx, data); - emit_single_path_wrapper_dispatch(emitter, ctx, "__rt_mkdir", MKDIR_SLOT); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/mod.rs b/src/codegen/builtins/io/mod.rs deleted file mode 100644 index e9a26963b8..0000000000 --- a/src/codegen/builtins/io/mod.rs +++ /dev/null @@ -1,369 +0,0 @@ -//! Purpose: -//! Dispatches filesystem, path, stream, and diagnostic PHP builtins to their focused codegen emitters. -//! Keeps the public builtin category surface small while leaf files own lowering details. -//! -//! Called from: -//! - `crate::codegen::builtins::emit_builtin_call()`. -//! -//! Key details: -//! - Dispatcher names must stay aligned with the builtin catalog and signature normalization layer. - -mod basename; -mod chdir; -mod chgrp; -mod chmod; -mod chown; -mod clearstatcache; -mod copy; -mod disk_space; -mod dirname; -mod fclose; -mod fdatasync; -mod feof; -mod fflush; -mod fgetc; -mod fgetcsv; -mod fgets; -mod flock; -mod fnmatch; -mod file; -mod file_exists; -mod file_get_contents; -mod file_put_contents; -mod hash_file; -mod fileatime; -mod filectime; -mod filegroup; -mod fileinode; -mod filemtime; -mod fileowner; -mod fileperms; -mod data_stream; -mod compress_bzip2_stream; -mod compress_zlib_stream; -mod ftp_stream; -mod ftps_stream; -mod http_stream; -mod https_stream; -pub(crate) mod phar_stream; -mod php_filter_stream; -mod filesize; -mod filetype; -mod fopen; -mod fsockopen; -mod gethostname; -mod gethostbyname; -mod gethostbyaddr; -mod getprotobyname; -mod getprotobynumber; -mod getservbyname; -mod getservbyport; -mod fpassthru; -mod fprintf; -mod vfprintf; -mod fputcsv; -mod fread; -mod fscanf; -mod readfile; -mod readlink; -mod fseek; -mod fsync; -mod ftell; -mod ftruncate; -mod fwrite; -mod getcwd; -mod glob_fn; -mod is_dir; -mod is_executable; -mod is_file; -mod is_link; -mod is_readable; -mod is_writable; -mod link; -mod linkinfo; -mod mkdir; -mod pathinfo; -mod path_op_wrapper; -mod pclose; -mod popen; -mod opendir; -mod readdir; -mod closedir; -mod rewinddir; -mod print_r; -mod readline; -mod realpath; -mod rename; -mod rewind; -mod rmdir; -mod fstat; -mod lstat; -mod scandir; -mod stat; -mod stat_result; -pub(crate) mod stream_arg; -mod stream_copy_to_stream; -mod stream_get_line; -mod stream_socket_accept; -mod stream_socket_client; -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_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_notification; -mod stream_resolve_include_path; -mod stream_bucket; -mod stream_filter_register; -mod stream_set_buffer; -mod stream_socket_enable_crypto; -mod stream_wrapper_register; -mod stream_wrapper_restore; -mod stream_wrapper_unregister; -mod stream_get_contents; -mod stream_get_meta_data; -mod stream_introspection; -mod stream_filter; -pub(crate) mod stream_filter_bzip2; -pub(crate) mod stream_filter_iconv; -pub(crate) mod stream_filter_iconv_write; -pub(crate) mod stream_filter_inflate; -pub(crate) mod stream_filter_zlib; -mod stream_isatty; -mod stream_select; -mod stream_set_blocking; -mod stream_set_timeout; -mod symlink; -mod sys_get_temp_dir; -mod tempnam; -mod tmpfile; -mod touch; -mod umask; -mod unlink; -mod var_dump; - -pub(crate) use https_stream::publish_tls_function_pointers; - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Dispatches a PHP builtin call by name to its focused codegen emitter. -/// -/// `name` must match a catalogued PHP builtin in the `io` category (e.g., `fopen`, -/// `file_get_contents`, `copy`). The matching emitter receives the raw argument -/// expressions and emits target-specific assembly for the call. -/// -/// Returns `Some(PhpType)` with the return type on successful dispatch, or `None` -/// if `name` is not a recognised io builtin. -/// -/// # Arguments -/// - `name` — lowercase ASCII builtin name (case-insensitive per PHP semantics) -/// - `args` — parsed argument expressions from the call site -/// - `emitter` — target-aware assembly emitter (controls instruction emission) -/// - `ctx` — shared codegen context (frame layout, locals, ownership) -/// - `data` — writable data section for relocations, string tables, and metadata -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "var_dump" => var_dump::emit(name, args, emitter, ctx, data), - "print_r" => print_r::emit(name, args, emitter, ctx, data), - "fopen" => fopen::emit(name, args, emitter, ctx, data), - "fclose" => fclose::emit(name, args, emitter, ctx, data), - "fread" => fread::emit(name, args, emitter, ctx, data), - "fwrite" => fwrite::emit(name, args, emitter, ctx, data), - "fgets" => fgets::emit(name, args, emitter, ctx, data), - "fgetc" => fgetc::emit(name, args, emitter, ctx, data), - "fprintf" => fprintf::emit(name, args, emitter, ctx, data), - "vfprintf" => vfprintf::emit(name, args, emitter, ctx, data), - "fscanf" => fscanf::emit(name, args, emitter, ctx, data), - "fpassthru" => fpassthru::emit(name, args, emitter, ctx, data), - "flock" => flock::emit(name, args, emitter, ctx, data), - "tmpfile" => tmpfile::emit(name, args, emitter, ctx, data), - "readfile" => readfile::emit(name, args, emitter, ctx, data), - "symlink" => symlink::emit(name, args, emitter, ctx, data), - "link" => link::emit(name, args, emitter, ctx, data), - "readlink" => readlink::emit(name, args, emitter, ctx, data), - "linkinfo" => linkinfo::emit(name, args, emitter, ctx, data), - "feof" => feof::emit(name, args, emitter, ctx, data), - "readline" => readline::emit(name, args, emitter, ctx, data), - "fseek" => fseek::emit(name, args, emitter, ctx, data), - "ftell" => ftell::emit(name, args, emitter, ctx, data), - "rewind" => rewind::emit(name, args, emitter, ctx, data), - "file_get_contents" => file_get_contents::emit(name, args, emitter, ctx, data), - "file_put_contents" => file_put_contents::emit(name, args, emitter, ctx, data), - "hash_file" => hash_file::emit(name, args, emitter, ctx, data), - "file" => file::emit(name, args, emitter, ctx, data), - "file_exists" => file_exists::emit(name, args, emitter, ctx, data), - "is_file" => is_file::emit(name, args, emitter, ctx, data), - "is_dir" => is_dir::emit(name, args, emitter, ctx, data), - "is_readable" => is_readable::emit(name, args, emitter, ctx, data), - "is_writable" => is_writable::emit(name, args, emitter, ctx, data), - "filesize" => filesize::emit(name, args, emitter, ctx, data), - "filemtime" => filemtime::emit(name, args, emitter, ctx, data), - "copy" => copy::emit(name, args, emitter, ctx, data), - "disk_free_space" | "disk_total_space" => { - disk_space::emit(name, args, emitter, ctx, data) - } - "rename" => rename::emit(name, args, emitter, ctx, data), - "unlink" => unlink::emit(name, args, emitter, ctx, data), - "mkdir" => mkdir::emit(name, args, emitter, ctx, data), - "rmdir" => rmdir::emit(name, args, emitter, ctx, data), - "scandir" => scandir::emit(name, args, emitter, ctx, data), - "glob" => glob_fn::emit(name, args, emitter, ctx, data), - "getcwd" => getcwd::emit(name, args, emitter, ctx, data), - "chdir" => chdir::emit(name, args, emitter, ctx, data), - "tempnam" => tempnam::emit(name, args, emitter, ctx, data), - "sys_get_temp_dir" => sys_get_temp_dir::emit(name, args, emitter, ctx, data), - "fgetcsv" => fgetcsv::emit(name, args, emitter, ctx, data), - "fputcsv" => fputcsv::emit(name, args, emitter, ctx, data), - "fileatime" => fileatime::emit(name, args, emitter, ctx, data), - "filectime" => filectime::emit(name, args, emitter, ctx, data), - "fileperms" => fileperms::emit(name, args, emitter, ctx, data), - "fileowner" => fileowner::emit(name, args, emitter, ctx, data), - "filegroup" => filegroup::emit(name, args, emitter, ctx, data), - "fileinode" => fileinode::emit(name, args, emitter, ctx, data), - "filetype" => filetype::emit(name, args, emitter, ctx, data), - "is_executable" => is_executable::emit(name, args, emitter, ctx, data), - "is_link" => is_link::emit(name, args, emitter, ctx, data), - // is_writeable is a documented PHP alias of is_writable. - "is_writeable" => is_writable::emit(name, args, emitter, ctx, data), - "clearstatcache" => clearstatcache::emit(name, args, emitter, ctx, data), - "stat" => stat::emit(name, args, emitter, ctx, data), - "lstat" => lstat::emit(name, args, emitter, ctx, data), - "fstat" => fstat::emit(name, args, emitter, ctx, data), - "basename" => basename::emit(name, args, emitter, ctx, data), - "dirname" => dirname::emit(name, args, emitter, ctx, data), - "fnmatch" => fnmatch::emit(name, args, emitter, ctx, data), - "realpath" => realpath::emit(name, args, emitter, ctx, data), - "pathinfo" => pathinfo::emit(name, args, emitter, ctx, data), - "chmod" => chmod::emit(name, args, emitter, ctx, data), - "chown" => chown::emit(name, args, emitter, ctx, data), - "chgrp" => chgrp::emit(name, args, emitter, ctx, data), - "umask" => umask::emit(name, args, emitter, ctx, data), - "ftruncate" => ftruncate::emit(name, args, emitter, ctx, data), - "fsync" => fsync::emit(name, args, emitter, ctx, data), - "fflush" => fflush::emit(name, args, emitter, ctx, data), - "fdatasync" => fdatasync::emit(name, args, emitter, ctx, data), - "touch" => touch::emit(name, args, emitter, ctx, data), - "gethostname" => gethostname::emit(name, args, emitter, ctx, data), - "gethostbyname" => gethostbyname::emit(name, args, emitter, ctx, data), - "gethostbyaddr" => gethostbyaddr::emit(name, args, emitter, ctx, data), - "getprotobyname" => getprotobyname::emit(name, args, emitter, ctx, data), - "getprotobynumber" => getprotobynumber::emit(name, args, emitter, ctx, data), - "getservbyname" => getservbyname::emit(name, args, emitter, ctx, data), - "getservbyport" => getservbyport::emit(name, args, emitter, ctx, data), - "stream_copy_to_stream" => { - stream_copy_to_stream::emit(name, args, emitter, ctx, data) - } - "stream_get_contents" => stream_get_contents::emit(name, args, emitter, ctx, data), - "stream_get_meta_data" => { - stream_get_meta_data::emit(name, args, emitter, ctx, data) - } - "stream_get_line" => stream_get_line::emit(name, args, emitter, ctx, data), - "stream_isatty" => stream_isatty::emit(name, args, emitter, ctx, data), - "stream_select" => stream_select::emit(name, args, emitter, ctx, data), - "stream_set_blocking" => stream_set_blocking::emit(name, args, emitter, ctx, data), - "stream_set_timeout" => stream_set_timeout::emit(name, args, emitter, ctx, data), - "stream_socket_server" => stream_socket_server::emit(name, args, emitter, ctx, data), - "stream_socket_client" => stream_socket_client::emit(name, args, emitter, ctx, data), - "fsockopen" | "pfsockopen" => fsockopen::emit(name, args, emitter, ctx, data), - "stream_wrapper_register" => { - stream_wrapper_register::emit(name, args, emitter, ctx, data) - } - "stream_wrapper_unregister" => { - stream_wrapper_unregister::emit(name, args, emitter, ctx, data) - } - "stream_wrapper_restore" => { - stream_wrapper_restore::emit(name, args, emitter, ctx, data) - } - "stream_socket_enable_crypto" => { - stream_socket_enable_crypto::emit(name, args, emitter, ctx, data) - } - "stream_context_create" => { - stream_context_create::emit(name, args, emitter, ctx, data) - } - "stream_context_get_default" => { - stream_context_get_default::emit(name, args, emitter, ctx, data) - } - "stream_context_set_default" => { - stream_context_set_default::emit(name, args, emitter, ctx, data) - } - "stream_context_set_option" => { - stream_context_set_option::emit(name, args, emitter, ctx, data) - } - "stream_context_set_params" => { - stream_context_set_params::emit(name, args, emitter, ctx, data) - } - "stream_context_get_options" => { - stream_context_get_options::emit(name, args, emitter, ctx, data) - } - "stream_context_get_params" => { - stream_context_get_params::emit(name, args, emitter, ctx, data) - } - "stream_resolve_include_path" => { - stream_resolve_include_path::emit(name, args, emitter, ctx, data) - } - "stream_filter_register" => { - stream_filter_register::emit(name, args, emitter, ctx, data) - } - "stream_bucket_new" => stream_bucket::emit_new(name, args, emitter, ctx, data), - "stream_bucket_make_writeable" => { - stream_bucket::emit_make_writeable(name, args, emitter, ctx, data) - } - "stream_bucket_append" | "stream_bucket_prepend" => { - stream_bucket::emit_append_or_prepend(name, args, emitter, ctx, data) - } - "stream_set_chunk_size" - | "stream_set_read_buffer" - | "stream_set_write_buffer" => { - stream_set_buffer::emit(name, args, emitter, ctx, data) - } - "stream_socket_accept" => stream_socket_accept::emit(name, args, emitter, ctx, data), - "stream_socket_shutdown" => { - stream_socket_shutdown::emit(name, args, emitter, ctx, data) - } - "stream_socket_sendto" => { - stream_socket_sendto::emit(name, args, emitter, ctx, data) - } - "stream_socket_recvfrom" => { - stream_socket_recvfrom::emit(name, args, emitter, ctx, data) - } - "stream_socket_get_name" => { - stream_socket_get_name::emit(name, args, emitter, ctx, data) - } - "stream_socket_pair" => { - stream_socket_pair::emit(name, args, emitter, ctx, data) - } - "popen" => popen::emit(name, args, emitter, ctx, data), - "pclose" => pclose::emit(name, args, emitter, ctx, data), - "opendir" => opendir::emit(name, args, emitter, ctx, data), - "readdir" => readdir::emit(name, args, emitter, ctx, data), - "closedir" => closedir::emit(name, args, emitter, ctx, data), - "rewinddir" => rewinddir::emit(name, args, emitter, ctx, data), - "stream_is_local" | "stream_supports_lock" | "stream_get_wrappers" - | "stream_get_transports" | "stream_get_filters" => { - stream_introspection::emit(name, args, emitter, ctx, data) - } - "stream_filter_append" | "stream_filter_prepend" => { - stream_filter::emit_attach(name, args, emitter, ctx, data) - } - "stream_filter_remove" => stream_filter::emit_remove(name, args, emitter, ctx, data), - _ => None, - } -} diff --git a/src/codegen/builtins/io/opendir.rs b/src/codegen/builtins/io/opendir.rs deleted file mode 100644 index 5d1db438d6..0000000000 --- a/src/codegen/builtins/io/opendir.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Purpose: -//! Emits PHP `opendir` calls. -//! Opens a directory stream and yields it as a PHP stream resource. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_opendir` helper returns the directory descriptor or -1; the -//! result is boxed by the shared `box_socket_result` helper as `resource|false`. - -use crate::codegen::abi; -use crate::codegen::builtins::io::stream_socket_server::box_socket_result; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `opendir()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("opendir()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_opendir"); - box_socket_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/path_op_wrapper.rs b/src/codegen/builtins/io/path_op_wrapper.rs deleted file mode 100644 index bf0d378ec1..0000000000 --- a/src/codegen/builtins/io/path_op_wrapper.rs +++ /dev/null @@ -1,339 +0,0 @@ -//! Purpose: -//! Shared codegen helper that routes a single-path filesystem-mutation builtin -//! (`unlink`/`mkdir`/`rmdir`) to a registered userspace stream wrapper when the -//! path's `scheme://` prefix matches, or to the builtin's libc runtime helper -//! otherwise. -//! -//! Called from: -//! - `crate::codegen::builtins::io::{unlink, mkdir, rmdir}::emit()`, after the -//! path string has been materialized into the string-result registers -//! (`x1`/`x2` on AArch64, `rax`/`rdx` on x86_64). -//! -//! Key details: -//! - Mirrors the `readfile()` builtin's wrapper/filesystem split: probe with -//! `__rt_path_is_wrapper`, then branch to `__rt_user_wrapper_path_op` (with the -//! method's vtable slot) or the libc helper. -//! - `__rt_path_is_wrapper` takes the path in the SysV first/second args -//! (`x0`/`x1`, `rdi`/`rsi`); the libc helpers keep consuming the path from the -//! string-result registers; `__rt_user_wrapper_path_op` takes `x0`=ptr, -//! `x1`=len, `x2`=slot (`rdi`/`rsi`/`rdx` on x86_64), with the extra int args -//! zeroed (PHP's `$mode`/`$options` default to wrapper-defined behavior). - -use crate::codegen::context::Context; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; - -/// `stream_metadata` vtable slot index in the per-class user-wrapper vtable. -const STREAM_METADATA_SLOT: usize = 14; -/// PHP `STREAM_META_OWNER_NAME` option value (`chown` by string user name). -pub(crate) const STREAM_META_OWNER_NAME: usize = 2; -/// PHP `STREAM_META_OWNER` option value (`chown` by integer uid). -pub(crate) const STREAM_META_OWNER: usize = 3; -/// PHP `STREAM_META_GROUP_NAME` option value (`chgrp` by string group name). -pub(crate) const STREAM_META_GROUP_NAME: usize = 4; -/// PHP `STREAM_META_GROUP` option value (`chgrp` by integer gid). -pub(crate) const STREAM_META_GROUP: usize = 5; - -/// Boxes a raw integer (in `x0` / `rax`) into an owned `Mixed` cell, leaving the -/// boxed pointer in `x0` / `rax`. -/// -/// PHP passes `stream_metadata`'s `$value` as `mixed`, so an integer value (chmod -/// mode, chown uid, chgrp gid) must be boxed before it is handed to the wrapper -/// method. The caller owns the returned cell and must release it with -/// `__rt_decref_mixed` once the wrapper call has returned (the callee borrows). -pub fn emit_box_int_as_mixed(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // value_lo = the integer - emitter.instruction("mov x2, #0"); // value_hi = 0 for an integer scalar - emitter.instruction("mov x0, #0"); // runtime tag 0 = int - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the int → x0 = owned Mixed ptr - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // value_lo = the integer - emitter.instruction("xor esi, esi"); // value_hi = 0 for an integer scalar - emitter.instruction("xor eax, eax"); // runtime tag 0 = int - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the int → rax = owned Mixed ptr - } - } -} - -/// Boxes a runtime string (pointer in `x1`/`rax`, length in `x2`/`rdx` — the -/// string-result registers) into an owned `Mixed` cell, leaving the boxed pointer -/// in `x0` / `rax`. -/// -/// `__rt_mixed_from_value` persists the string payload for the boxed owner, so the -/// caller owns the returned cell and must release it with `__rt_decref_mixed` -/// after the wrapper call returns. -pub fn emit_box_string_as_mixed(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - // x1 = ptr, x2 = len already match __rt_mixed_from_value's value_lo/value_hi. - emitter.instruction("mov x0, #1"); // runtime tag 1 = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist+box → x0 = owned Mixed ptr - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // value_lo = string pointer - emitter.instruction("mov rsi, rdx"); // value_hi = string length - emitter.instruction("mov eax, 1"); // runtime tag 1 = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist+box → rax = owned Mixed ptr - } - } -} - -/// Emits the wrapper-vs-filesystem dispatch for a single-path mutation builtin. -/// -/// On entry the path string occupies the string-result registers (`x1`=ptr, -/// `x2`=len on AArch64; `rax`=ptr, `rdx`=len on x86_64). When the path's scheme -/// matches a registered wrapper, calls `__rt_user_wrapper_path_op(path, len, -/// slot, 0, 0)`; otherwise calls `libc_helper` (e.g. `__rt_unlink`) which -/// consumes the path from the string-result registers. The bool result is left -/// in the standard return register (`x0`/`rax`). -pub fn emit_single_path_wrapper_dispatch( - emitter: &mut Emitter, - ctx: &mut Context, - libc_helper: &str, - vtable_slot: usize, -) { - let wrapper = ctx.next_label("path_op_wrapper"); - let after = ctx.next_label("path_op_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("sub sp, sp, #16"); // scratch: [sp,#0] path ptr, [sp,#8] path len - emitter.instruction("str x1, [sp, #0]"); // preserve path ptr across the wrapper-scheme probe - emitter.instruction("str x2, [sp, #8]"); // preserve path len across the wrapper-scheme probe - emitter.instruction("mov x0, x1"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov x1, x2"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // x0 = 1 when the scheme matches a registered wrapper - emitter.instruction("ldr x1, [sp, #0]"); // restore path ptr for the chosen helper - emitter.instruction("ldr x2, [sp, #8]"); // restore path len for the chosen helper - emitter.instruction(&format!("cbnz x0, {}", wrapper)); // registered wrapper scheme → wrapper path-op - abi::emit_call_label(emitter, libc_helper); // normal path: libc filesystem helper (path in x1/x2) - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov x0, x1"); // user_wrapper_path_op arg0 = path ptr - emitter.instruction("mov x1, x2"); // user_wrapper_path_op arg1 = path len - emitter.instruction(&format!("mov x2, #{}", vtable_slot)); // arg2 = the method's vtable slot index - emitter.instruction("mov x3, #0"); // arg3 = 0 (mode/options default) - emitter.instruction("mov x4, #0"); // arg4 = 0 (options default) - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's path method - emitter.label(&after); - emitter.instruction("add sp, sp, #16"); // release the scratch frame - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // scratch: [rsp+0] path ptr, [rsp+8] path len - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // preserve path ptr across the wrapper-scheme probe - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // preserve path len across the wrapper-scheme probe - emitter.instruction("mov rdi, rax"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov rsi, rdx"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // rax = 1 when the scheme matches a registered wrapper - emitter.instruction("test rax, rax"); // matched a registered wrapper scheme? - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore path ptr for the chosen helper - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore path len for the chosen helper - emitter.instruction(&format!("jnz {}", wrapper)); // registered wrapper scheme → wrapper path-op - abi::emit_call_label(emitter, libc_helper); // normal path: libc filesystem helper (path in rax/rdx) - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rdi, rax"); // user_wrapper_path_op arg0 = path ptr - emitter.instruction("mov rsi, rdx"); // user_wrapper_path_op arg1 = path len - emitter.instruction(&format!("mov rdx, {}", vtable_slot)); // arg2 = the method's vtable slot index - emitter.instruction("xor ecx, ecx"); // arg3 = 0 (mode/options default) - emitter.instruction("xor r8d, r8d"); // arg4 = 0 (options default) - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's path method - emitter.label(&after); - emitter.instruction("add rsp, 16"); // release the scratch frame - } - } -} - -/// Emits the wrapper-vs-filesystem dispatch for a string-named ownership metadata -/// change — `chown`/`chgrp` with a user/group NAME instead of a numeric id. -/// -/// Precondition: the path string was just pushed (`stp x1,x2,[sp,#-16]!` on -/// AArch64, `emit_push_reg_pair(rax,rdx)` on x86_64) and the name string is in the -/// string-result registers (`x1`/`x2` on AArch64, `rax`/`rdx` on x86_64). The -/// wrapper is probed FIRST so the libc fallback keeps the raw name: a registered -/// scheme boxes the name as `Mixed` and calls -/// `__rt_user_wrapper_path_op(path, len, slot=14, option, boxed_value)` invoking -/// `stream_metadata($path, $option, $value)` (releasing the boxed value after); a -/// non-wrapper path calls `libc_helper(path, name_ptr, name_len)` -/// (`__rt_chown_user` / `__rt_chgrp_group`). Bool result in `x0` / `rax`. -pub fn emit_owner_group_name_wrapper_dispatch( - emitter: &mut Emitter, - ctx: &mut Context, - option: usize, - libc_helper: &str, -) { - let wrapper = ctx.next_label("meta_name_wrapper"); - let after = ctx.next_label("meta_name_after"); - match emitter.target.arch { - Arch::AArch64 => { - // On entry: path at [sp,#0]/[sp,#8] (caller's push), name in x1/x2. - emitter.instruction("sub sp, sp, #16"); // name scratch: [sp,#0] name ptr, [sp,#8] name len (path now at [sp,#16]/[sp,#24]) - emitter.instruction("str x1, [sp, #0]"); // save the name pointer - emitter.instruction("str x2, [sp, #8]"); // save the name length - emitter.instruction("ldr x0, [sp, #16]"); // path_is_wrapper arg0 = path ptr - emitter.instruction("ldr x1, [sp, #24]"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // x0 = 1 when the scheme matches a registered wrapper - emitter.instruction(&format!("cbnz x0, {}", wrapper)); // registered wrapper scheme -> stream_metadata - emitter.instruction("ldr x1, [sp, #16]"); // libc path ptr -> x1 - emitter.instruction("ldr x2, [sp, #24]"); // libc path len -> x2 - emitter.instruction("ldr x3, [sp, #0]"); // libc name ptr -> x3 - emitter.instruction("ldr x4, [sp, #8]"); // libc name len -> x4 - emitter.instruction("add sp, sp, #32"); // release the name scratch and the caller's path push - abi::emit_call_label(emitter, libc_helper); // normal path: resolve the name and call libc chown - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("ldr x1, [sp, #0]"); // reload the name pointer for boxing - emitter.instruction("ldr x2, [sp, #8]"); // reload the name length for boxing - emit_box_string_as_mixed(emitter); // box $value as mixed -> x0 = owned Mixed(string) - emitter.instruction("str x0, [sp, #0]"); // stash the boxed value pointer (name ptr slot reused) - emitter.instruction("ldr x0, [sp, #16]"); // wrapper path ptr -> x0 - emitter.instruction("ldr x1, [sp, #24]"); // wrapper path len -> x1 - emitter.instruction(&format!("mov x2, #{}", STREAM_METADATA_SLOT)); // stream_metadata vtable slot - emitter.instruction(&format!("mov x3, #{}", option)); // option = STREAM_META_OWNER_NAME/GROUP_NAME - emitter.instruction("ldr x4, [sp, #0]"); // value = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's stream_metadata - emitter.instruction("str x0, [sp, #8]"); // stash the bool result (free name-len slot) across the value release - emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed value pointer - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the boxed $value (caller owns; the method borrowed it) - emitter.instruction("ldr x0, [sp, #8]"); // restore the bool result - emitter.instruction("add sp, sp, #32"); // release the name scratch and the caller's path push - emitter.label(&after); - } - Arch::X86_64 => { - // On entry: path at [rsp+0]/[rsp+8] (caller's push), name in rax/rdx. - emitter.instruction("sub rsp, 16"); // name scratch: [rsp+0] name ptr, [rsp+8] name len (path now at [rsp+16]/[rsp+24]) - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the name pointer - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the name length - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // rax = 1 when the scheme matches a registered wrapper - emitter.instruction("test rax, rax"); // matched a registered wrapper scheme? - emitter.instruction(&format!("jnz {}", wrapper)); // registered wrapper scheme -> stream_metadata - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // libc path ptr -> rax - emitter.instruction("mov rdx, QWORD PTR [rsp + 24]"); // libc path len -> rdx - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // libc name ptr -> rdi - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // libc name len -> rsi - emitter.instruction("add rsp, 32"); // release the name scratch and the caller's path push - abi::emit_call_label(emitter, libc_helper); // normal path: resolve the name and call libc chown - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // reload the name pointer for boxing - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // reload the name length for boxing - emit_box_string_as_mixed(emitter); // box $value as mixed -> rax = owned Mixed(string) - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // stash the boxed value pointer (name ptr slot reused) - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // wrapper path ptr -> rdi - emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // wrapper path len -> rsi - emitter.instruction(&format!("mov rdx, {}", STREAM_METADATA_SLOT)); // stream_metadata vtable slot - emitter.instruction(&format!("mov rcx, {}", option)); // option = STREAM_META_OWNER_NAME/GROUP_NAME - emitter.instruction("mov r8, QWORD PTR [rsp + 0]"); // value = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's stream_metadata - emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // stash the bool result (free name-len slot) across the value release - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // reload the boxed value pointer - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the boxed $value (caller owns; the method borrowed it) - emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // restore the bool result - emitter.instruction("add rsp, 32"); // release the name scratch and the caller's path push - emitter.label(&after); - } - } -} - -/// Emits the wrapper-vs-filesystem dispatch for an integer-valued ownership -/// metadata change — `chown`/`chgrp` with a numeric uid/gid. -/// -/// Precondition: the path string was just pushed (`stp x1,x2,[sp,#-16]!` on -/// AArch64, `emit_push_reg_pair(rax, rdx)` on x86_64) and the uid/gid value is in -/// `x0`/`rax`. When the path scheme matches a registered wrapper, routes to -/// `__rt_user_wrapper_path_op(path, len, slot=14, option, value)` invoking the -/// wrapper's `stream_metadata($path, $option, $value)`; otherwise calls the libc -/// `__rt_chown(path, uid, gid)` with the value placed in uid (`option`==OWNER) or -/// gid (`option`==GROUP) and the other field left at -1. Bool result in `x0`/`rax`. -pub fn emit_owner_group_wrapper_dispatch(emitter: &mut Emitter, ctx: &mut Context, option: usize) { - let wrapper = ctx.next_label("meta_owngrp_wrapper"); - let after = ctx.next_label("meta_owngrp_after"); - let is_owner = option == STREAM_META_OWNER; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x9, x0"); // stash the uid/gid value across the path pop - emitter.instruction("ldp x1, x2, [sp], #16"); // restore path ptr/len from the caller's push - emitter.instruction("sub sp, sp, #32"); // scratch: [sp,#0] path ptr, [sp,#8] path len, [sp,#16] value - emitter.instruction("str x1, [sp, #0]"); // save path ptr - emitter.instruction("str x2, [sp, #8]"); // save path len - emitter.instruction("str x9, [sp, #16]"); // save the uid/gid value across the probe - emitter.instruction("mov x0, x1"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov x1, x2"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // x0 = 1 when the scheme matches a registered wrapper - emitter.instruction(&format!("cbnz x0, {}", wrapper)); // registered wrapper scheme → stream_metadata - emitter.instruction("ldr x1, [sp, #0]"); // libc path ptr → x1 - emitter.instruction("ldr x2, [sp, #8]"); // libc path len → x2 - if is_owner { - emitter.instruction("ldr x3, [sp, #16]"); // uid = value - emitter.instruction("mov x4, #-1"); // gid = -1 (leave group unchanged) - } else { - emitter.instruction("mov x3, #-1"); // uid = -1 (leave owner unchanged) - emitter.instruction("ldr x4, [sp, #16]"); // gid = value - } - emitter.instruction("add sp, sp, #32"); // release the scratch frame before the call - abi::emit_call_label(emitter, "__rt_chown"); // normal path: libc chown(path, uid, gid) - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("ldr x0, [sp, #16]"); // reload the uid/gid integer - emit_box_int_as_mixed(emitter); // box $value as mixed → x0 = owned Mixed(int) - emitter.instruction("str x0, [sp, #16]"); // stash the boxed value pointer (value slot reused) - emitter.instruction("ldr x0, [sp, #0]"); // wrapper path ptr → x0 - emitter.instruction("ldr x1, [sp, #8]"); // wrapper path len → x1 - emitter.instruction(&format!("mov x2, #{}", STREAM_METADATA_SLOT)); // stream_metadata vtable slot - emitter.instruction(&format!("mov x3, #{}", option)); // option = STREAM_META_OWNER/GROUP - emitter.instruction("ldr x4, [sp, #16]"); // value = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's stream_metadata - emitter.instruction("str x0, [sp, #0]"); // stash the bool result across the value release - emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed value pointer - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the boxed $value (caller owns; the method borrowed it) - emitter.instruction("ldr x0, [sp, #0]"); // restore the bool result - emitter.instruction("add sp, sp, #32"); // release the scratch frame - emitter.label(&after); - } - Arch::X86_64 => { - emitter.instruction("mov r9, rax"); // stash the uid/gid value across the path pop - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore path ptr/len from the caller's push - emitter.instruction("sub rsp, 32"); // scratch: [rsp+0] path ptr, [rsp+8] path len, [rsp+16] value - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save path ptr - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save path len - emitter.instruction("mov QWORD PTR [rsp + 16], r9"); // save the uid/gid value across the probe - emitter.instruction("mov rdi, rax"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov rsi, rdx"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // rax = 1 when the scheme matches a registered wrapper - emitter.instruction("test rax, rax"); // matched a registered wrapper scheme? - emitter.instruction(&format!("jnz {}", wrapper)); // registered wrapper scheme → stream_metadata - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // libc path ptr → rax - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // libc path len → rdx - if is_owner { - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // uid = value - emitter.instruction("mov rsi, -1"); // gid = -1 (leave group unchanged) - } else { - emitter.instruction("mov rdi, -1"); // uid = -1 (leave owner unchanged) - emitter.instruction("mov rsi, QWORD PTR [rsp + 16]"); // gid = value - } - emitter.instruction("add rsp, 32"); // release the scratch frame before the call - abi::emit_call_label(emitter, "__rt_chown"); // normal path: libc chown(path, uid, gid) - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the uid/gid integer - emit_box_int_as_mixed(emitter); // box $value as mixed → rax = owned Mixed(int) - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // stash the boxed value pointer (value slot reused) - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // wrapper path ptr → rdi - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // wrapper path len → rsi - emitter.instruction(&format!("mov rdx, {}", STREAM_METADATA_SLOT)); // stream_metadata vtable slot - emitter.instruction(&format!("mov rcx, {}", option)); // option = STREAM_META_OWNER/GROUP - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // value = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's stream_metadata - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // stash the bool result across the value release - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the boxed value pointer - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the boxed $value (caller owns; the method borrowed it) - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore the bool result - emitter.instruction("add rsp, 32"); // release the scratch frame - emitter.label(&after); - } - } -} diff --git a/src/codegen/builtins/io/pathinfo.rs b/src/codegen/builtins/io/pathinfo.rs deleted file mode 100644 index 493faa5852..0000000000 --- a/src/codegen/builtins/io/pathinfo.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Purpose: -//! Emits PHP `pathinfo` path-oriented builtin calls. -//! Marshals path strings into runtime helpers that normalize, split, or enumerate filesystem paths. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returned strings and arrays must use runtime allocation/layout compatible with PHP false-on-failure behavior. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{BinOp, Expr, ExprKind}; -use crate::types::PhpType; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits code for PHP's `pathinfo($path, $options)` builtin. -/// -/// `args[0]` is the path string (emitted first, returning ptr/len in x0/x1 or rax/rdx). -/// `args[1]`, if present, is the options flag. -/// -/// Returns `PhpType::AssocArray{Str, Str}` when no flag or `PATHINFO_ALL` is determined at -/// compile time, `PhpType::Str` for a static single-flag value, or `PhpType::Mixed` when the -/// flag is dynamic or involves runtime-only constants. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("pathinfo()"); - emit_expr(&args[0], emitter, ctx, data); - let static_flag = pathinfo_static_flag_value(args.get(1)); - if args.len() == 1 || static_flag == Some(15) { - // No-flag form: build the associative array via the runtime helper. - abi::emit_call_label(emitter, "__rt_pathinfo_array"); // call the runtime helper that builds the dirname/basename/extension/filename hash - // The hash pointer comes back in x0 / rax — that is already the - // standard integer-result register used everywhere else for hash-typed - // expression results. - return Some(PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Str), - }); - } - if static_flag.is_none() { - emit_dynamic_pathinfo(&args[1], emitter, ctx, data); - return Some(PhpType::Mixed); - } - // Single-flag form. - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the path ptr/len while the flag expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x0"); // move the flag value into the runtime's flag register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the path ptr/len after evaluating the flag expression - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the path ptr/len while the flag expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the flag value into the x86_64 runtime flag register - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the path ptr/len after evaluating the flag expression - } - } - abi::emit_call_label(emitter, "__rt_pathinfo_str"); // call the target-aware single-flag runtime helper that returns the requested component - Some(PhpType::Str) -} - -/// Emits code for a runtime-dynamic `pathinfo($path, $flag)` call. -/// -/// The path ptr/len is already in registers from emitting `args[0]`. This function evaluates -/// the flag expression, then dispatches to either `__rt_pathinfo_str` (single component) or -/// `__rt_pathinfo_array` (PATHINFO_ALL), boxing the result as `PhpType::Mixed`. For non-all -/// flags the returned string is boxed as a mixed scalar; for PATHINFO_ALL the associative -/// array is boxed as a mixed container. -fn emit_dynamic_pathinfo( - flag: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let array_label = ctx.next_label("pathinfo_dynamic_array"); - let done_label = ctx.next_label("pathinfo_dynamic_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the path ptr/len while the runtime flag expression is evaluated - emit_expr(flag, emitter, ctx, data); - emitter.instruction("mov x3, x0"); // keep the evaluated flag in the pathinfo string-helper flag register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the path ptr/len for whichever runtime branch is selected - emitter.instruction("cmp x3, #15"); // does the runtime flag request PATHINFO_ALL exactly? - emitter.instruction(&format!("b.eq {}", array_label)); // runtime PATHINFO_ALL must return the associative-array shape - abi::emit_call_label(emitter, "__rt_pathinfo_str"); // compute the requested component string for all non-all runtime flags - emitter.instruction("mov x0, #1"); // runtime tag 1 = string for the mixed boxing helper - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist and box the string branch as a mixed result - emitter.instruction(&format!("b {}", done_label)); // skip the array-shape branch after boxing the string result - emitter.label(&array_label); - abi::emit_call_label(emitter, "__rt_pathinfo_array"); // build the full associative array for runtime PATHINFO_ALL - box_owned_pathinfo_array_as_mixed(emitter); - emitter.label(&done_label); - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the path ptr/len while the runtime flag expression is evaluated - emit_expr(flag, emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // keep the evaluated flag in the x86_64 pathinfo flag register - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the path ptr/len for the selected runtime branch - emitter.instruction("cmp rdi, 15"); // does the runtime flag request PATHINFO_ALL exactly? - emitter.instruction(&format!("je {}", array_label)); // runtime PATHINFO_ALL must return the associative-array shape - abi::emit_call_label(emitter, "__rt_pathinfo_str"); // compute the requested component string for all non-all runtime flags - emitter.instruction("mov rdi, rax"); // pass the component string pointer as the mixed payload low word - emitter.instruction("mov rsi, rdx"); // pass the component string length as the mixed payload high word - emitter.instruction("mov eax, 1"); // runtime tag 1 = string for the mixed boxing helper - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist and box the string branch as a mixed result - emitter.instruction(&format!("jmp {}", done_label)); // skip the array-shape branch after boxing the string result - emitter.label(&array_label); - abi::emit_call_label(emitter, "__rt_pathinfo_array"); // build the full associative array for runtime PATHINFO_ALL - box_owned_pathinfo_array_as_mixed(emitter); - emitter.label(&done_label); - } - } -} - -/// Boxes a freshly owned pathinfo hash as a `PhpType::Mixed` cell in the runtime heap. -/// -/// The owned hash pointer is consumed from x0/rax (caller's result register). Allocates a -/// 24-byte mixed cell (tag + two payload words), stamps it with heap kind 5, stores the -/// associative-array tag (5) at mixed[0] and the hash pointer at mixed[8], and zero-fills -/// mixed[16] (associative-array payloads do not use the high word). Preserves the hash -/// pointer by pushing/popping through a scratch register so the allocation itself does not -/// consume the owner. -fn box_owned_pathinfo_array_as_mixed(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, "x0"); // preserve the freshly owned hash while allocating the mixed cell - emitter.instruction("mov x0, #24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the boxed runtime result cell - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the heap allocation as a mixed cell - emitter.instruction("mov x9, #5"); // runtime tag 5 = associative array payload - emitter.instruction("str x9, [x0]"); // store the array tag at mixed[0] - abi::emit_pop_reg(emitter, "x10"); // reload the owned pathinfo hash pointer - emitter.instruction("str x10, [x0, #8]"); // store the hash pointer without retaining the fresh owner - emitter.instruction("str xzr, [x0, #16]"); // associative-array mixed payloads do not use a high word - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the freshly owned hash while allocating the mixed cell - emitter.instruction("mov rax, 24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the boxed runtime result cell - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5)); // materialize the mixed-cell heap kind word with the x86_64 heap marker - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap allocation as a mixed cell - emitter.instruction("mov QWORD PTR [rax], 5"); // store runtime tag 5 = associative array payload - abi::emit_pop_reg(emitter, "r10"); // reload the owned pathinfo hash pointer - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the hash pointer without retaining the fresh owner - emitter.instruction("mov QWORD PTR [rax + 16], 0"); // associative-array mixed payloads do not use a high word - } - } -} - -/// Statically evaluates a `pathinfo` options argument at compile time. -/// -/// Walks the expression tree to extract a literal integer or known `PATHINFO_*` constants. -/// Handles negation and bitwise AND/OR/XOR combinations of static operands. Returns `None` -/// when the flag cannot be resolved statically (e.g., variable, function call, or unsupported -/// operator), allowing the caller to emit runtime-dynamic dispatch. -fn pathinfo_static_flag_value(flag: Option<&Expr>) -> Option { - match flag.map(|expr| &expr.kind) { - Some(ExprKind::IntLiteral(value)) => Some(*value), - Some(ExprKind::ConstRef(name)) => match name.as_str() { - "PATHINFO_DIRNAME" => Some(1), - "PATHINFO_BASENAME" => Some(2), - "PATHINFO_EXTENSION" => Some(4), - "PATHINFO_FILENAME" => Some(8), - "PATHINFO_ALL" => Some(15), - _ => None, - }, - Some(ExprKind::Negate(inner)) => { - pathinfo_static_flag_value(Some(inner.as_ref())).map(|value| -value) - } - Some(ExprKind::BinaryOp { left, op, right }) => { - let left = pathinfo_static_flag_value(Some(left.as_ref()))?; - let right = pathinfo_static_flag_value(Some(right.as_ref()))?; - match op { - BinOp::BitAnd => Some(left & right), - BinOp::BitOr => Some(left | right), - BinOp::BitXor => Some(left ^ right), - _ => None, - } - } - _ => None, - } -} diff --git a/src/codegen/builtins/io/pclose.rs b/src/codegen/builtins/io/pclose.rs deleted file mode 100644 index 9c42cda96f..0000000000 --- a/src/codegen/builtins/io/pclose.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Purpose: -//! Emits PHP `pclose` calls. -//! Closes a process pipe opened by `popen()` and yields the child status. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The descriptor is unboxed from the stream resource and handed to the -//! `__rt_pclose` runtime helper, which calls libc `pclose`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `pclose()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("pclose()"); - emit_stream_fd_arg("pclose", &args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the descriptor into the runtime-helper argument register - } - abi::emit_call_label(emitter, "__rt_pclose"); - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/phar_stream.rs b/src/codegen/builtins/io/phar_stream.rs deleted file mode 100644 index 103ca7ce08..0000000000 --- a/src/codegen/builtins/io/phar_stream.rs +++ /dev/null @@ -1,437 +0,0 @@ -//! Purpose: -//! Lowers `fopen()` calls whose path is a `phar://` URL by reading the named -//! entry out of a PHAR archive at compile time and materializing it as a -//! readable stream through the shared `__rt_data_stream` runtime helper. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `phar://`. -//! -//! Key details: -//! - The URL must be a string literal. The archive file is read and parsed at -//! compile time (relative paths resolve against the compiler's working -//! directory), the requested entry's uncompressed bytes are embedded in the -//! binary's data section, and reads come from that embedded copy — mirroring -//! how `data://` lowers a literal payload. Read-only entries from native PHAR, -//! tar-based PHAR, and zip-based PHAR containers are supported; native gzip -//! (raw-DEFLATE), native bzip2, and zip deflate entries are decompressed at -//! compile time. -//! - A missing archive or a missing entry lowers to PHP `false`, matching a -//! failed `fopen()`. -//! - Write-mode literal URLs seed the shared PHAR write runtime. The splitter -//! recognizes `.phar/`, `.tar/`, and `.zip/` archive boundaries so the -//! runtime bridge can preserve the requested archive family. -//! - PHAR binary layout parsed here (all integers little-endian): a PHP stub -//! ending in `__HALT_COMPILER();`, then the manifest -//! (`manifest_len`, `num_files`, 2-byte api version, 4-byte global flags, -//! `alias_len`+alias, `meta_len`+metadata, then per file: -//! `name_len`+name, `uncompressed_size`, timestamp, `compressed_size`, crc32, -//! `flags`, `meta_len`+metadata), then the file-data section beginning at -//! `manifest_start + 4 + manifest_len`, holding each entry's bytes -//! consecutively in manifest order. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// PHAR per-entry flag bit: the entry's data is stored as raw DEFLATE (what PHP -/// writes for gzip-compressed entries — no zlib or gzip header). -const PHAR_FLAG_GZIP: u32 = 0x0000_1000; -/// PHAR per-entry flag bit: the entry's data is bzip2 compressed. -const PHAR_FLAG_BZIP2: u32 = 0x0000_2000; - -/// Emits a `fopen("phar://...", ...)` call. The path is known to be a string -/// literal beginning with `phar://`. Mirrors `data_stream::emit`: the resolved -/// entry bytes are embedded and served through `__rt_data_stream`. -pub fn emit( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() phar:// stream"); - // Write/append/create modes lower to the PHAR write runtime/bridge, which - // can update native PHAR, tar, and ZIP archives while preserving siblings. - if let ExprKind::StringLiteral(mode) = &args[1].kind { - if is_phar_write_mode(mode) { - return emit_write(args, emitter, ctx, data); - } - } - let bytes = match &args[0].kind { - ExprKind::StringLiteral(path) => extract_phar_entry(path), - _ => None, - }; - // Read-mode literal phar:// URLs embed the resolved payload; optional fopen - // args are still evaluated for PHP-visible side effects. - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - match bytes { - Some(payload) => { - let (symbol, len) = data.add_string(&payload); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x0", &symbol); - emitter.instruction(&format!("mov x1, #{}", len)); // embedded entry length - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &symbol); - emitter.instruction(&format!("mov rsi, {}", len)); // embedded entry length - } - } - abi::emit_call_label(emitter, "__rt_data_stream"); // build the readable phar entry descriptor - } - None => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // unresolved phar:// entry lowers to PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // unresolved phar:// entry lowers to PHP false - }, - } - super::fopen::box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Returns true for `fopen()` modes that open a `phar://` entry for writing. -/// `w`/`a`/`c`/`x` and their `+` variants use the runtime write bridge, while -/// `r`/`r+` use the read path. -fn is_phar_write_mode(mode: &str) -> bool { - matches!( - mode.as_bytes().first(), - Some(b'w') | Some(b'a') | Some(b'c') | Some(b'x') - ) -} - -/// Splits a `phar:///` write URL into `(archive_path, entry)`. -/// Unlike the read path the archive need not exist yet, so the split happens at -/// the first `.phar/`, `.tar/`, or `.zip/` boundary; if none is present it -/// falls back to the longest existing-file prefix. Returns `None` when neither -/// rule yields an entry. -pub(crate) fn resolve_write_target(url: &str) -> Option<(String, String)> { - let rest = url.strip_prefix("phar://")?; - for suffix in [".phar/", ".tar/", ".zip/"] { - if let Some(idx) = rest.find(suffix) { - let archive_end = idx + suffix.len() - 1; - let archive = &rest[..archive_end]; - let entry = &rest[archive_end + 1..]; - if !entry.is_empty() { - return Some((archive.to_string(), entry.to_string())); - } - } - } - let (archive, entry) = split_archive_entry(rest)?; - let entry = entry.strip_prefix('/').unwrap_or(entry); - Some((archive.to_string(), entry.to_string())) -} - -/// Emits the `fopen("phar://...", "w")` write path. The output archive path and -/// the single-entry template are embedded at compile time; the runtime -/// `__rt_phar_write_open` seeds the in-memory archive buffer, `fwrite` appends -/// the entry content, and `fclose` runs `__rt_phar_write_finalize`. Returns the -/// synthetic descriptor `0x50000000` (boxed as a resource), or PHP false when -/// the write target cannot be resolved. -fn emit_write( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - // The mode is a string literal here; evaluate it plus optional args for - // parity with the read path. - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - let target = match &args[0].kind { - ExprKind::StringLiteral(url) => resolve_write_target(url), - _ => None, - }; - match target { - Some((archive, entry)) => { - let tpl = build_phar_write_template(&entry); - let (tpl_sym, tpl_len) = data.add_string(&tpl); - let (path_sym, path_len) = data.add_string(archive.as_bytes()); - // The phar signature is computed with elephc-crypto SHA1, so publish - // its entry pointers before __rt_phar_write_finalize runs at fclose(). - crate::codegen::builtins::hash_crypto::publish_elephc_crypto_function_pointers(emitter); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", &path_sym); - abi::emit_symbol_address(emitter, "x10", "_phar_write_path_ptr"); - emitter.instruction("str x9, [x10]"); // record the on-disk archive path pointer - emitter.instruction(&format!("mov x9, #{}", path_len)); // archive path length - abi::emit_symbol_address(emitter, "x10", "_phar_write_path_len"); - emitter.instruction("str x9, [x10]"); // record the on-disk archive path length - abi::emit_symbol_address(emitter, "x0", &tpl_sym); - emitter.instruction(&format!("mov x1, #{}", tpl_len)); // template prefix length - abi::emit_call_label(emitter, "__rt_phar_write_open"); - emitter.instruction("mov w0, #0x5000"); // low half of the phar-write descriptor 0x50000000 - emitter.instruction("lsl w0, w0, #16"); // form the full 0x50000000 phar-write descriptor - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", &path_sym); - abi::emit_symbol_address(emitter, "r10", "_phar_write_path_ptr"); - emitter.instruction("mov QWORD PTR [r10], r9"); // record the on-disk archive path pointer - abi::emit_symbol_address(emitter, "r10", "_phar_write_path_len"); - emitter.instruction(&format!("mov QWORD PTR [r10], {}", path_len)); // record the archive path length - abi::emit_symbol_address(emitter, "rdi", &tpl_sym); - emitter.instruction(&format!("mov rsi, {}", tpl_len)); // template prefix length - abi::emit_call_label(emitter, "__rt_phar_write_open"); - emitter.instruction("mov eax, 0x50000000"); // the phar-write synthetic descriptor - } - } - } - None => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // unresolved phar:// write target → PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // unresolved phar:// write target → PHP false - }, - } - super::fopen::box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Emits `file_put_contents("phar://archive/entry", $data)` as a one-shot phar -/// write: it reuses the same runtime as the `fopen`+`fwrite`+`fclose` path — -/// `__rt_phar_write_open` seeds the in-memory archive with the entry template, -/// `__rt_phar_write_append` appends the data, and `__rt_phar_write_finalize` -/// assembles, SHA1-signs, and writes the archive. Returns `Int` (the byte count -/// written), or `None` when `url` is not a resolvable phar write target (the -/// caller then falls back to a normal file write). -pub(crate) fn emit_file_put_contents_write( - url: &str, - data_arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let (archive, entry) = resolve_write_target(url)?; - let tpl = build_phar_write_template(&entry); - let (tpl_sym, tpl_len) = data.add_string(&tpl); - let (path_sym, path_len) = data.add_string(archive.as_bytes()); - // The phar signature is computed with elephc-crypto SHA1, so publish its - // entry pointers before the inline finalize signs the archive. - crate::codegen::builtins::hash_crypto::publish_elephc_crypto_function_pointers(emitter); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", &path_sym); - abi::emit_symbol_address(emitter, "x10", "_phar_write_path_ptr"); - emitter.instruction("str x9, [x10]"); // record the on-disk archive path pointer - emitter.instruction(&format!("mov x9, #{}", path_len)); // archive path length - abi::emit_symbol_address(emitter, "x10", "_phar_write_path_len"); - emitter.instruction("str x9, [x10]"); // record the on-disk archive path length - abi::emit_symbol_address(emitter, "x0", &tpl_sym); - emitter.instruction(&format!("mov x1, #{}", tpl_len)); // template prefix length - abi::emit_call_label(emitter, "__rt_phar_write_open"); // seed the archive buffer with the entry template - emit_expr(data_arg, emitter, ctx, data); // $data → x1 = ptr, x2 = len (string ABI) - abi::emit_call_label(emitter, "__rt_phar_write_append"); // append the entry content; x0 = byte count - abi::emit_push_reg(emitter, "x0"); // preserve the byte count across finalize - abi::emit_call_label(emitter, "__rt_phar_write_finalize"); // assemble + SHA1-sign + write the archive - abi::emit_pop_reg(emitter, "x0"); // restore the byte count as the file_put_contents result - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", &path_sym); - abi::emit_symbol_address(emitter, "r10", "_phar_write_path_ptr"); - emitter.instruction("mov QWORD PTR [r10], r9"); // record the on-disk archive path pointer - abi::emit_symbol_address(emitter, "r10", "_phar_write_path_len"); - emitter.instruction(&format!("mov QWORD PTR [r10], {}", path_len)); // record the archive path length - abi::emit_symbol_address(emitter, "rdi", &tpl_sym); - emitter.instruction(&format!("mov rsi, {}", tpl_len)); // template prefix length - abi::emit_call_label(emitter, "__rt_phar_write_open"); // seed the archive buffer with the entry template - emit_expr(data_arg, emitter, ctx, data); // $data → rax = ptr, rdx = len (string ABI) - emitter.instruction("mov rsi, rax"); // append payload pointer (rdx already holds the length) - abi::emit_call_label(emitter, "__rt_phar_write_append"); // append the entry content; rax = byte count - abi::emit_push_reg(emitter, "rax"); // preserve the byte count across finalize - abi::emit_call_label(emitter, "__rt_phar_write_finalize"); // assemble + SHA1-sign + write the archive - abi::emit_pop_reg(emitter, "rax"); // restore the byte count as the file_put_contents result - } - } - Some(PhpType::Int) -} - -/// Resolves a `phar:///` URL to the entry's uncompressed bytes. -/// Splits the archive (the longest leading path that names an existing file) -/// from the inner entry, reads and parses the archive, and returns the entry -/// payload, or `None` on any failure. -pub(crate) fn extract_phar_entry(url: &str) -> Option> { - if let Some(bytes) = elephc_phar::extract_url_bytes(url.as_bytes()) { - return Some(bytes); - } - let rest = url.strip_prefix("phar://")?; - let (archive, entry) = split_archive_entry(rest)?; - let archive_bytes = std::fs::read(archive).ok()?; - let entry = entry.strip_prefix('/').unwrap_or(entry); - parse_phar_entry(&archive_bytes, entry) -} - -/// Splits a `phar://` body into `(archive_path, inner_entry)` by taking the -/// shortest `/`-delimited prefix that names an existing file as the archive — -/// the same disambiguation PHP uses to find where the archive ends and the -/// entry begins. Returns `None` if no prefix is an existing file. -fn split_archive_entry(rest: &str) -> Option<(&str, &str)> { - for (i, &c) in rest.as_bytes().iter().enumerate() { - if c == b'/' { - let candidate = &rest[..i]; - if std::path::Path::new(candidate).is_file() { - return Some((candidate, &rest[i + 1..])); - } - } - } - None -} - -/// Parses the native PHAR manifest in `data` and returns the uncompressed bytes -/// of `entry`, or `None` if the archive is malformed or the entry is absent. -fn parse_phar_entry(data: &[u8], entry: &str) -> Option> { - let halt = b"__HALT_COMPILER();"; - let halt_idx = find_subslice(data, halt)?; - let mut p = halt_idx + halt.len(); - // PHP writes `__HALT_COMPILER(); ?>\r\n`; skip each of those bytes in order - // when present, leaving `p` at the first manifest byte. - for &ch in &[b' ', b'?', b'>', b'\r', b'\n'] { - if data.get(p) == Some(&ch) { - p += 1; - } - } - - let manifest_start = p; - let manifest_len = le32(data, manifest_start)? as usize; - let data_section = manifest_start.checked_add(4)?.checked_add(manifest_len)?; - let num_files = le32(data, manifest_start + 4)?; - - // Skip the rest of the manifest header: api version (2) + global flags (4) + - // alias (len-prefixed) + manifest metadata (len-prefixed). - let mut q = manifest_start + 8 + 2 + 4; - let alias_len = le32(data, q)? as usize; - q = q.checked_add(4)?.checked_add(alias_len)?; - let meta_len = le32(data, q)? as usize; - q = q.checked_add(4)?.checked_add(meta_len)?; - - // Walk each entry, accumulating the running data-section offset so a matched - // entry's bytes can be sliced even when earlier entries precede it. - let mut data_offset = 0usize; - for _ in 0..num_files { - let name_len = le32(data, q)? as usize; - q += 4; - let name = data.get(q..q.checked_add(name_len)?)?; - q += name_len; - let uncompressed = le32(data, q)? as usize; - q += 4; // uncompressed size - q += 4; // timestamp - let compressed = le32(data, q)? as usize; - q += 4; // compressed size - q += 4; // crc32 - let flags = le32(data, q)?; - q += 4; - let entry_meta_len = le32(data, q)? as usize; - q = q.checked_add(4)?.checked_add(entry_meta_len)?; - - if name == entry.as_bytes() { - let start = data_section.checked_add(data_offset)?; - let stored = data.get(start..start.checked_add(compressed)?)?; - return decode_entry(stored, flags, uncompressed); - } - data_offset = data_offset.checked_add(compressed)?; - } - None -} - -/// Decodes a stored PHAR entry payload into its uncompressed bytes according to -/// the entry `flags`: raw-DEFLATE for gzip entries and bzip2 for bzip2 entries -/// (each verified against the entry's recorded `uncompressed` size), passthrough -/// for uncompressed entries, and `None` on a malformed compressed stream. -fn decode_entry(stored: &[u8], flags: u32, uncompressed: usize) -> Option> { - if flags & PHAR_FLAG_GZIP != 0 { - let mut out = Vec::with_capacity(uncompressed); - let mut decoder = flate2::read::DeflateDecoder::new(stored); - std::io::Read::read_to_end(&mut decoder, &mut out).ok()?; - if out.len() != uncompressed { - return None; // recorded size disagrees with the inflated length - } - Some(out) - } else if flags & PHAR_FLAG_BZIP2 != 0 { - let mut out = Vec::with_capacity(uncompressed); - let mut decoder = bzip2_rs::DecoderReader::new(stored); - std::io::Read::read_to_end(&mut decoder, &mut out).ok()?; - if out.len() != uncompressed { - return None; // recorded size disagrees with the decompressed length - } - Some(out) - } else { - Some(stored.to_vec()) - } -} - -/// Reads a little-endian `u32` at `off`, or `None` if fewer than 4 bytes remain. -fn le32(data: &[u8], off: usize) -> Option { - let b = data.get(off..off + 4)?; - Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) -} - -/// Returns the index of the first occurrence of `needle` in `hay`, or `None`. -fn find_subslice(hay: &[u8], needle: &[u8]) -> Option { - if needle.is_empty() || hay.len() < needle.len() { - return None; - } - hay.windows(needle.len()).position(|w| w == needle) -} - -/// Builds the native-PHAR prefix for a single uncompressed `entry`, stored at -/// `0644`. The global manifest flags set `PHAR_HDR_SIGNATURE` (0x10000), so the -/// archive declares an appended signature; `__rt_phar_write_finalize` computes a -/// SHA1 signature over the assembled bytes and appends the -/// `raw-sha1 ++ LE32(0x0002) ++ "GBMB"` trailer, making the archive readable by -/// real PHP (which requires a hash by default), not just elephc. The returned -/// bytes are everything up to the data section; the runtime appends the entry -/// content and patches the size/CRC fields, which sit at fixed negative offsets -/// from the end of the template (uncompressed at -24, compressed at -16, crc at -/// -12) — so `__rt_phar_write_finalize` derives them from the template length. -pub(crate) fn build_phar_write_template(entry: &str) -> Vec { - let name = entry.as_bytes(); - let mut out = Vec::new(); - out.extend_from_slice(b"\r\n"); - // manifest length = every byte after this LE32 up to the data section: - // num_files(4)+api(2)+flags(4)+alias_len(4)+meta_len(4) + the entry record - // (name_len(4)+name + uncomp(4)+ts(4)+comp(4)+crc(4)+flags(4)+emeta(4)). - let manifest_len = (18 + name.len() + 28) as u32; - out.extend_from_slice(&manifest_len.to_le_bytes()); - out.extend_from_slice(&1u32.to_le_bytes()); // num_files - out.extend_from_slice(&[0x11, 0x00]); // api version (1.1.0) - out.extend_from_slice(&0x0001_0000u32.to_le_bytes()); // global flags: PHAR_HDR_SIGNATURE (signed; trailer appended by finalize) - out.extend_from_slice(&0u32.to_le_bytes()); // alias length - out.extend_from_slice(&0u32.to_le_bytes()); // manifest metadata length - out.extend_from_slice(&(name.len() as u32).to_le_bytes()); // entry name length - out.extend_from_slice(name); // entry name - out.extend_from_slice(&0u32.to_le_bytes()); // uncompressed size (runtime patch, -24) - out.extend_from_slice(&0u32.to_le_bytes()); // timestamp - out.extend_from_slice(&0u32.to_le_bytes()); // compressed size (runtime patch, -16) - out.extend_from_slice(&0u32.to_le_bytes()); // crc32 (runtime patch, -12) - out.extend_from_slice(&0x0000_01a4u32.to_le_bytes()); // flags: mode 0644, uncompressed - out.extend_from_slice(&0u32.to_le_bytes()); // entry metadata length - out -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Round-trips the write-side template through the read-side parser: build a - /// template, simulate the runtime finalize (patch sizes, append content), - /// and confirm `parse_phar_entry` extracts the same bytes back. The reader - /// ignores CRC, so the crc field is left zero here. - #[test] - fn write_template_round_trips_through_reader() { - let content = b"hello from a written phar entry"; - let tpl = build_phar_write_template("dir/inner.txt"); - let tpl_len = tpl.len(); - let mut archive = tpl.clone(); - let len = (content.len() as u32).to_le_bytes(); - // finalize patches uncompressed at tpl_len-24 and compressed at tpl_len-16 - // (crc at tpl_len-12, left zero here because the reader ignores it). - archive[tpl_len - 24..tpl_len - 20].copy_from_slice(&len); - archive[tpl_len - 16..tpl_len - 12].copy_from_slice(&len); - archive.extend_from_slice(content); - - assert_eq!( - parse_phar_entry(&archive, "dir/inner.txt").as_deref(), - Some(&content[..]) - ); - assert!(parse_phar_entry(&archive, "absent.txt").is_none()); - } -} diff --git a/src/codegen/builtins/io/php_filter_stream.rs b/src/codegen/builtins/io/php_filter_stream.rs deleted file mode 100644 index 1af6c9301f..0000000000 --- a/src/codegen/builtins/io/php_filter_stream.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Purpose: -//! Lowers `fopen()` calls whose path is a `php://filter/...` URL. -//! Opens the underlying `resource=` stream (reusing all of `fopen`'s scheme -//! handling) and then attaches a built-in filter to its descriptor. -//! -//! Called from: -//! - `crate::codegen::builtins::io::fopen::emit()` when the path literal -//! begins with `php://filter/`. -//! -//! Key details: -//! - URL form: `php://filter/[read=|write=]/resource=`. A bare -//! filter (no `read=`/`write=`) applies to both directions (`STREAM_FILTER_ALL`). -//! - The filter name is mapped at compile time through [`super::stream_filter::filter_id`] -//! (`string.toupper`/`tolower`/`rot13`, etc.). The id is written into the per-fd -//! `_stream_read_filters` / `_stream_write_filters` byte tables, exactly like -//! `stream_filter_append`, so `__rt_fread`/`__rt_fwrite` apply it. -//! - elephc's filter model is single-filter-per-direction, so a chained -//! `read=F1|F2` list keeps only the first filter (documented limitation). -//! - Unparseable URL, missing `resource=`, or an unknown filter lower to PHP -//! `false` (matching PHP's `fopen()` failure for a bad filter spec). -//! - The underlying open is reused via `super::fopen::emit`, which already boxes -//! the descriptor as a resource Mixed cell; this wrapper only stamps the filter -//! table on the cell's fd and returns the same cell, so it does not re-box. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits a `fopen("php://filter/...", mode)` call. The path is known to be a -/// string literal beginning with `php://filter/`. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fopen() php://filter stream"); - let spec = match &args[0].kind { - ExprKind::StringLiteral(p) => p.strip_prefix("php://filter/"), - _ => None, - }; - let parsed = spec.and_then(parse_filter_url); - let (mode_bits, id, resource) = match parsed { - Some(v) => v, - None => { - super::fopen::emit_mode_and_ignored_optional_args(args, emitter, ctx, data); - return emit_false(emitter, ctx); - } - }; - - // Open the underlying resource with the caller's mode, reusing fopen's full - // scheme handling (plain paths, php://temp, data://, http://, ...). - let resource_expr = Expr { - kind: ExprKind::StringLiteral(resource), - span: args[0].span, - }; - let mut synthetic = vec![resource_expr, args[1].clone()]; - synthetic.extend(args.iter().skip(2).cloned()); - super::fopen::emit(name, &synthetic, emitter, ctx, data); - - // The result is a boxed Mixed cell: tag 9 (resource) on success, tag 3 - // (false) on open failure. Only stamp the filter table when it is a resource. - let done_label = ctx.next_label("phpf_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [x0]"); // boxed Mixed tag - emitter.instruction("cmp x9, #9"); // runtime tag 9 = resource? - emitter.instruction(&format!("b.ne {}", done_label)); // open failed (false) → return it unchanged - emitter.instruction("ldr x1, [x0, #8]"); // descriptor from the resource payload - if mode_bits & 1 != 0 { - abi::emit_symbol_address(emitter, "x9", "_stream_read_filters"); - emitter.instruction(&format!("mov w10, #{}", id)); // built-in filter id - emitter.instruction("strb w10, [x9, x1]"); // record the read filter for this descriptor - } - if mode_bits & 2 != 0 { - abi::emit_symbol_address(emitter, "x9", "_stream_write_filters"); - emitter.instruction(&format!("mov w10, #{}", id)); // built-in filter id - emitter.instruction("strb w10, [x9, x1]"); // record the write filter for this descriptor - } - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("mov r9, QWORD PTR [rax]"); // boxed Mixed tag - emitter.instruction("cmp r9, 9"); // runtime tag 9 = resource? - emitter.instruction(&format!("jne {}", done_label)); // open failed (false) → return it unchanged - emitter.instruction("mov rcx, QWORD PTR [rax + 8]"); // descriptor from the resource payload - if mode_bits & 1 != 0 { - abi::emit_symbol_address(emitter, "r8", "_stream_read_filters"); // read-filter table base - emitter.instruction(&format!("mov BYTE PTR [r8 + rcx], {}", id)); // record the read filter for this descriptor - } - if mode_bits & 2 != 0 { - abi::emit_symbol_address(emitter, "r8", "_stream_write_filters"); // write-filter table base - emitter.instruction(&format!("mov BYTE PTR [r8 + rcx], {}", id)); // record the write filter for this descriptor - } - emitter.label(&done_label); - } - } - Some(PhpType::Mixed) -} - -/// Parses the portion after `php://filter/` into `(mode_bits, filter_id, resource)`. -/// Returns `None` only for a malformed URL (missing `resource=`, empty resource, -/// or a self-referential `resource=php://filter...`). An unrecognized filter name -/// is NOT a hard error: PHP emits a warning but still returns the unfiltered -/// stream, so we report `mode_bits = 0` (no filter table write) to match. -fn parse_filter_url(spec: &str) -> Option<(i64, i64, String)> { - let (filter_part, resource) = spec.split_once("/resource=")?; - if resource.is_empty() || resource.starts_with("php://filter") { - return None; - } - let (mode_bits, list) = if let Some(f) = filter_part.strip_prefix("read=") { - (1i64, f) - } else if let Some(f) = filter_part.strip_prefix("write=") { - (2i64, f) - } else { - (3i64, filter_part) - }; - // Single-filter-per-direction model: keep the first filter of a `|` chain. - let first = list.split('|').next().unwrap_or(""); - match super::stream_filter::filter_id(first) { - Some(id) => Some((mode_bits, id, resource.to_string())), - // Unknown filter → open the resource unfiltered (PHP returns the stream). - None => Some((0, 0, resource.to_string())), - } -} - -/// Emits a boxed PHP `false`, reusing fopen's failure boxing (negative fd). -fn emit_false(emitter: &mut Emitter, ctx: &mut Context) -> Option { - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // negative fd sentinel → boxes PHP false - Arch::X86_64 => emitter.instruction("mov rax, -1"), // negative fd sentinel → boxes PHP false - } - super::fopen::box_fopen_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/popen.rs b/src/codegen/builtins/io/popen.rs deleted file mode 100644 index 82105ad2f7..0000000000 --- a/src/codegen/builtins/io/popen.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Purpose: -//! Emits PHP `popen` calls. -//! Opens a process pipe and yields it as a PHP stream resource. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_popen` helper returns the pipe descriptor or -1; the result is -//! boxed by the shared `box_socket_result` helper as `resource|false`. - -use crate::codegen::builtins::io::stream_socket_server::box_socket_result; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `popen()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("popen()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the command string while the mode string is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // mode pointer becomes the third helper argument - emitter.instruction("mov x4, x2"); // mode length becomes the fourth helper argument - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the command string into the first two helper arguments - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the command string while the mode string is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // mode length becomes the fourth SysV helper argument - emitter.instruction("mov rdx, rax"); // mode pointer becomes the third SysV helper argument - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the command string into the first two SysV helper arguments - } - } - abi::emit_call_label(emitter, "__rt_popen"); - box_socket_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/print_r.rs b/src/codegen/builtins/io/print_r.rs deleted file mode 100644 index 7d38d224d3..0000000000 --- a/src/codegen/builtins/io/print_r.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Purpose: -//! Emits PHP `print_r` diagnostic output for scalar, array, and mixed values. -//! Owns recursive/runtime-aware formatting needed for PHP-visible stdout text. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Output is a side effect, and refcounted values must be inspected without consuming ownership. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `print_r` diagnostic output to stdout for a single argument. -/// -/// # Arguments -/// - `_name`: The builtin name (unused, always `print_r`). -/// - `args`: Single expression to print. Must not be empty. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context carrying type information for the argument. -/// - `data`: Writable data section for string/symbol materialization. -/// -/// # Returns -/// Always returns `Some(PhpType::Void)`. -/// -/// # Behavior -/// - `bool`: Prints `"1"` for `true`, nothing for `false`. -/// - `void` (null): Prints nothing. -/// - `array`: Prints `"Array\n"` label only (recursion not supported). -/// - `int`, `float`, `string`: Same output as `echo` via `emit_write_stdout`. -/// -/// # Side effects -/// Writes to stdout. Does not consume ownership of the argument value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("print_r()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - match &ty { - PhpType::Bool => { - // print_r(true) prints "1", print_r(false) prints nothing - let skip = ctx.next_label("pr_skip"); - match emitter.target.arch { - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // test the boolean payload in the x86_64 integer result register before deciding whether print_r() should print anything - emitter.instruction(&format!("je {}", skip)); // skip the print_r() write path entirely when the boolean payload is false on x86_64 - } - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // test the boolean payload in the AArch64 integer result register before deciding whether print_r() should print anything - emitter.instruction(&format!("cbz x0, {}", skip)); // skip the print_r() write path entirely when the boolean payload is false on AArch64 - } - } - abi::emit_write_stdout(emitter, &ty); - emitter.label(&skip); - } - PhpType::Void => { - // print_r(null) prints nothing - } - PhpType::Array(elem_ty) => { - // -- print "Array\n" -- - let (lbl, len) = data.add_string(b"Array\n"); - abi::emit_symbol_address(emitter, abi::string_result_regs(emitter).0, &lbl); // materialize the borrowed \"Array\\n\" string pointer in the active target string-result pointer register - abi::emit_load_int_immediate(emitter, abi::string_result_regs(emitter).1, len as i64); // materialize the borrowed \"Array\\n\" string length in the paired target string-result length register - abi::emit_write_stdout(emitter, &PhpType::Str); // print the synthetic array label through the shared target-aware string stdout helper - let _ = elem_ty; - } - _ => { - // print_r for int, float, string — same as echo - abi::emit_write_stdout(emitter, &ty); - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/io/readdir.rs b/src/codegen/builtins/io/readdir.rs deleted file mode 100644 index e942a370fd..0000000000 --- a/src/codegen/builtins/io/readdir.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Purpose: -//! Emits PHP `readdir` calls. -//! Reads the next entry name from a directory handle opened by `opendir()`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_readdir` helper returns a null pointer at end-of-directory; that -//! case is boxed as PHP false, an entry name as a boxed string. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `readdir()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("readdir()"); - emit_stream_fd_arg("readdir", &args[0], emitter, ctx, data); - // -- dispatch: synthetic wrapper fd -> dir_readdir, else libc readdir -- - let wrapper = ctx.next_label("readdir_wrapper"); - let after = ctx.next_label("readdir_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper)); // dispatch into dir_readdir - abi::emit_call_label(emitter, "__rt_readdir"); // libc readdir -> name in x1/x2 - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - abi::emit_call_label(emitter, "__rt_user_wrapper_dir_readdir"); // wrapper dir_readdir - emitter.label(&after); - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper)); // dispatch into dir_readdir - emitter.instruction("mov rdi, rax"); // descriptor into the runtime-helper argument register - abi::emit_call_label(emitter, "__rt_readdir"); // libc readdir -> name in rax/rdx - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rdi, rax"); // descriptor into the runtime-helper argument register - abi::emit_call_label(emitter, "__rt_user_wrapper_dir_readdir"); // wrapper dir_readdir - emitter.label(&after); - } - } - box_string_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string without copying the buffer. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("readdir_false"); - let done_label = ctx.next_label("readdir_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means end of directory - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means end of directory - emitter.instruction(&format!("jz {}", false_label)); // box false at end of directory - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/readfile.rs b/src/codegen/builtins/io/readfile.rs deleted file mode 100644 index 28257a7dea..0000000000 --- a/src/codegen/builtins/io/readfile.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Purpose: -//! Emits PHP `readfile` builtin calls. -//! Streams a path to stdout through the runtime helper and returns bytes copied. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returns a boxed `int|false` so byte counts, including `0` and read-error -//! `-1`, stay distinguishable from an open failure. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `readfile($path)` builtin call. -/// -/// Arguments: -/// - `args[0]` must be a path expression (string). -/// -/// Behavior: -/// - Calls `__rt_readfile` runtime helper which opens the path and streams its -/// contents to stdout. The raw byte count is returned in the standard return -/// register. -/// - Boxes the result into a `PhpType::Mixed`: `-1` (open failure) → PHP `false`, -/// any `>= 0` byte count → PHP `int` (including `0` for empty files). -/// -/// Returns: -/// - Always returns `Some(PhpType::Mixed)` since readfile always produces a -/// boxed value regardless of success or failure. -/// -/// A `scheme://...` path whose scheme matches a registered userspace wrapper is -/// routed through `__rt_readfile_wrapper` (fopen + fpassthru + close); any other -/// path uses `__rt_readfile` (raw open + stream, which preserves the read-error -/// `-1` semantics for e.g. directories). Both return the count / `-2` convention -/// that `box_readfile_result` boxes into `int` / `false`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("readfile()"); - emit_expr(&args[0], emitter, ctx, data); - let wrapper = ctx.next_label("readfile_wrapper"); - let after = ctx.next_label("readfile_after"); - match emitter.target.arch { - Arch::AArch64 => { - // -- path string: x1 = ptr, x2 = len -- - emitter.instruction("sub sp, sp, #16"); // scratch: [sp,#0] path ptr, [sp,#8] path len - emitter.instruction("str x1, [sp, #0]"); // preserve path ptr across the wrapper-scheme probe - emitter.instruction("str x2, [sp, #8]"); // preserve path len across the wrapper-scheme probe - emitter.instruction("mov x0, x1"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov x1, x2"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // x0 = 1 when the scheme matches a registered wrapper - emitter.instruction("ldr x1, [sp, #0]"); // restore path ptr for the chosen readfile helper - emitter.instruction("ldr x2, [sp, #8]"); // restore path len for the chosen readfile helper - emitter.instruction(&format!("cbnz x0, {}", wrapper)); // registered wrapper scheme → wrapper readfile path - abi::emit_call_label(emitter, "__rt_readfile"); // normal path: raw open + stream to stdout - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - abi::emit_call_label(emitter, "__rt_readfile_wrapper"); // wrapper path: fopen + fpassthru + close - emitter.label(&after); - emitter.instruction("add sp, sp, #16"); // release the scratch frame - } - Arch::X86_64 => { - // -- path string: rax = ptr, rdx = len -- - emitter.instruction("sub rsp, 16"); // scratch: [rsp+0] path ptr, [rsp+8] path len - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // preserve path ptr across the wrapper-scheme probe - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // preserve path len across the wrapper-scheme probe - emitter.instruction("mov rdi, rax"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov rsi, rdx"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // rax = 1 when the scheme matches a registered wrapper - emitter.instruction("test rax, rax"); // matched a registered wrapper scheme? - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore path ptr for the chosen readfile helper - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore path len for the chosen readfile helper - emitter.instruction(&format!("jnz {}", wrapper)); // registered wrapper scheme → wrapper readfile path - abi::emit_call_label(emitter, "__rt_readfile"); // normal path: raw open + stream to stdout - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - abi::emit_call_label(emitter, "__rt_readfile_wrapper"); // wrapper path: fopen + fpassthru + close - emitter.label(&after); - emitter.instruction("add rsp, 16"); // release the scratch frame - } - } - box_readfile_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the raw readfile return value into a PHP `Mixed` value. -/// -/// Input (register convention): -/// - AArch64: byte count in `x0`, where `-2` indicates open failure. -/// - x86_64: byte count in `rax`, where `-2` indicates open failure. -/// -/// Output (ABI): -/// - `x0` / `rax`: boxed `Mixed` — `int` for byte counts `>= 0`, `false` for `-2`. -/// -/// For AArch64, uses `x9` as a scratch register for the sentinel comparison. -fn box_readfile_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("readfile_false"); - let done_label = ctx.next_label("readfile_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x9, #-2"); // runtime sentinel -2 means the file could not be opened - emitter.instruction("cmp x0, x9"); // did readfile() fail before streaming began? - emitter.instruction(&format!("b.eq {}", false_label)); // box PHP false for open failure - emitter.instruction("mov x1, x0"); // move the byte count into the mixed integer payload - emitter.instruction("mov x2, #0"); // integer mixed payloads do not use a high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = int - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the successful byte count, including zero for empty files - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for readfile() failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible readfile() failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, -2"); // runtime sentinel -2 means the file could not be opened - emitter.instruction(&format!("je {}", false_label)); // box PHP false for open failure - emitter.instruction("mov rdi, rax"); // move the byte count into the mixed integer payload - emitter.instruction("xor esi, esi"); // integer mixed payloads do not use a high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = int - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the successful byte count, including zero for empty files - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for readfile() failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible readfile() failure semantics - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/readline.rs b/src/codegen/builtins/io/readline.rs deleted file mode 100644 index 58ee10c003..0000000000 --- a/src/codegen/builtins/io/readline.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Purpose: -//! Emits PHP `readline` file input builtin calls. -//! Coordinates path or stream arguments with runtime helpers that allocate returned strings or arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Failure paths must distinguish PHP false from empty string or empty array results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `readline([prompt])` builtin. -/// -/// Takes one optional argument: the prompt string to write to stdout before reading. -/// When a prompt is provided, emits a `write` syscall (AArch64) or libc `write` call (x86_64) -/// to stdout before reading. Always reads one line from stdin via `__rt_fgets`. -/// -/// Returns `Some(PhpType::Str)` with the line excluding the trailing newline. -/// The runtime helper distinguishes PHP `false` (on EOF) from empty string. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("readline()"); - if args.len() == 1 { - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // fd = stdout - emitter.syscall(4); // write the prompt string to stdout before reading from stdin - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // move the prompt string pointer into the second SysV libc write() argument register - emitter.instruction("mov rdi, 1"); // pass stdout as the destination file descriptor for the prompt write - emitter.instruction("call write"); // write the prompt string through libc write() before reading from stdin - } - } - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // fd = stdin (fd 0) - } - Arch::X86_64 => { - emitter.instruction("xor edi, edi"); // fd = stdin (fd 0) in the first SysV runtime-helper argument register - } - } - abi::emit_call_label(emitter, "__rt_fgets"); // read one line from stdin through the target-aware stream helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/readlink.rs b/src/codegen/builtins/io/readlink.rs deleted file mode 100644 index eca196143a..0000000000 --- a/src/codegen/builtins/io/readlink.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Purpose: -//! Emits PHP `readlink` builtin calls. -//! Returns the canonical link target boxed as `Mixed` (`string|false`). -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The runtime helper returns either an owned string pointer/length pair or -//! `(0, 0)` on failure; this wrapper boxes both shapes into a Mixed cell so -//! `=== false` and string echo behave PHP-compatibly. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Magic high 32 bits of the x86_64 heap-cell marker word, forming -/// `(X86_64_HEAP_MAGIC_HI32 << 32) | kind` together with the runtime kind. -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Lowers a PHP `readlink()` call into target assembly. -/// -/// Evaluates the path argument, calls the `__rt_readlink` runtime helper, -/// then boxes the raw result (owned string pointer/length or 0/0 on failure) -/// into a `Mixed` cell so PHP's `=== false` and string-echo semantics work -/// correctly. Returns `PhpType::Mixed`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("readlink()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_readlink"); // libc readlink wrapper that returns an owned heap string (or 0/0 on failure) - box_readlink_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the raw `__rt_readlink` result into a `Mixed` cell. -/// -/// On success the runtime helper returns `(ptr, len)` in registers (x1/x2 on -/// ARM64, rax/rdx on x86_64); this function allocates a heap cell, stamps it -/// with the string tag, and stores the pointer/length words without copying -/// the owned buffer. On failure the helper returns a null pointer; this path -/// jumps to `__rt_mixed_from_value` to box PHP's `false` value. Both paths -/// converge at `done_label`. -fn box_readlink_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("readlink_false"); - let done_label = ctx.next_label("readlink_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means readlink() failed - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the successful link target while we allocate the mixed box - emitter.instruction("mov x0, #24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the mixed result cell for a successful string payload - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocated payload as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag in the mixed result - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the owned link target pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words without copying the owned readlink buffer - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path after a successful read - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for readlink() failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // null pointer means readlink() failed - emitter.instruction(&format!("jz {}", false_label)); // box false when the runtime helper reports failure - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the successful link target while we allocate the mixed box - emitter.instruction("mov rax, 24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the mixed result cell for a successful string payload - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5)); // materialize the mixed-cell heap kind word with the x86_64 heap marker - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocated payload as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag in the mixed result - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the owned link target pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer without copying the owned readlink buffer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length without copying the owned readlink buffer - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path after a successful read - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for readlink() failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/realpath.rs b/src/codegen/builtins/io/realpath.rs deleted file mode 100644 index 85cbdaca07..0000000000 --- a/src/codegen/builtins/io/realpath.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Purpose: -//! Emits PHP `realpath` path-oriented builtin calls. -//! Marshals path strings into runtime helpers that normalize, split, or enumerate filesystem paths. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returned strings and arrays must use runtime allocation/layout compatible with PHP false-on-failure behavior. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// x86_64 heap marker: high 32 bits of the magic `0x454C5048` signature baked into -/// mixed-cell heap kind words on this platform to distinguish allocated buffers from -/// inline/special values during runtime verification. -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits a call to the `realpath` builtin. -/// -/// Evaluates the path argument, calls `__rt_realpath` to resolve it via libc, -/// then boxes the result into a `Mixed` cell: `String` on success or `Bool(false)` -/// on failure (matching PHP semantics). The returned type is always `PhpType::Mixed`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("realpath()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_realpath"); // call the target-aware runtime helper that canonicalizes the path through libc realpath() - box_realpath_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Box the realpath runtime result into a Mixed cell. -/// -/// The runtime helper returns either `(ptr, len)` in registers or `(0, 0)` on failure. -/// On success, allocates a heap mixed cell, stamps it as kind 5, tags it as a string (tag 1), -/// and stores the path pointer/length directly without copying the owned realpath buffer. -/// On failure, calls `__rt_mixed_from_value` to box `false`. Caller is responsible for -/// preserving any caller-saved registers required by the ABI before this call. -fn box_realpath_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("realpath_false"); - let done_label = ctx.next_label("realpath_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null runtime string pointer means realpath() failed - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the canonical path while we allocate the mixed box - emitter.instruction("mov x0, #24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the mixed result cell for a successful string payload - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocated payload as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag in the mixed result - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the owned canonical path pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words without copying the owned realpath buffer - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path after a successful resolve - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for realpath() failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null runtime string pointer means realpath() failed - emitter.instruction(&format!("jz {}", false_label)); // box false when the runtime helper reports failure - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the canonical path while we allocate the mixed box - emitter.instruction("mov rax, 24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the mixed result cell for a successful string payload - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5)); // materialize the mixed-cell heap kind word with the x86_64 heap marker - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocated payload as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag in the mixed result - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the owned canonical path pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer without copying the owned realpath buffer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length without copying the owned realpath buffer - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path after a successful resolve - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for realpath() failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/rename.rs b/src/codegen/builtins/io/rename.rs deleted file mode 100644 index ffbfa90474..0000000000 --- a/src/codegen/builtins/io/rename.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Purpose: -//! Emits PHP `rename` filesystem mutation builtin calls. -//! Routes a `scheme://` source path matching a registered userspace wrapper to -//! the wrapper's `rename()` method; all other paths use the libc `__rt_rename`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. -//! - The wrapper split mirrors `readfile()`: a `__rt_path_is_wrapper` probe on -//! the SOURCE path picks the wrapper branch (`__rt_user_wrapper_rename`, vtable -//! slot 16) over the libc filesystem branch. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `rename($from, $to)` filesystem function. -/// -/// Evaluates the source path first, spills it, then evaluates the destination -/// path and spills it. A `__rt_path_is_wrapper` probe on the source path selects -/// the wrapper branch (`__rt_user_wrapper_rename`) or the libc branch -/// (`__rt_rename`). -/// -/// # Arguments -/// - `_name`: Unused; builtin dispatch is handled at the call site. -/// - `args`: Two expressions — the source path and destination path. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context (types, locals, class metadata). -/// - `data`: Data section for string literals and constants. -/// -/// # Returns -/// Always returns `PhpType::Bool` — PHP's rename returns false on failure, true on success. -/// -/// # Implementation notes -/// - String arguments use pointer/length pairs: `x1`/`x2` on AArch64, `rax`/`rdx` -/// on x86_64. Both paths are spilled to a 32-byte scratch frame so the source -/// data survives destination evaluation and the wrapper-scheme probe. -/// - The libc `__rt_rename` takes `from` in `x1`/`x2` and `to` in `x3`/`x4` -/// (AArch64) / `from` in `rax`/`rdx` and `to` in `rdi`/`rsi` (x86_64). -/// - `__rt_user_wrapper_rename` takes `from` then `to` in the SysV-style argument -/// registers (`x0`/`x1`, `x2`/`x3`; `rdi`/`rsi`, `rdx`/`rcx`). -/// - Effectful: observable OS filesystem mutation with PHP-visible ordering. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rename()"); - emit_expr(&args[0], emitter, ctx, data); - let wrapper = ctx.next_label("rename_wrapper"); - let after = ctx.next_label("rename_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("sub sp, sp, #32"); // scratch: [sp,#0] from ptr, [sp,#8] from len, [sp,#16] to ptr, [sp,#24] to len - emitter.instruction("str x1, [sp, #0]"); // save the source path pointer - emitter.instruction("str x2, [sp, #8]"); // save the source path length - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("str x1, [sp, #16]"); // save the destination path pointer - emitter.instruction("str x2, [sp, #24]"); // save the destination path length - emitter.instruction("ldr x0, [sp, #0]"); // path_is_wrapper arg0 = source path ptr - emitter.instruction("ldr x1, [sp, #8]"); // path_is_wrapper arg1 = source path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // x0 = 1 when the source scheme matches a registered wrapper - emitter.instruction(&format!("cbnz x0, {}", wrapper)); // registered wrapper scheme → wrapper rename - emitter.instruction("ldr x1, [sp, #0]"); // libc from ptr → x1 - emitter.instruction("ldr x2, [sp, #8]"); // libc from len → x2 - emitter.instruction("ldr x3, [sp, #16]"); // libc to ptr → x3 - emitter.instruction("ldr x4, [sp, #24]"); // libc to len → x4 - abi::emit_call_label(emitter, "__rt_rename"); // normal path: libc rename(from, to) - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("ldr x0, [sp, #0]"); // wrapper from ptr → x0 - emitter.instruction("ldr x1, [sp, #8]"); // wrapper from len → x1 - emitter.instruction("ldr x2, [sp, #16]"); // wrapper to ptr → x2 - emitter.instruction("ldr x3, [sp, #24]"); // wrapper to len → x3 - abi::emit_call_label(emitter, "__rt_user_wrapper_rename"); // dispatch into the wrapper's rename method - emitter.label(&after); - emitter.instruction("add sp, sp, #32"); // release the scratch frame - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 32"); // scratch: [rsp+0] from ptr, [rsp+8] from len, [rsp+16] to ptr, [rsp+24] to len - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the source path pointer - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the source path length - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // save the destination path pointer - emitter.instruction("mov QWORD PTR [rsp + 24], rdx"); // save the destination path length - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // path_is_wrapper arg0 = source path ptr - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // path_is_wrapper arg1 = source path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // rax = 1 when the source scheme matches a registered wrapper - emitter.instruction("test rax, rax"); // matched a registered wrapper scheme? - emitter.instruction(&format!("jnz {}", wrapper)); // registered wrapper scheme → wrapper rename - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // libc from ptr → rax - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // libc from len → rdx - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // libc to ptr → rdi - emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // libc to len → rsi - abi::emit_call_label(emitter, "__rt_rename"); // normal path: libc rename(from, to) - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // wrapper from ptr → rdi - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // wrapper from len → rsi - emitter.instruction("mov rdx, QWORD PTR [rsp + 16]"); // wrapper to ptr → rdx - emitter.instruction("mov rcx, QWORD PTR [rsp + 24]"); // wrapper to len → rcx - abi::emit_call_label(emitter, "__rt_user_wrapper_rename"); // dispatch into the wrapper's rename method - emitter.label(&after); - emitter.instruction("add rsp, 32"); // release the scratch frame - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/rewind.rs b/src/codegen/builtins/io/rewind.rs deleted file mode 100644 index 3e3d48c9b1..0000000000 --- a/src/codegen/builtins/io/rewind.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Purpose: -//! Emits PHP `rewind` stream builtin calls over runtime file handles. -//! Uses shared stream unboxing before invoking file descriptor runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Stream resources must be validated and failure results must follow PHP false/null conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits code for the PHP `rewind()` builtin. -/// -/// Unboxes the stream resource from `args[0]` to extract its file descriptor, -/// then calls the platform lseek routine with offset=0 and whence=SEEK_SET to -/// reset the file pointer to the start of the stream. On success, clears the -/// EOF flag for the file descriptor. On failure, returns false without modifying -/// the stream state. -/// -/// # Arguments -/// - `_name`: Ignored; present for dispatcher uniformity. -/// - `args`: Must contain exactly one expression resolving to a stream resource. -/// - `emitter`: Target-aware instruction emitter. -/// - `ctx`: Codegen context providing labels, frame layout, and platform details. -/// - `data`: Data section for literals and global symbol addresses. -/// -/// # Returns -/// Always returns `Some(PhpType::Bool)` — `true` on success, `false` on failure. -/// -/// # Platform details -/// - **AArch64**: Uses syscall 199 (`lseek`), preserves the fd across the call via -/// stack push/pop, and clears `_eof_flags[x9]` on success. -/// - **x86_64**: Calls libc `lseek()`, preserves the fd across the call via stack -/// push/pop, and clears `_eof_flags[r10]` on success. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rewind()"); - emit_stream_fd_arg("rewind", &args[0], emitter, ctx, data); - let success_label = ctx.next_label("rewind_success"); - let done_label = ctx.next_label("rewind_done"); - let user_wrapper_label = ctx.next_label("rewind_user_wrapper"); - let after_dispatch = ctx.next_label("rewind_after_dispatch"); - match emitter.target.arch { - Arch::AArch64 => { - // -- user-wrapper synthetic fd path: rewind via stream_seek(0, SEEK_SET) -- - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 in w9 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", user_wrapper_label)); // dispatch into the wrapper's stream_seek - abi::emit_push_reg(emitter, "x0"); // preserve fd so successful rewind() can clear its EOF flag - emitter.instruction("mov x1, #0"); // offset = 0 for the AArch64 rewind() lseek syscall - emitter.instruction("mov x2, #0"); // whence = SEEK_SET for the AArch64 rewind() lseek syscall - emitter.syscall(199); // reset the file position through the platform lseek syscall path - if emitter.platform.needs_cmp_before_error_branch() { - emitter.instruction("cmp x0, #0"); // Linux: negative lseek result means rewind() failed - } - emitter.instruction(&emitter.platform.branch_on_syscall_success(&success_label)); // continue only when lseek succeeded - abi::emit_pop_reg(emitter, "x9"); // discard preserved fd on the rewind() failure path - emitter.instruction("mov x0, #0"); // rewind() returns false when lseek fails - emitter.instruction(&format!("b {}", done_label)); // skip EOF reset after a failed seek - emitter.label(&success_label); - abi::emit_pop_reg(emitter, "x9"); // restore fd for EOF-flag reset after a successful seek - abi::emit_symbol_address(emitter, "x10", "_eof_flags"); - emitter.instruction("strb wzr, [x10, x9]"); // clear EOF because rewind() moved the stream back to the start - emitter.instruction("mov x0, #1"); // rewind() returns true after a successful seek - emitter.label(&done_label); - emitter.instruction(&format!("b {}", after_dispatch)); // skip the wrapper path on the normal-fd outcome - emitter.label(&user_wrapper_label); - emitter.instruction("mov x1, #0"); // offset = 0 (seek to the start of the stream) - emitter.instruction("mov x2, #0"); // whence = SEEK_SET - abi::emit_call_label(emitter, "__rt_user_wrapper_fseek"); // dispatch into stream_seek (x0 = 0 ok / -1 fail) - emitter.instruction("cmp x0, #0"); // did the wrapper's stream_seek report success? - emitter.instruction("cset x0, eq"); // rewind() returns true on success, false otherwise - emitter.label(&after_dispatch); - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the file descriptor into the first SysV lseek() argument register - // -- user-wrapper synthetic fd path: rewind via stream_seek(0, SEEK_SET) -- - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rdi, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", user_wrapper_label)); // dispatch into the wrapper's stream_seek - abi::emit_push_reg(emitter, "rdi"); // preserve fd so successful rewind() can clear its EOF flag - emitter.instruction("xor esi, esi"); // offset = 0 for the linux-x86_64 rewind() lseek() call - emitter.instruction("xor edx, edx"); // whence = SEEK_SET for the linux-x86_64 rewind() lseek() call - emitter.instruction("call lseek"); // reset the file position through libc lseek() on linux-x86_64 - emitter.instruction("cmp rax, 0"); // did libc lseek() succeed with a non-negative resulting offset? - emitter.instruction(&format!("jge {}", success_label)); // continue only when rewind() succeeded - abi::emit_pop_reg(emitter, "r10"); // discard preserved fd on the rewind() failure path - emitter.instruction("xor eax, eax"); // rewind() returns false when lseek fails - emitter.instruction(&format!("jmp {}", done_label)); // skip EOF reset after a failed seek - emitter.label(&success_label); - abi::emit_pop_reg(emitter, "r10"); // restore fd for EOF-flag reset after a successful seek - abi::emit_symbol_address(emitter, "r11", "_eof_flags"); // materialize the eof-flag table for rewind() - emitter.instruction("mov BYTE PTR [r11 + r10], 0"); // clear EOF because rewind() moved the stream back to the start - emitter.instruction("mov rax, 1"); // rewind() returns true after a successful seek - emitter.label(&done_label); - emitter.instruction(&format!("jmp {}", after_dispatch)); // skip the wrapper path on the normal-fd outcome - emitter.label(&user_wrapper_label); - emitter.instruction("xor esi, esi"); // offset = 0 (seek to the start of the stream) - emitter.instruction("xor edx, edx"); // whence = SEEK_SET - abi::emit_call_label(emitter, "__rt_user_wrapper_fseek"); // dispatch into stream_seek (rax = 0 ok / -1 fail) - emitter.instruction("cmp rax, 0"); // did the wrapper's stream_seek report success? - emitter.instruction("sete al"); // al = 1 when stream_seek succeeded - emitter.instruction("movzx eax, al"); // rewind() returns true on success, false otherwise - emitter.label(&after_dispatch); - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/rewinddir.rs b/src/codegen/builtins/io/rewinddir.rs deleted file mode 100644 index ae5ca4f1f6..0000000000 --- a/src/codegen/builtins/io/rewinddir.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Purpose: -//! Emits PHP `rewinddir` calls. -//! Rewinds a directory handle opened by `opendir()` back to its first entry. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The descriptor is unboxed from the stream resource and handed to the -//! `__rt_rewinddir` runtime helper, which calls libc `rewinddir`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `rewinddir()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rewinddir()"); - emit_stream_fd_arg("rewinddir", &args[0], emitter, ctx, data); - // -- dispatch: synthetic wrapper fd -> dir_rewinddir, else libc rewinddir -- - let wrapper = ctx.next_label("rewinddir_wrapper"); - let after = ctx.next_label("rewinddir_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper)); // dispatch into dir_rewinddir - abi::emit_call_label(emitter, "__rt_rewinddir"); // libc rewinddir - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - abi::emit_call_label(emitter, "__rt_user_wrapper_dir_rewinddir"); // wrapper dir_rewinddir - emitter.label(&after); - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper)); // dispatch into dir_rewinddir - emitter.instruction("mov rdi, rax"); // descriptor into the runtime-helper argument register - abi::emit_call_label(emitter, "__rt_rewinddir"); // libc rewinddir - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rdi, rax"); // descriptor into the runtime-helper argument register - abi::emit_call_label(emitter, "__rt_user_wrapper_dir_rewinddir"); // wrapper dir_rewinddir - emitter.label(&after); - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/io/rmdir.rs b/src/codegen/builtins/io/rmdir.rs deleted file mode 100644 index 42b2f3737a..0000000000 --- a/src/codegen/builtins/io/rmdir.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Purpose: -//! Emits PHP `rmdir` filesystem mutation builtin calls. -//! Routes `scheme://` paths matching a registered userspace wrapper to the -//! wrapper's `rmdir()` method; all other paths use the libc `__rt_rmdir`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. -//! - The wrapper split mirrors `readfile()`: a `__rt_path_is_wrapper` probe picks -//! the wrapper branch (`__rt_user_wrapper_path_op` with the `rmdir` vtable slot -//! 18) over the libc filesystem branch. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::path_op_wrapper::emit_single_path_wrapper_dispatch; - -/// `rmdir` vtable slot index in the per-class user-wrapper vtable. -const RMDIR_SLOT: usize = 18; - -/// Emits the `rmdir` PHP builtin call. -/// -/// Arguments (evaluated left-to-right): -/// - `args[0]`: path string to the directory to remove -/// - `args[1]`: context resource (ignored, reserved for stream context) -/// -/// A registered `scheme://` path dispatches to the wrapper's `rmdir()` (vtable -/// slot 18) via `__rt_user_wrapper_path_op`; any other path calls the libc -/// `__rt_rmdir` (path in x1/x2 on AArch64, rax/rdx on x86_64). Returns bool; -/// false on failure (not empty, permissions, wrapper miss, etc.). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rmdir()"); - emit_expr(&args[0], emitter, ctx, data); - emit_single_path_wrapper_dispatch(emitter, ctx, "__rt_rmdir", RMDIR_SLOT); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/scandir.rs b/src/codegen/builtins/io/scandir.rs deleted file mode 100644 index bd288c3bb3..0000000000 --- a/src/codegen/builtins/io/scandir.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits PHP `scandir` path-oriented builtin calls. -//! Marshals path strings into runtime helpers that normalize, split, or enumerate filesystem paths. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returned strings and arrays must use runtime allocation/layout compatible with PHP false-on-failure behavior. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `scandir` builtin call. -/// -/// First argument (path) is evaluated and emitted as an expression. Then calls the -/// `__rt_scandir` runtime helper which enumerates directory entries and returns them -/// as a string array. On failure (e.g., invalid path, not a directory), runtime returns -/// `false` rather than an array — callers must handle false-on-failure semantics. -/// -/// # Arguments -/// * `_name` - Unused; present for dispatcher uniformity with other builtin emitters. -/// * `args` - Must contain at least a path expression as the first element. -/// * `emitter` - Target-aware assembly emitter. -/// * `ctx` - Codegen context carrying variable layout and metadata. -/// * `data` - Data section for relocations and static storage. -/// -/// # Returns -/// `Some(PhpType::Array(Box::new(PhpType::Str)))` — callers should treat `false` -/// from runtime as the actual failure indicator. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("scandir()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_scandir"); // call the target-aware runtime helper that lists directory entries into a string array - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/io/stat.rs b/src/codegen/builtins/io/stat.rs deleted file mode 100644 index 6eb8b23cf5..0000000000 --- a/src/codegen/builtins/io/stat.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Purpose: -//! Emits PHP `stat` filesystem metadata builtin calls. -//! Delegates platform stat work to runtime helpers and boxes PHP false-or-value results. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Filesystem state is observable, so emitters must preserve call order and failure sentinels. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use super::stat_result::box_stat_array_or_false_result; - -/// Emits the PHP `stat` builtin call. -/// -/// Consumes `args[0]` as the filesystem path expression, evaluates it, calls the -/// target-aware runtime helper `__rt_stat_array` to build the PHP-compatible stat -/// array, boxes the result into `PhpType::Mixed`, and returns that type. -/// -/// Arguments: -/// - `name`: unused ( builtin dispatch is by name in the catalog) -/// - `args`: must contain exactly one path argument; the first element is consumed -/// -/// Outputs: -/// - Emits path evaluation code followed by a `bl __rt_stat_array` call -/// - Boxes the returned stat array (or false sentinel) into `PhpType::Mixed` -/// - Returns `Some(PhpType::Mixed)` -/// -/// Side effects: -/// - Filesystem is accessed by `__rt_stat_array`; call order is observable -/// - `ctx` may be mutated by `emit_expr` and `box_stat_array_or_false_result` -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stat()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_stat_array"); // call the target-aware runtime helper that builds the PHP-compatible stat array - box_stat_array_or_false_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/stat_result.rs b/src/codegen/builtins/io/stat_result.rs deleted file mode 100644 index f03904fba5..0000000000 --- a/src/codegen/builtins/io/stat_result.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! Purpose: -//! Boxes filesystem stat runtime results into PHP arrays, strings, ints, or false. -//! Centralizes result-shape handling shared by stat-family builtin emitters. -//! -//! Called from: -//! - `crate::codegen::builtins::io::*::emit() for stat-family builtins`. -//! -//! Key details: -//! - False sentinels and Mixed array payloads must match PHP failure semantics and runtime GC layout. - -use crate::codegen::context::Context; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Boxes a stat-family integer result into a Mixed cell, or returns PHP `false` on failure. -/// -/// ## ARM64 register contract -/// - `x0`: stat integer payload on entry; holds box result on exit -/// - `x1`: success flag (non-zero = success, zero = failure) -/// - `x2`: unused for integers (high word = 0) -/// - Calls `__rt_mixed_from_value` to box int (tag 0) or bool false (tag 3) -/// -/// ## x86_64 register contract -/// - `rax`: stat integer payload on entry; holds box result on exit -/// - `rdx`: success flag (non-zero = success, zero = failure) -/// - `rdi`: payload for `__rt_mixed_from_value` -/// - `esi`: 0 (high word unused for integers) -/// - `eax`: runtime tag (0 = int, 3 = bool false) -/// -/// ## Ownership -/// Neither path retains the input registers as owners — the callee helper takes ownership. -pub(super) fn box_stat_int_or_false_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("stat_int_false"); - let done_label = ctx.next_label("stat_int_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // runtime success flag unset: box PHP false - emitter.instruction("mov x2, xzr"); // integer mixed payloads do not use a high word - emitter.instruction("mov x1, x0"); // move the stat integer payload into the mixed helper low word - emitter.instruction("mov x0, #0"); // runtime tag 0 = int - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the successful integer result - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for stat failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rdx, rdx"); // runtime success flag unset: box PHP false - emitter.instruction(&format!("jz {}", false_label)); // jump to false boxing when stat failed - emitter.instruction("mov rdi, rax"); // move the stat integer payload into the mixed helper low word - emitter.instruction("xor esi, esi"); // integer mixed payloads do not use a high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = int - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the successful integer result - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for stat failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - } -} - -/// Boxes a stat-family string result into a Mixed cell, or returns PHP `false` on failure. -/// -/// The string is assumed to be null-terminated with length in a separate register. -/// -/// ## ARM64 register contract -/// - `x0`: null pointer means failure; otherwise holds box result -/// - `x1`: success flag (non-zero = success, zero = failure) -/// - Calls `__rt_mixed_from_value` to box string (tag 1) or bool false (tag 3) -/// -/// ## x86_64 register contract -/// - `rax`: null pointer means failure; otherwise holds the string pointer -/// - `rdx`: string length -/// - `rdi`: string pointer for `__rt_mixed_from_value` -/// - `rsi`: string length for `__rt_mixed_from_value` -/// - `eax`: runtime tag (1 = string, 3 = bool false) -/// -/// ## Ownership -/// The callee helper takes ownership of the string pointer. -pub(super) fn box_stat_string_or_false_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("stat_string_false"); - let done_label = ctx.next_label("stat_string_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // null string pointer means filetype() failed - emitter.instruction("mov x0, #1"); // runtime tag 1 = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist and box the successful filetype string - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for filetype() failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // null string pointer means filetype() failed - emitter.instruction(&format!("jz {}", false_label)); // jump to false boxing when lstat failed - emitter.instruction("mov rdi, rax"); // move the filetype string pointer into the mixed helper low word - emitter.instruction("mov rsi, rdx"); // move the filetype string length into the mixed helper high word - emitter.instruction("mov eax, 1"); // runtime tag 1 = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // persist and box the successful filetype string - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for filetype() failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - } -} - -/// Boxes a stat-family associative-array result into a Mixed cell, or returns PHP `false` on failure. -/// -/// Allocates a heap mixed cell with tag 5 (associative array) and stores the hash pointer as the payload. -/// The false path uses `__rt_mixed_from_value` (tag 3) to box a bool false. -/// -/// ## ARM64 register contract -/// - `x0`: null pointer means failure; otherwise holds the freshly built hash pointer -/// - `x9`: scratch used to stamp heap kind and runtime tag -/// - `x10`: scratch for reloading the hash pointer after allocation -/// - Calls `__rt_heap_alloc` to allocate 24-byte mixed cell, then `__rt_mixed_from_value` for false -/// -/// ## x86_64 register contract -/// - `rax`: null pointer means failure; otherwise holds the freshly built hash pointer -/// - `r10`: scratch for heap kind stamping and reloading hash pointer -/// - Uses `X86_64_HEAP_MAGIC_HI32` marker in the heap kind stamp -/// - Calls `__rt_heap_alloc` to allocate 24-byte mixed cell, then `__rt_mixed_from_value` for false -/// -/// ## Ownership -/// The allocated mixed cell owns the hash payload. The false path transfers no ownership. -pub(super) fn box_stat_array_or_false_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("stat_array_false"); - let done_label = ctx.next_label("stat_array_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x0, {}", false_label)); // null hash pointer means stat()/lstat()/fstat() failed - abi::emit_push_reg(emitter, "x0"); // preserve the freshly built hash while allocating the mixed cell - emitter.instruction("mov x0, #24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the mixed result cell for a successful stat array - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocated payload as a mixed cell - emitter.instruction("mov x9, #5"); // runtime tag 5 = associative array - emitter.instruction("str x9, [x0]"); // store the associative-array tag in the mixed result - abi::emit_pop_reg(emitter, "x10"); // reload the newly built stat hash pointer - emitter.instruction("str x10, [x0, #8]"); // store the hash pointer without retaining the new owner twice - emitter.instruction("str xzr, [x0, #16]"); // associative-array payloads do not use a high word - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for stat-array failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // null hash pointer means stat()/lstat()/fstat() failed - emitter.instruction(&format!("jz {}", false_label)); // jump to false boxing when the runtime stat call failed - abi::emit_push_reg(emitter, "rax"); // preserve the freshly built hash while allocating the mixed cell - emitter.instruction("mov rax, 24"); // mixed cells store tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the mixed result cell for a successful stat array - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5)); // materialize the mixed-cell heap kind word with the x86_64 heap marker - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocated payload as a mixed cell - emitter.instruction("mov QWORD PTR [rax], 5"); // runtime tag 5 = associative array - abi::emit_pop_reg(emitter, "r10"); // reload the newly built stat hash pointer - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the hash pointer without retaining the new owner twice - emitter.instruction("mov QWORD PTR [rax + 16], 0"); // associative-array payloads do not use a high word - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for stat-array failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible failure semantics - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/stream_arg.rs b/src/codegen/builtins/io/stream_arg.rs deleted file mode 100644 index cb15534e94..0000000000 --- a/src/codegen/builtins/io/stream_arg.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! Purpose: -//! Unboxes PHP stream resources for file-handle based builtin emitters. -//! Emits consistent fatal/type-error paths when a stream argument is not valid. -//! -//! Called from: -//! - `crate::codegen::builtins::io::*::emit() for stream builtins`. -//! -//! Key details: -//! - Resource handles are runtime-owned file descriptors; validation must happen before syscall/helper use. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits argument expression and validates it as a stream resource. -/// -/// Emits `arg` via `emit_expr`. If the resulting type is `Mixed` or `Union`, -/// emits `emit_unbox_stream_or_fatal` to unbox the resource and produce a fatal -/// TypeError at runtime if the value is not a valid stream. Returns the PHP type -/// of the argument expression. -/// -/// # Arguments -/// * `function_name` - PHP builtin name used in error messages -/// * `arg` - The argument expression to emit and validate -/// * `emitter` - Target-specific assembly emitter -/// * `ctx` - Codegen context (label generation) -/// * `data` - Data section for string/constant emission -/// -/// # Returns -/// The `PhpType` of the emitted argument expression. -pub(crate) fn emit_stream_fd_arg( - function_name: &str, - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let ty = emit_expr(arg, emitter, ctx, data); - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - emit_unbox_stream_or_fatal(function_name, emitter, ctx, data); - } - ty -} - -/// Unboxes a Mixed/Union stream value or emits a fatal TypeError. -/// -/// Calls `__rt_mixed_unbox` to extract the runtime value, then checks the boxed -/// payload tag (tag 9 = stream resource). On success, copies the native file -/// descriptor from `x1`/`rdi` to the integer result register. On failure, -/// branches to `emit_stream_type_error` for the appropriate PHP TypeError. -/// -/// # Arguments -/// * `function_name` - PHP builtin name used in error messages -/// * `emitter` - Target-specific assembly emitter -/// * `ctx` - Codegen context (label generation) -/// * `data` - Data section for string/constant emission -fn emit_unbox_stream_or_fatal( - function_name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let ok_label = ctx.next_label("stream_resource_ok"); - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // unwrap a resource|false handle returned by fopen() - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #9"); // is the boxed handle a stream resource payload? - emitter.instruction(&format!("b.eq {}", ok_label)); // continue only for resource values - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 9"); // is the boxed handle a stream resource payload? - emitter.instruction(&format!("je {}", ok_label)); // continue only for resource values - } - } - emit_stream_type_error(function_name, emitter, ctx, data); - emitter.label(&ok_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // expose the unboxed native stream descriptor as the ordinary integer result - } - Arch::X86_64 => { - emitter.instruction("mov rax, rdi"); // expose the unboxed native stream descriptor as the ordinary integer result - } - } -} - -/// Emits a fatal TypeError for a stream argument with an unexpected PHP type. -/// -/// Dispatches to type-specific error case labels based on the unboxed runtime -/// tag from `__rt_mixed_unbox`. Each case calls `emit_stream_type_error_case` -/// to emit the error message and terminate. -/// -/// # Arguments -/// * `function_name` - PHP builtin name used in error messages -/// * `emitter` - Target-specific assembly emitter -/// * `ctx` - Codegen context (label generation) -/// * `data` - Data section for string/constant emission -fn emit_stream_type_error( - function_name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let int_label = ctx.next_label("stream_type_error_int"); - let string_label = ctx.next_label("stream_type_error_string"); - let float_label = ctx.next_label("stream_type_error_float"); - let bool_label = ctx.next_label("stream_type_error_bool"); - let false_label = ctx.next_label("stream_type_error_false"); - let true_label = ctx.next_label("stream_type_error_true"); - let array_label = ctx.next_label("stream_type_error_array"); - let object_label = ctx.next_label("stream_type_error_object"); - let null_label = ctx.next_label("stream_type_error_null"); - let unknown_label = ctx.next_label("stream_type_error_unknown"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the bad stream value unwrap to an integer? - emitter.instruction(&format!("b.eq {}", int_label)); // report PHP's int-given stream TypeError - emitter.instruction("cmp x0, #1"); // did the bad stream value unwrap to a string? - emitter.instruction(&format!("b.eq {}", string_label)); // report PHP's string-given stream TypeError - emitter.instruction("cmp x0, #2"); // did the bad stream value unwrap to a float? - emitter.instruction(&format!("b.eq {}", float_label)); // report PHP's float-given stream TypeError - emitter.instruction("cmp x0, #3"); // did the bad stream value unwrap to a boolean? - emitter.instruction(&format!("b.eq {}", bool_label)); // split boolean payloads into true/false diagnostics - emitter.instruction("cmp x0, #4"); // did the bad stream value unwrap to an indexed array? - emitter.instruction(&format!("b.eq {}", array_label)); // report PHP's array-given stream TypeError - emitter.instruction("cmp x0, #5"); // did the bad stream value unwrap to an associative array? - emitter.instruction(&format!("b.eq {}", array_label)); // associative arrays share PHP's array-given wording - emitter.instruction("cmp x0, #6"); // did the bad stream value unwrap to an object? - emitter.instruction(&format!("b.eq {}", object_label)); // report PHP's object-given stream TypeError - emitter.instruction("cmp x0, #8"); // did the bad stream value unwrap to null? - emitter.instruction(&format!("b.eq {}", null_label)); // report PHP's null-given stream TypeError - emitter.instruction(&format!("b {}", unknown_label)); // fall back for unsupported boxed payload tags - emitter.label(&bool_label); - emitter.instruction("cmp x1, #0"); // is the unboxed boolean payload false? - emitter.instruction(&format!("b.eq {}", false_label)); // report PHP's false-given stream TypeError - emitter.instruction(&format!("b {}", true_label)); // report PHP's true-given stream TypeError - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // did the bad stream value unwrap to an integer? - emitter.instruction(&format!("je {}", int_label)); // report PHP's int-given stream TypeError - emitter.instruction("cmp rax, 1"); // did the bad stream value unwrap to a string? - emitter.instruction(&format!("je {}", string_label)); // report PHP's string-given stream TypeError - emitter.instruction("cmp rax, 2"); // did the bad stream value unwrap to a float? - emitter.instruction(&format!("je {}", float_label)); // report PHP's float-given stream TypeError - emitter.instruction("cmp rax, 3"); // did the bad stream value unwrap to a boolean? - emitter.instruction(&format!("je {}", bool_label)); // split boolean payloads into true/false diagnostics - emitter.instruction("cmp rax, 4"); // did the bad stream value unwrap to an indexed array? - emitter.instruction(&format!("je {}", array_label)); // report PHP's array-given stream TypeError - emitter.instruction("cmp rax, 5"); // did the bad stream value unwrap to an associative array? - emitter.instruction(&format!("je {}", array_label)); // associative arrays share PHP's array-given wording - emitter.instruction("cmp rax, 6"); // did the bad stream value unwrap to an object? - emitter.instruction(&format!("je {}", object_label)); // report PHP's object-given stream TypeError - emitter.instruction("cmp rax, 8"); // did the bad stream value unwrap to null? - emitter.instruction(&format!("je {}", null_label)); // report PHP's null-given stream TypeError - emitter.instruction(&format!("jmp {}", unknown_label)); // fall back for unsupported boxed payload tags - emitter.label(&bool_label); - emitter.instruction("test rdi, rdi"); // is the unboxed boolean payload false? - emitter.instruction(&format!("je {}", false_label)); // report PHP's false-given stream TypeError - emitter.instruction(&format!("jmp {}", true_label)); // report PHP's true-given stream TypeError - } - } - - emit_stream_type_error_case(function_name, "int", &int_label, emitter, data); - emit_stream_type_error_case(function_name, "string", &string_label, emitter, data); - emit_stream_type_error_case(function_name, "float", &float_label, emitter, data); - emit_stream_type_error_case(function_name, "false", &false_label, emitter, data); - emit_stream_type_error_case(function_name, "true", &true_label, emitter, data); - emit_stream_type_error_case(function_name, "array", &array_label, emitter, data); - emit_stream_type_error_case(function_name, "object", &object_label, emitter, data); - emit_stream_type_error_case(function_name, "null", &null_label, emitter, data); - emit_stream_type_error_case(function_name, "unknown", &unknown_label, emitter, data); -} - -/// Emits a single stream TypeError case for a given PHP type. -/// -/// Formats the PHP TypeError message using `function_name` and `given_type`, -/// adds it to the data section, and emits a jump to -/// `emit_write_type_error_and_exit`. -/// -/// # Arguments -/// * `function_name` - PHP builtin name used in the error message -/// * `given_type` - The PHP type that was incorrectly provided -/// * `case_label` - Label to branch here for this type case -/// * `emitter` - Target-specific assembly emitter -/// * `data` - Data section for string/constant emission -fn emit_stream_type_error_case( - function_name: &str, - given_type: &str, - case_label: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let message = format!( - "Fatal error: Uncaught TypeError: {}(): Argument #1 ($stream) must be of type resource, {} given\n", - function_name, given_type - ); - let (label, len) = data.add_string(message.as_bytes()); - emitter.label(case_label); - emit_write_type_error_and_exit(&label, len, emitter); -} - -/// Emits the stream TypeError diagnostic to stderr and exits with status 1. -/// -/// Writes the formatted error message to stderr using the Linux `write` syscall, -/// then calls `exit` with status 1. Target-specific: ARM64 uses `syscall` -/// instruction; x86_64 uses `syscall` instruction. -/// -/// # Arguments -/// * `label` - Data section label for the error message string -/// * `len` - Length of the error message string in bytes -/// * `emitter` - Target-specific assembly emitter -fn emit_write_type_error_and_exit(label: &str, len: usize, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // fd = stderr for the stream TypeError diagnostic - abi::emit_symbol_address(emitter, "x1", label); // load the page that contains the stream TypeError diagnostic - emitter.instruction(&format!("mov x2, #{}", len)); // pass the stream TypeError diagnostic length to write() - emitter.syscall(4); - emitter.instruction("mov x0, #1"); // exit status 1 indicates abnormal termination - emitter.syscall(1); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", label); // point the Linux write buffer at the stream TypeError diagnostic - emitter.instruction(&format!("mov edx, {}", len)); // pass the stream TypeError diagnostic length to write() - emitter.instruction("mov edi, 2"); // fd = stderr for the stream TypeError diagnostic - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the stream TypeError diagnostic - emitter.instruction("mov edi, 1"); // exit status 1 indicates abnormal termination - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate after reporting the stream TypeError diagnostic - } - } -} diff --git a/src/codegen/builtins/io/stream_bucket.rs b/src/codegen/builtins/io/stream_bucket.rs deleted file mode 100644 index d868c1d006..0000000000 --- a/src/codegen/builtins/io/stream_bucket.rs +++ /dev/null @@ -1,403 +0,0 @@ -//! Purpose: -//! Emits PHP stream-bucket builtins: -//! `stream_bucket_make_writeable`, `stream_bucket_new`, -//! `stream_bucket_append`, `stream_bucket_prepend`. Buckets are -//! stdClass-backed objects with `data` (string) and `datalen` (int) -//! public properties. A brigade is a stdClass with an internal -//! `_buckets` property (indexed array of Mixed-boxed bucket objects); -//! a brigade with no `_buckets` property is treated as empty. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - v1 API-surface delivery: these builtins work as a stand-alone -//! primitive for code that needs PHP-shaped bucket plumbing. They -//! are NOT yet wired into the filter dispatch — the existing -//! `filter(string $data): string` contract continues to drive -//! stream_filter_append's chain. A future increment will detect a -//! class's filter() arity and route 4-arg `filter($in, $out, -//! &$consumed, $closing): int` methods through these brigades. -//! - The brigade's `_buckets` property is an indexed array of -//! boxed-Mixed bucket references. `make_writeable` pops the head -//! and rewrites the array (no in-place mutation in v1); `append` -//! reconstructs the array with the new tail entry. Performance is -//! O(n) per call — acceptable since real filter brigades stay -//! small (typically 1-3 buckets). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// `stream_bucket_new($stream, $data)`: build a stdClass with -/// `data` and `datalen` properties. The `$stream` arg is evaluated -/// for side effects but unused — bucket lifetime is tied to the -/// owning brigade, not the stream. -pub fn emit_new( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_bucket_new()"); - // Evaluate $stream for side effects; the result is dropped. - emit_expr(&args[0], emitter, ctx, data); - // Evaluate $data → string in x1/x2 (ARM64) or rax/rdx (x86_64). - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - // Save the string ptr/len across the stdclass_new + property-set calls. - abi::emit_push_reg_pair(emitter, "x1", "x2"); - abi::emit_call_label(emitter, "__rt_stdclass_new"); // x0 = bucket obj - abi::emit_push_reg(emitter, "x0"); // preserve bucket across the property set - // Set $bucket->data = boxed_mixed_string - emitter.instruction("ldr x1, [sp, #16]"); // string ptr (peek the saved pair) - emitter.instruction("ldr x2, [sp, #24]"); // string len - emitter.instruction("mov x0, #1"); // tag = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // x0 = mixed-cell ptr - emitter.instruction("mov x3, x0"); // value → 4th arg - abi::emit_pop_reg(emitter, "x0"); // bucket obj → 1st arg - abi::emit_push_reg(emitter, "x0"); // re-push for the next set - let (data_sym, data_len) = data.add_string(b"data"); - abi::emit_symbol_address(emitter, "x1", &data_sym); // name_ptr - emitter.instruction(&format!("mov x2, #{}", data_len)); // name_len = 4 - abi::emit_call_label(emitter, "__rt_stdclass_set"); - // Set $bucket->datalen = boxed_mixed_int(strlen). - emitter.instruction("ldr x1, [sp, #24]"); // string len → int payload - emitter.instruction("mov x2, #0"); // high word - emitter.instruction("mov x0, #0"); // tag = int - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction("mov x3, x0"); // value → 4th arg - abi::emit_pop_reg(emitter, "x0"); // bucket obj - let (datalen_sym, datalen_len) = data.add_string(b"datalen"); - abi::emit_symbol_address(emitter, "x1", &datalen_sym); - emitter.instruction(&format!("mov x2, #{}", datalen_len)); // name_len = 7 - abi::emit_push_reg(emitter, "x0"); // hold bucket across the set - abi::emit_call_label(emitter, "__rt_stdclass_set"); - abi::emit_pop_reg(emitter, "x0"); // bucket → return - abi::emit_release_temporary_stack(emitter, 16); // drop the original ptr/len pair - // Box as Mixed object so the caller can pass it through Mixed pipelines. - emitter.instruction("mov x1, x0"); // bucket ptr - emitter.instruction("mov x2, #0"); // prepare AArch64 call argument - emitter.instruction("mov x0, #6"); // tag = object - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - } - Arch::X86_64 => { - // Save the string ptr/len pair. - abi::emit_push_reg_pair(emitter, "rax", "rdx"); - abi::emit_call_label(emitter, "__rt_stdclass_new"); // rax = bucket - abi::emit_push_reg(emitter, "rax"); // preserve bucket - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // peek string ptr - emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // peek string len - emitter.instruction("mov rax, 1"); // tag = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction("mov rcx, rax"); // mixed_ptr → 4th arg - abi::emit_pop_reg(emitter, "rax"); // bucket - abi::emit_push_reg(emitter, "rax"); // re-push - emitter.instruction("mov rdi, rax"); // bucket → 1st arg - let (data_sym, data_len) = data.add_string(b"data"); - abi::emit_symbol_address(emitter, "rsi", &data_sym); - emitter.instruction(&format!("mov rdx, {}", data_len)); // prepare SysV call argument - abi::emit_call_label(emitter, "__rt_stdclass_set"); - emitter.instruction("mov rdi, QWORD PTR [rsp + 24]"); // string len → int payload - emitter.instruction("xor esi, esi"); // high word - emitter.instruction("mov rax, 0"); // tag = int - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction("mov rcx, rax"); // mixed → 4th arg - abi::emit_pop_reg(emitter, "rdi"); // bucket - abi::emit_push_reg(emitter, "rdi"); - let (datalen_sym, datalen_len) = data.add_string(b"datalen"); - abi::emit_symbol_address(emitter, "rsi", &datalen_sym); - emitter.instruction(&format!("mov rdx, {}", datalen_len)); // prepare SysV call argument - abi::emit_call_label(emitter, "__rt_stdclass_set"); - abi::emit_pop_reg(emitter, "rdi"); // bucket - abi::emit_release_temporary_stack(emitter, 16); // drop saved ptr/len pair - emitter.instruction("xor esi, esi"); // clear register value - emitter.instruction("mov rax, 6"); // tag = object - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - } - } - Some(PhpType::Mixed) -} - -/// `stream_bucket_make_writeable($brigade)`: pop the head bucket from -/// the brigade's internal `_buckets` indexed-array property. Returns -/// Mixed(null) when: -/// - the brigade arg is not a Mixed(object) (e.g. null was passed). -/// - the brigade has no `_buckets` property or it is not an indexed array. -/// - the `_buckets` array is empty. -/// -/// The popped bucket is returned as Mixed-boxed object (matching what -/// `stream_bucket_new` produces). The brigade's `_buckets` array is -/// mutated in place (`__rt_array_shift` decrements length and slides the -/// remaining entries left); the boxed-Mixed cell pointer stored as the -/// `_buckets` property stays the same so no stdclass_set write-back is -/// needed. -pub fn emit_make_writeable( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_bucket_make_writeable()"); - let arg_ty = emit_expr(&args[0], emitter, ctx, data); - let arg_is_mixed = matches!(arg_ty, PhpType::Mixed | PhpType::Union(_)); - let (buckets_sym, buckets_len) = data.add_string(b"_buckets"); - let return_null = ctx.next_label("sbmw_null"); - let done = ctx.next_label("sbmw_done"); - match emitter.target.arch { - Arch::AArch64 => { - // x0 = Mixed cell ptr (when Mixed) or raw obj ptr (when Object). - if arg_is_mixed { - emitter.instruction(&format!("cbz x0, {}", return_null)); // null Mixed → no brigade - emitter.instruction("ldr x9, [x0]"); // tag - emitter.instruction("cmp x9, #6"); // tag==6 (object)? - emitter.instruction(&format!("b.ne {}", return_null)); // branch when the checked value is nonzero or different - emitter.instruction("ldr x0, [x0, #8]"); // unbox: obj ptr - } - emitter.instruction(&format!("cbz x0, {}", return_null)); // branch when the checked value is zero or equal - // x0 = brigade obj; look up _buckets. - abi::emit_symbol_address(emitter, "x1", &buckets_sym); - emitter.instruction(&format!("mov x2, #{}", buckets_len)); // prepare AArch64 call argument - abi::emit_call_label(emitter, "__rt_stdclass_get"); // x0 = Mixed* - emitter.instruction(&format!("cbz x0, {}", return_null)); // branch when the checked value is zero or equal - emitter.instruction("ldr x9, [x0]"); // Mixed tag - emitter.instruction("cmp x9, #4"); // tag==4 (indexed array)? - emitter.instruction(&format!("b.ne {}", return_null)); // branch when the checked value is nonzero or different - emitter.instruction("ldr x9, [x0, #8]"); // array ptr from Mixed payload_lo - emitter.instruction(&format!("cbz x9, {}", return_null)); // branch when the checked value is zero or equal - emitter.instruction("ldr x10, [x9]"); // length - emitter.instruction(&format!("cbz x10, {}", return_null)); // branch when the checked value is zero or equal - emitter.instruction("mov x0, x9"); // array ptr → x0 - abi::emit_call_label(emitter, "__rt_array_shift"); // x0 = popped Mixed* (the bucket) - emitter.instruction(&format!("b {}", done)); // continue at target label - emitter.label(&return_null); - emitter.instruction("mov x0, #8"); // tag = null - emitter.instruction("mov x1, #0"); // prepare AArch64 call argument - emitter.instruction("mov x2, #0"); // prepare AArch64 call argument - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done); - } - Arch::X86_64 => { - if arg_is_mixed { - emitter.instruction("test rax, rax"); // null Mixed? - emitter.instruction(&format!("jz {}", return_null)); // branch when the checked value is zero or equal - emitter.instruction("mov r10, QWORD PTR [rax]"); // tag - emitter.instruction("cmp r10, 6"); // object? - emitter.instruction(&format!("jne {}", return_null)); // branch when the checked value is nonzero or different - emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // unbox: obj ptr - } - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", return_null)); // branch when the checked value is zero or equal - // SysV: stdclass_get(rdi=obj, rsi=name_ptr, rdx=name_len). - emitter.instruction("mov rdi, rax"); // prepare SysV call argument - abi::emit_symbol_address(emitter, "rsi", &buckets_sym); - emitter.instruction(&format!("mov rdx, {}", buckets_len)); // prepare SysV call argument - abi::emit_call_label(emitter, "__rt_stdclass_get"); // rax = Mixed* - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", return_null)); // branch when the checked value is zero or equal - emitter.instruction("mov r10, QWORD PTR [rax]"); // Mixed tag - emitter.instruction("cmp r10, 4"); // indexed array? - emitter.instruction(&format!("jne {}", return_null)); // branch when the checked value is nonzero or different - emitter.instruction("mov r10, QWORD PTR [rax + 8]"); // array ptr from Mixed payload_lo - emitter.instruction("test r10, r10"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", return_null)); // branch when the checked value is zero or equal - emitter.instruction("mov r11, QWORD PTR [r10]"); // length - emitter.instruction("test r11, r11"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", return_null)); // branch when the checked value is zero or equal - // SysV: array_shift(rdi=array). - emitter.instruction("mov rdi, r10"); // prepare SysV call argument - abi::emit_call_label(emitter, "__rt_array_shift"); // rax = popped Mixed* (the bucket) - emitter.instruction(&format!("jmp {}", done)); // continue at target label - emitter.label(&return_null); - emitter.instruction("mov rax, 8"); // tag = null - emitter.instruction("xor edi, edi"); // clear register value - emitter.instruction("xor esi, esi"); // clear register value - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done); - } - } - Some(PhpType::Mixed) -} - -/// `stream_bucket_append($brigade, $bucket)` / `_prepend(...)`: actually -/// push the bucket into the brigade's `_buckets` indexed-array property. -/// If `_buckets` is missing or not an indexed array, a fresh -/// indexed array of Mixed-boxed pointers is created and stored back via -/// `__rt_stdclass_set`. Otherwise the existing array is appended to via -/// `__rt_array_push_int`; if the push grew the array, the new pointer -/// is also written back through `__rt_stdclass_set`. -/// -/// Both append and prepend share the same emit body — prepend would -/// require an `__rt_array_unshift` helper which doesn't exist yet, so v1 -/// treats prepend as an append (PHP filter chains rarely use prepend; the -/// dispatcher walks the brigade head-to-tail in order, so the practical -/// difference is small). -pub fn emit_append_or_prepend( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_bucket_append/prepend()"); - // Evaluate $brigade — Mixed cell ptr (when Mixed-typed) or raw obj ptr. - let brigade_ty = emit_expr(&args[0], emitter, ctx, data); - let brigade_is_mixed = matches!(brigade_ty, PhpType::Mixed | PhpType::Union(_)); - let (buckets_sym, buckets_len) = data.add_string(b"_buckets"); - let done = ctx.next_label("sba_done"); - let skip_init = ctx.next_label("sba_existing"); - let push = ctx.next_label("sba_push"); - let writeback = ctx.next_label("sba_writeback"); - match emitter.target.arch { - Arch::AArch64 => { - if brigade_is_mixed { - emitter.instruction(&format!("cbz x0, {}", done)); // branch when the checked value is zero or equal - emitter.instruction("ldr x9, [x0]"); // load runtime value - emitter.instruction("cmp x9, #6"); // compare runtime values for the next branch - emitter.instruction(&format!("b.ne {}", done)); // branch when the checked value is nonzero or different - emitter.instruction("ldr x0, [x0, #8]"); // load runtime value - } - emitter.instruction(&format!("cbz x0, {}", done)); // branch when the checked value is zero or equal - // Save brigade obj on a temp stack slot for use after evaluating $bucket. - abi::emit_push_reg(emitter, "x0"); - // Evaluate $bucket — Mixed cell ptr in x0. - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, "x0"); // save bucket Mixed* - // Reload brigade and look up _buckets. - emitter.instruction("ldr x0, [sp, #16]"); // brigade obj (peek) - abi::emit_symbol_address(emitter, "x1", &buckets_sym); - emitter.instruction(&format!("mov x2, #{}", buckets_len)); // prepare AArch64 call argument - abi::emit_call_label(emitter, "__rt_stdclass_get"); // x0 = Mixed* - // Either it's a Mixed(indexed-array) we can push into, or we need a fresh one. - emitter.instruction(&format!("cbz x0, {}", push)); // null Mixed → make new - emitter.instruction("ldr x9, [x0]"); // load runtime value - emitter.instruction("cmp x9, #4"); // indexed array? - emitter.instruction(&format!("b.ne {}", push)); // wrong tag → make new - emitter.instruction("ldr x9, [x0, #8]"); // array ptr from Mixed - emitter.instruction(&format!("cbz x9, {}", push)); // null array → make new - emitter.instruction("mov x0, x9"); // array ptr ready for push_int - emitter.instruction(&format!("b {}", skip_init)); // continue at target label - - emitter.label(&push); - // Allocate a fresh empty indexed array (capacity 4, stride 8 = Mixed pointer slots). - emitter.instruction("mov x0, #4"); // prepare AArch64 call argument - emitter.instruction("mov x1, #8"); // prepare AArch64 call argument - abi::emit_call_label(emitter, "__rt_array_new"); // x0 = new array - // Stamp value_type tag = 7 (boxed Mixed) so dispatchers route correctly. - // Mask 0x80ff matches the existing AArch64 stamp helper convention: - // preserve the kind byte + COW bit, clear the rest before OR'ing the - // new value_type tag in. - emitter.instruction("ldr x10, [x0, #-8]"); // packed kind word - emitter.instruction("mov x12, #0x80ff"); // mask: low byte (kind) + COW bit - emitter.instruction("and x10, x10, x12"); // keep persistent metadata - emitter.instruction("mov x11, #7"); // value_type = boxed Mixed - emitter.instruction("lsl x11, x11, #8"); // place in byte lane - emitter.instruction("orr x10, x10, x11"); // combine runtime bit flags - emitter.instruction("str x10, [x0, #-8]"); // store runtime value - - emitter.label(&skip_init); - // Push the bucket Mixed* into the array. We also incref so the - // cell survives the caller's end-of-scope decref (common pattern - // in brigade-driven filters: $b = make_writeable(); append(out, $b); - // — when the method returns, $b's slot is decref'd, and without - // the extra owner the array would dangle). - abi::emit_push_reg(emitter, "x0"); // save array across incref - emitter.instruction("ldr x0, [sp, #16]"); // peek bucket Mixed* - abi::emit_call_label(emitter, "__rt_incref"); - abi::emit_pop_reg(emitter, "x0"); // restore array - emitter.instruction("ldr x1, [sp, #0]"); // bucket Mixed* - abi::emit_call_label(emitter, "__rt_array_push_int"); // x0 = updated array - // We always write the (re-boxed) Mixed back so the brigade sees the right pointer. - emitter.instruction("mov x3, x0"); // array ptr → boxing low payload arg - emitter.instruction("mov x0, #4"); // tag = indexed array - emitter.instruction("mov x1, x3"); // payload lo - emitter.instruction("mov x2, #0"); // payload hi - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // x0 = fresh Mixed cell wrapping the array - // stdclass_set(brigade, "_buckets", 8, mixed_array). - emitter.label(&writeback); - emitter.instruction("mov x3, x0"); // mixed* → 4th arg - emitter.instruction("ldr x0, [sp, #16]"); // brigade obj - abi::emit_symbol_address(emitter, "x1", &buckets_sym); - emitter.instruction(&format!("mov x2, #{}", buckets_len)); // prepare AArch64 call argument - abi::emit_call_label(emitter, "__rt_stdclass_set"); - // Pop the saved (bucket Mixed*, brigade obj) pair. - abi::emit_release_temporary_stack(emitter, 32); - emitter.label(&done); - } - Arch::X86_64 => { - if brigade_is_mixed { - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", done)); // branch when the checked value is zero or equal - emitter.instruction("mov r10, QWORD PTR [rax]"); // move runtime value between registers - emitter.instruction("cmp r10, 6"); // compare runtime values for the next branch - emitter.instruction(&format!("jne {}", done)); // branch when the checked value is nonzero or different - emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // prepare runtime result value - } - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", done)); // branch when the checked value is zero or equal - abi::emit_push_reg(emitter, "rax"); // save brigade obj - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, "rax"); // save bucket Mixed* - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // brigade obj - abi::emit_symbol_address(emitter, "rsi", &buckets_sym); - emitter.instruction(&format!("mov rdx, {}", buckets_len)); // prepare SysV call argument - abi::emit_call_label(emitter, "__rt_stdclass_get"); // rax = Mixed* - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", push)); // branch when the checked value is zero or equal - emitter.instruction("mov r10, QWORD PTR [rax]"); // move runtime value between registers - emitter.instruction("cmp r10, 4"); // compare runtime values for the next branch - emitter.instruction(&format!("jne {}", push)); // branch when the checked value is nonzero or different - emitter.instruction("mov r10, QWORD PTR [rax + 8]"); // move runtime value between registers - emitter.instruction("test r10, r10"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", push)); // branch when the checked value is zero or equal - emitter.instruction("mov rax, r10"); // array ptr - emitter.instruction(&format!("jmp {}", skip_init)); // continue at target label - - emitter.label(&push); - emitter.instruction("mov rdi, 4"); // prepare SysV call argument - emitter.instruction("mov rsi, 8"); // prepare SysV call argument - abi::emit_call_label(emitter, "__rt_array_new"); // rax = new array - // Stamp value_type=7. Mask matches the existing x86_64 stamp helper: - // preserve the high-dword magic marker plus low-byte kind + COW. - emitter.instruction("mov r10, QWORD PTR [rax - 8]"); // move runtime value between registers - emitter.instruction("mov r11, 0xffffffff000080ff"); // move runtime value between registers - emitter.instruction("and r10, r11"); // mask runtime value - emitter.instruction("mov r11, 7"); // move runtime value between registers - emitter.instruction("shl r11, 8"); // shift runtime value - emitter.instruction("or r10, r11"); // combine runtime bit flags - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // store runtime value - - emitter.label(&skip_init); - // Incref the bucket Mixed* so it survives caller's end-of-scope decref. - abi::emit_push_reg(emitter, "rax"); // save array across incref - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // peek bucket Mixed* - abi::emit_call_label(emitter, "__rt_incref"); - abi::emit_pop_reg(emitter, "rax"); // restore array - emitter.instruction("mov rdi, rax"); // array → SysV first arg of push_int - emitter.instruction("mov rsi, QWORD PTR [rsp]"); // bucket Mixed* → SysV second arg - abi::emit_call_label(emitter, "__rt_array_push_int"); // rax = updated array - - emitter.instruction("mov rdi, rax"); // prepare SysV call argument - emitter.instruction("xor esi, esi"); // clear register value - emitter.instruction("mov rax, 4"); // tag = indexed array - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // rax = fresh Mixed* wrapping array - emitter.label(&writeback); - emitter.instruction("mov rcx, rax"); // value mixed → 4th SysV arg - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // brigade obj → 1st - abi::emit_symbol_address(emitter, "rsi", &buckets_sym); - emitter.instruction(&format!("mov rdx, {}", buckets_len)); // prepare SysV call argument - abi::emit_call_label(emitter, "__rt_stdclass_set"); - abi::emit_release_temporary_stack(emitter, 32); - emitter.label(&done); - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/io/stream_context_create.rs b/src/codegen/builtins/io/stream_context_create.rs deleted file mode 100644 index b8fecde6af..0000000000 --- a/src/codegen/builtins/io/stream_context_create.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_context_create` calls. Persists the options hash in -//! the runtime's `_stream_context_options` slot so -//! `stream_context_get_options` / `stream_context_set_option` and future -//! consumer integrations (http://, ftp://, fopen's 4th-arg context) can -//! read it back. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - v1 limitation: only one active context at a time — every -//! stream_context_create call overwrites the previous options slot. -//! Per-resource contexts would need a registry indexed by the synthetic -//! context fd; deferred until any real use case needs it. -//! - The options hash is `__rt_incref`'d before being saved so the global -//! slot survives the surrounding owner's scope-exit decref. -//! - Returns a non-zero synthetic resource id so `is_resource()` and -//! `gettype()` keep working as before. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_context_create()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_context_create()"); - if !args.is_empty() { - // -- evaluate the options array, retain it, and stash it globally -- - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - // Unique labels: a program may call stream_context_create more - // than once, so a fixed label name would be defined twice and - // fail to assemble. - let store_zero = ctx.next_label("scc_store_zero"); - let store_done = ctx.next_label("scc_store_done"); - emitter.instruction(&format!("cbz x0, {}", store_zero)); // null option ptr → store null without incref - abi::emit_symbol_address(emitter, "x9", "_stream_context_options"); - emitter.instruction("str x0, [x9]"); // _stream_context_options = options hash - emitter.instruction("bl __rt_incref"); // retain the hash so the global slot owns it - emitter.instruction(&format!("b {}", store_done)); // continue at target label - emitter.label(&store_zero); - abi::emit_symbol_address(emitter, "x9", "_stream_context_options"); - emitter.instruction("str xzr, [x9]"); // clear the slot when no options were passed - emitter.label(&store_done); - } - Arch::X86_64 => { - // Unique labels: see the AArch64 note above. - let store_zero = ctx.next_label("scc_store_zero_x86"); - let store_done = ctx.next_label("scc_store_done_x86"); - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", store_zero)); // null options pointer → clear the slot - abi::emit_symbol_address(emitter, "r9", "_stream_context_options"); // load runtime data address - emitter.instruction("mov QWORD PTR [r9], rax"); // _stream_context_options = options hash - emitter.instruction("mov rdi, rax"); // incref's SysV arg - emitter.instruction("call __rt_incref"); // retain the hash - emitter.instruction(&format!("jmp {}", store_done)); // continue at target label - emitter.label(&store_zero); - abi::emit_symbol_address(emitter, "r9", "_stream_context_options"); // load runtime data address - emitter.instruction("mov QWORD PTR [r9], 0"); // clear the slot - emitter.label(&store_done); - } - } - } else { - // -- no options arg: leave the slot untouched -- - } - // -- capture the optional second arg (params) `notification` callback -- - // Evaluates params for side effects and stashes a literal closure / - // first-class-callable `notification` entry into the global so __rt_http_open - // can fire it at the STREAM_NOTIFY_* milestones. - super::stream_notification::capture_notification_callback(args.get(1), emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #1"), // synthetic context resource id (1 = the single global context) - Arch::X86_64 => emitter.instruction("mov eax, 1"), // synthetic context resource id - } - Some(PhpType::stream_resource()) -} diff --git a/src/codegen/builtins/io/stream_context_get_default.rs b/src/codegen/builtins/io/stream_context_get_default.rs deleted file mode 100644 index b2898a7384..0000000000 --- a/src/codegen/builtins/io/stream_context_get_default.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_context_get_default` calls. -//! v1 returns a placeholder context resource matching `stream_context_create` -//! so PHP code that consults the default context compiles cleanly. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The optional options argument is evaluated for its side effects. Options -//! storage on the returned resource is deferred along with the rest of the -//! `stream_context_*` family. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_context_get_default()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_context_get_default()"); - // Evaluate the optional options for its side effects; v1 does not yet - // persist them on the returned default context. - for arg in args { - emit_expr(arg, emitter, ctx, data); - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #0"), // placeholder default-context resource identifier - Arch::X86_64 => emitter.instruction("xor eax, eax"), // placeholder default-context resource identifier - } - Some(PhpType::stream_resource()) -} diff --git a/src/codegen/builtins/io/stream_context_get_options.rs b/src/codegen/builtins/io/stream_context_get_options.rs deleted file mode 100644 index 05d4d889b1..0000000000 --- a/src/codegen/builtins/io/stream_context_get_options.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_context_get_options` calls. Returns the global -//! `_stream_context_options` hash (set by `stream_context_create` / -//! `stream_context_set_option`) so callers can inspect the persisted -//! options. When no context has been created yet, returns an empty hash. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The context argument is evaluated for its side effects but ignored: -//! v1 keeps a single global context, so every `$ctx` resolves to the -//! same hash. -//! - The returned pointer is the same hash stored globally — callers -//! should not free it. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_context_get_options()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_context_get_options()"); - // Evaluate the context for its side effects; v1 keeps one global context. - emit_expr(&args[0], emitter, ctx, data); - let empty_label = ctx.next_label("scgo_empty"); - let done_label = ctx.next_label("scgo_done"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", "_stream_context_options"); - emitter.instruction("ldr x0, [x9]"); // load the persisted hash pointer - emitter.instruction(&format!("cbz x0, {}", empty_label)); // no context yet → return an empty hash - emitter.instruction("bl __rt_incref"); // hand the caller a retained reference - emitter.instruction(&format!("b {}", done_label)); // continue at target label - emitter.label(&empty_label); - emitter.instruction("mov x0, #1"); // initial capacity for the empty fallback hash - emitter.instruction("mov x1, #7"); // value type tag = Mixed - abi::emit_call_label(emitter, "__rt_hash_new"); - emitter.label(&done_label); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", "_stream_context_options"); // load runtime data address - emitter.instruction("mov rax, QWORD PTR [r9]"); // load the persisted hash pointer - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", empty_label)); // empty fallback when no context exists - emitter.instruction("mov rdi, rax"); // incref's SysV arg - emitter.instruction("call __rt_incref"); // hand the caller a retained reference - emitter.instruction(&format!("jmp {}", done_label)); // continue at target label - emitter.label(&empty_label); - emitter.instruction("mov edi, 1"); // initial capacity - emitter.instruction("mov esi, 7"); // value type tag = Mixed - abi::emit_call_label(emitter, "__rt_hash_new"); - emitter.label(&done_label); - } - } - Some(PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Mixed), - }) -} diff --git a/src/codegen/builtins/io/stream_context_get_params.rs b/src/codegen/builtins/io/stream_context_get_params.rs deleted file mode 100644 index 6f5c8208cc..0000000000 --- a/src/codegen/builtins/io/stream_context_get_params.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_context_get_params` calls. -//! v1 stub: returns an empty associative array (allocated through -//! `__rt_hash_new`) because contexts do not yet persist their parameters. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Mirrors `stream_context_get_options`: the context is evaluated for its -//! side effects and an empty Mixed-valued associative hash is returned. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_context_get_params()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_context_get_params()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // initial capacity (minimum non-zero) - emitter.instruction("mov x1, #7"); // value type tag = Mixed - } - Arch::X86_64 => { - emitter.instruction("mov edi, 1"); // initial capacity (minimum non-zero) - emitter.instruction("mov esi, 7"); // value type tag = Mixed - } - } - abi::emit_call_label(emitter, "__rt_hash_new"); - Some(PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Mixed), - }) -} diff --git a/src/codegen/builtins/io/stream_context_set_default.rs b/src/codegen/builtins/io/stream_context_set_default.rs deleted file mode 100644 index 96e814f413..0000000000 --- a/src/codegen/builtins/io/stream_context_set_default.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_context_set_default($options)` calls. Returns the -//! default-context resource (matching `stream_context_get_default`). -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - v1 evaluates the options array for side effects but does not yet -//! walk-and-apply each entry into `_stream_context_options`. PHP code -//! that needs the options persisted should use repeated -//! `stream_context_set_option(stream_context_get_default(), ...)` -//! calls, which DO mutate the default context's options table. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_context_set_default()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_context_set_default()"); - for arg in args { - emit_expr(arg, emitter, ctx, data); - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #0"), // placeholder default-context resource identifier - Arch::X86_64 => emitter.instruction("xor eax, eax"), // placeholder default-context resource identifier - } - Some(PhpType::stream_resource()) -} diff --git a/src/codegen/builtins/io/stream_context_set_option.rs b/src/codegen/builtins/io/stream_context_set_option.rs deleted file mode 100644 index 065bfa08fd..0000000000 --- a/src/codegen/builtins/io/stream_context_set_option.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_context_set_option` calls. -//! -//! - 2-arg form `stream_context_set_option($ctx, array $options)`: -//! replaces the global `_stream_context_options` hash with the new -//! options array, same persistence semantics as -//! `stream_context_create`. -//! - 4-arg form `stream_context_set_option($ctx, $wrapper, $option, $value)`: -//! v1 stub — evaluates all arguments for side effects, reports `true`, -//! but the single-option update is not yet propagated into the global -//! hash. Full nested-hash mutation is deferred (the runtime support -//! would need __rt_hash_get + __rt_hash_set chained across the -//! wrapper sub-hash). -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The retained options hash is `__rt_incref`'d so the global slot -//! outlives the temporary owner produced by the caller's array -//! literal. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_context_set_option()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_context_set_option()"); - // -- evaluate the context arg (just for side effects: v1 has one global slot) -- - emit_expr(&args[0], emitter, ctx, data); - - if args.len() == 2 { - // 2-arg form: replace the global options hash with the new array. - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - let store_done = ctx.next_label("scso_store_done"); - emitter.instruction(&format!("cbz x0, {}", store_done)); // null options → leave the slot unchanged - abi::emit_symbol_address(emitter, "x9", "_stream_context_options"); - emitter.instruction("str x0, [x9]"); // overwrite the persisted options - emitter.instruction("bl __rt_incref"); // retain the new hash so the global slot owns it - emitter.label(&store_done); - emitter.instruction("mov x0, #1"); // PHP true - } - Arch::X86_64 => { - let store_done = ctx.next_label("scso_store_done_x86"); - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", store_done)); // null options → leave the slot unchanged - abi::emit_symbol_address(emitter, "r9", "_stream_context_options"); // load runtime data address - emitter.instruction("mov QWORD PTR [r9], rax"); // overwrite the persisted options - emitter.instruction("mov rdi, rax"); // incref's first arg - emitter.instruction("call __rt_incref"); // call runtime helper - emitter.label(&store_done); - emitter.instruction("mov eax, 1"); // PHP true - } - } - } else if args.len() == 4 { - // 4-arg form: stream_context_set_option($ctx, $wrapper, $opt, $value) - // Marshals the three strings into the runtime helper which navigates - // the nested options[wrapper][option] = value structure. - emit_expr(&args[1], emitter, ctx, data); // wrapper string in result regs - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve wrapper ptr/len - emit_expr(&args[2], emitter, ctx, data); // option string - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve option ptr/len - emit_expr(&args[3], emitter, ctx, data); // value string - emitter.instruction("mov x4, x1"); // value_ptr → 5th arg - emitter.instruction("mov x5, x2"); // value_len → 6th arg - abi::emit_pop_reg_pair(emitter, "x2", "x3"); // restore option ptr/len → 3rd/4th args - abi::emit_pop_reg_pair(emitter, "x0", "x1"); // restore wrapper ptr/len → 1st/2nd args - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve wrapper ptr/len - emit_expr(&args[2], emitter, ctx, data); // option string - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve option ptr/len - emit_expr(&args[3], emitter, ctx, data); // value string - emitter.instruction("mov r8, rax"); // value_ptr → 5th arg - emitter.instruction("mov r9, rdx"); // value_len → 6th arg - abi::emit_pop_reg_pair(emitter, "rdx", "rcx"); // restore option ptr/len → 3rd/4th args - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore wrapper ptr/len → 1st/2nd args - } - } - abi::emit_call_label(emitter, "__rt_stream_context_set_option_4"); - } else { - // Other arities (shouldn't reach here after the checker accepts only - // 2 or 4) — evaluate side effects and report success. - for arg in &args[1..] { - emit_expr(arg, emitter, ctx, data); - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #1"), // prepare AArch64 call argument - Arch::X86_64 => emitter.instruction("mov eax, 1"), // prepare runtime result value - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_context_set_params.rs b/src/codegen/builtins/io/stream_context_set_params.rs deleted file mode 100644 index c1341c8330..0000000000 --- a/src/codegen/builtins/io/stream_context_set_params.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_context_set_params($ctx, $params)` calls. Used -//! primarily for `notification` callbacks attached to a context. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Evaluates the context resource for side effects, then captures a literal -//! `['notification' => ]` entry from the params -//! array into the `_stream_notification_callback` global (see -//! `stream_notification::capture_notification_callback`). `__rt_http_open` -//! fires that callback at the `STREAM_NOTIFY_*` HTTP transfer milestones. -//! Always returns true (params accepted). v1 fires for `http://` only; HTTPS -//! and FTP milestones are deferred. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_context_set_params()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_context_set_params()"); - // Evaluate the context resource (args[0]) for its side effects, then capture - // a literal closure / first-class-callable `notification` entry from the - // params array (args[1]) into the global so __rt_http_open can fire it at the - // STREAM_NOTIFY_* milestones. - if let Some(context_arg) = args.first() { - emit_expr(context_arg, emitter, ctx, data); - } - super::stream_notification::capture_notification_callback(args.get(1), emitter, ctx, data); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1); // return true (params accepted) - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_copy_to_stream.rs b/src/codegen/builtins/io/stream_copy_to_stream.rs deleted file mode 100644 index 82cd510e74..0000000000 --- a/src/codegen/builtins/io/stream_copy_to_stream.rs +++ /dev/null @@ -1,414 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_copy_to_stream` calls. -//! Copies bytes from one stream resource to another, honoring the optional -//! `$length` (maximum bytes) and `$offset` (seek the source first) arguments. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Both arguments are unboxed to file descriptors; the source is preserved on -//! the stack while the destination expression is evaluated. -//! - With no `$length`/`$offset`: when EITHER side is a synthetic user-wrapper -//! descriptor (`>= 0x40000000`) a COMPILED, feof-gated loop reads each chunk -//! through `__rt_fread` and writes it through `__rt_fwrite` (both dispatch -//! normal vs wrapper fds); when both sides are real descriptors the efficient -//! `__rt_stream_copy_to_stream` syscall helper is used. -//! - With a `$length`/`$offset`: a capped `__rt_fread` / `__rt_fwrite` loop -//! runs for every fd combination (wrapper-aware). It seeks the source by -//! `$offset >= 0` first (lseek for a normal fd, the wrapper's `stream_seek` -//! for a synthetic fd); failed seeks box PHP `false`. Successful byte counts -//! are boxed too so `int|false` keeps one runtime representation. The loop -//! stops once `$length` bytes are copied or the source produces an empty read. -//! A `null`/negative `$length` copies to EOF; a negative/omitted `$offset` -//! does not seek. Returned chunks are clamped too, so wrappers that ignore the -//! requested count cannot copy past `$length`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::driver_support::emit_box_current_value_as_mixed; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; -use super::stream_get_contents::{emit_branch_if_unlimited_length, is_read_all_or_no_seek}; - -/// Emits codegen for PHP `stream_copy_to_stream()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_copy_to_stream()"); - emit_stream_fd_arg("stream_copy_to_stream", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the source descriptor while the destination is evaluated - emit_stream_fd_arg("stream_copy_to_stream", &args[1], emitter, ctx, data); - - let has_len = args.len() >= 3 && !is_read_all_or_no_seek(&args[2]); - let has_off = args.len() >= 4 && !is_read_all_or_no_seek(&args[3]); - if has_len || has_off { - return emit_bounded_copy(args, has_len, has_off, emitter, ctx, data); - } - - let wrapper_label = ctx.next_label("scs_wrapper"); - let loop_label = ctx.next_label("scs_loop"); - let release_eof_label = ctx.next_label("scs_release_eof"); - let wdone_label = ctx.next_label("scs_wrap_done"); - let done_label = ctx.next_label("scs_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // destination descriptor becomes the second helper argument - abi::emit_pop_reg(emitter, "x0"); // restore the source descriptor into the first helper argument - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 in w9 - emitter.instruction("cmp x0, x9"); // is the source a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper_label)); // wrapper source: use the compiled copy loop - emitter.instruction("cmp x1, x9"); // is the destination a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper_label)); // wrapper destination: use the compiled copy loop - abi::emit_call_label(emitter, "__rt_stream_copy_to_stream"); // both real fds: efficient syscall copy helper - emitter.instruction(&format!("b {}", done_label)); // skip the wrapper loop on the all-real path - - emitter.label(&wrapper_label); - emitter.instruction("sub sp, sp, #32"); // scratch: [sp,#0]=src, [sp,#8]=dst, [sp,#16]=total, [sp,#24]=chunk - emitter.instruction("str x0, [sp, #0]"); // save the source descriptor - emitter.instruction("str x1, [sp, #8]"); // save the destination descriptor - emitter.instruction("str xzr, [sp, #16]"); // bytes-copied total = 0 - emitter.label(&loop_label); - emitter.instruction("ldr x0, [sp, #0]"); // reload the source descriptor - abi::emit_call_label(emitter, "__rt_feof"); // check the source's EOF FIRST (x0 = 1 at EOF) - emitter.instruction(&format!("cbnz x0, {}", wdone_label)); // at EOF: stop without reading - emitter.instruction("ldr x0, [sp, #0]"); // reload the source descriptor - emitter.instruction("mov x1, #4096"); // request up to 4096 bytes - abi::emit_call_label(emitter, "__rt_fread"); // x1=chunk ptr, x2=len - emitter.instruction(&format!("cbz x2, {}", release_eof_label)); // defensive: empty read also stops - emitter.instruction("str x1, [sp, #24]"); // save the chunk ptr for the later release - emitter.instruction("ldr x9, [sp, #16]"); // current total - emitter.instruction("add x9, x9, x2"); // add this chunk's length - emitter.instruction("str x9, [sp, #16]"); // store the updated total - emitter.instruction("ldr x0, [sp, #8]"); // destination fd (x1=ptr, x2=len already in place) - abi::emit_call_label(emitter, "__rt_fwrite"); // write the chunk to the destination (dispatches wrapper vs fd) - emitter.instruction("ldr x0, [sp, #24]"); // reload the chunk ptr - abi::emit_call_label(emitter, "__rt_decref_any"); // release the owned chunk, then loop - emitter.instruction(&format!("b {}", loop_label)); // copy the next chunk - emitter.label(&release_eof_label); - emitter.instruction("mov x0, x1"); // the final (empty) owned chunk - abi::emit_call_label(emitter, "__rt_decref_any"); // release it (heap freed; non-heap skipped) - emitter.label(&wdone_label); - emitter.instruction("ldr x0, [sp, #16]"); // return the total bytes copied - emitter.instruction("add sp, sp, #32"); // release the scratch frame - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // destination descriptor becomes the second SysV argument - abi::emit_pop_reg(emitter, "rdi"); // restore the source descriptor into the first SysV argument - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rdi, r9"); // is the source a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper_label)); // wrapper source: use the compiled copy loop - emitter.instruction("cmp rsi, r9"); // is the destination a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper_label)); // wrapper destination: use the compiled copy loop - abi::emit_call_label(emitter, "__rt_stream_copy_to_stream"); // both real fds: efficient syscall copy helper - emitter.instruction(&format!("jmp {}", done_label)); // skip the wrapper loop on the all-real path - - emitter.label(&wrapper_label); - emitter.instruction("sub rsp, 32"); // scratch: [rsp+0]=src, [rsp+8]=dst, [rsp+16]=total, [rsp+24]=chunk - emitter.instruction("mov QWORD PTR [rsp + 0], rdi"); // save the source descriptor - emitter.instruction("mov QWORD PTR [rsp + 8], rsi"); // save the destination descriptor - emitter.instruction("mov QWORD PTR [rsp + 16], 0"); // bytes-copied total = 0 - emitter.label(&loop_label); - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the source descriptor - abi::emit_call_label(emitter, "__rt_feof"); // check the source's EOF FIRST (rax = 1 at EOF) - emitter.instruction("test rax, rax"); // at EOF? - emitter.instruction(&format!("jnz {}", wdone_label)); // at EOF: stop without reading - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the source descriptor - emitter.instruction("mov rsi, 4096"); // request up to 4096 bytes - abi::emit_call_label(emitter, "__rt_fread"); // rax=chunk ptr, rdx=len - emitter.instruction("test rdx, rdx"); // zero-length read? - emitter.instruction(&format!("jz {}", release_eof_label)); // defensive: empty read also stops - emitter.instruction("mov QWORD PTR [rsp + 24], rax"); // save the chunk ptr for the later release - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // current total - emitter.instruction("add r8, rdx"); // add this chunk's length - emitter.instruction("mov QWORD PTR [rsp + 16], r8"); // store the updated total - emitter.instruction("mov rsi, rax"); // chunk ptr → second fwrite argument - emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // destination fd → first argument (rdx=len already in place) - abi::emit_call_label(emitter, "__rt_fwrite"); // write the chunk to the destination (dispatches wrapper vs fd) - emitter.instruction("mov rax, QWORD PTR [rsp + 24]"); // reload the chunk ptr - abi::emit_call_label(emitter, "__rt_decref_any"); // release the owned chunk, then loop - emitter.instruction(&format!("jmp {}", loop_label)); // copy the next chunk - emitter.label(&release_eof_label); - abi::emit_call_label(emitter, "__rt_decref_any"); // release the final (empty) chunk (rax=ptr) - emitter.label(&wdone_label); - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // return the total bytes copied - emitter.instruction("add rsp, 32"); // release the scratch frame - emitter.label(&done_label); - } - } - emit_box_current_value_as_mixed(emitter, &PhpType::Int); - Some(PhpType::Mixed) -} - -/// Emits the bounded `stream_copy_to_stream($from, $to, $length, $offset)` path: -/// a single capped `__rt_fread`/`__rt_fwrite` loop that works for any -/// real/wrapper fd combination. On entry the source descriptor sits on the stack -/// (pushed by the caller) and the destination is in the int-result register. -/// Evaluates `$length` then `$offset` (PHP source order), seeks the source when -/// `$offset >= 0`, and copies until `$length` bytes are written or the source -/// produces an empty read. Returns a boxed byte count, or boxed false when the -/// seek requested by `$offset` fails. -fn emit_bounded_copy( - args: &[Expr], - has_len: bool, - has_off: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let skip_seek = ctx.next_label("scs_skip_seek"); - let wrap_seek = ctx.next_label("scs_wrap_seek"); - let seek_failed_label = ctx.next_label("scs_seek_failed"); - let loop_label = ctx.next_label("scs_b_loop"); - let release_eof_label = ctx.next_label("scs_b_release_eof"); - let done_label = ctx.next_label("scs_b_done"); - let boxed_done_label = ctx.next_label("scs_b_boxed_done"); - let len_unlimited_label = ctx.next_label("scs_b_len_unlimited"); - let after_len_check_label = ctx.next_label("scs_b_after_len_check"); - let request_unlimited_label = ctx.next_label("scs_b_request_unlimited"); - let after_request_label = ctx.next_label("scs_b_after_request"); - let chunk_unlimited_label = ctx.next_label("scs_b_chunk_unlimited"); - let after_chunk_label = ctx.next_label("scs_b_after_chunk"); - // Frame: [0]=src, [8]=dst, [16]=total, [24]=chunk_ptr, [32]=max_len (48 = 0 mod 16). - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // destination → temp - abi::emit_pop_reg(emitter, "x0"); // restore the source descriptor - emitter.instruction("sub sp, sp, #48"); // bounded-copy frame (16-aligned) - emitter.instruction("str x0, [sp, #0]"); // save the source descriptor - emitter.instruction("str x1, [sp, #8]"); // save the destination descriptor - emitter.instruction("str xzr, [sp, #16]"); // bytes-copied total = 0 - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // destination → temp - abi::emit_pop_reg(emitter, "rdi"); // restore the source descriptor - emitter.instruction("sub rsp, 48"); // bounded-copy frame (16-aligned) - emitter.instruction("mov QWORD PTR [rsp + 0], rdi"); // save the source descriptor - emitter.instruction("mov QWORD PTR [rsp + 8], rsi"); // save the destination descriptor - emitter.instruction("mov QWORD PTR [rsp + 16], 0"); // bytes-copied total = 0 - } - } - // $length → max_len (or -1 when omitted/unlimited). - if has_len { - emit_expr(&args[2], emitter, ctx, data); // evaluate $length first (source order) - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("str x0, [sp, #32]"), // save the requested max byte count - Arch::X86_64 => emitter.instruction("mov QWORD PTR [rsp + 32], rax"),// save the requested max byte count - } - } - // $offset: seek the source before copying. - if has_off { - emit_expr(&args[3], emitter, ctx, data); // evaluate $offset after $length - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // a negative offset means "do not seek" - emitter.instruction(&format!("b.lt {}", skip_seek)); // skip the seek on a negative offset - emitter.instruction("mov x1, x0"); // offset → seek arg1 - emitter.instruction("mov x2, #0"); // whence = SEEK_SET - emitter.instruction("ldr x0, [sp, #0]"); // reload the source fd → seek arg0 - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 - emitter.instruction("cmp x0, x9"); // synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrap_seek)); // wrapper: dispatch stream_seek - emitter.syscall(199); // lseek(src, offset, SEEK_SET) - if emitter.platform.needs_cmp_before_error_branch() { - emitter.instruction("cmp x0, #0"); // Linux reports lseek failure as a negative result - } - emitter.instruction(&emitter.platform.branch_on_syscall_success(&skip_seek)); // continue only when lseek succeeded - emitter.instruction(&format!("b {}", seek_failed_label)); // seek failure makes stream_copy_to_stream() return false - emitter.label(&wrap_seek); - abi::emit_call_label(emitter, "__rt_user_wrapper_fseek"); // wrapper stream_seek(offset, SEEK_SET) - emitter.instruction("cmp x0, #0"); // did the wrapper stream_seek report success? - emitter.instruction(&format!("b.ne {}", seek_failed_label)); // wrapper seek failure returns PHP false - emitter.label(&skip_seek); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // a negative offset means "do not seek" - emitter.instruction(&format!("jl {}", skip_seek)); // skip the seek on a negative offset - emitter.instruction("mov rsi, rax"); // offset → seek arg1 - emitter.instruction("mov rdx, 0"); // whence = SEEK_SET - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the source fd → seek arg0 - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rdi, r9"); // synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrap_seek)); // wrapper: dispatch stream_seek - emitter.instruction("call lseek"); // lseek(src, offset, SEEK_SET) - emitter.instruction("cmp rax, 0"); // did libc lseek return a non-negative offset? - emitter.instruction(&format!("jl {}", seek_failed_label)); // seek failure makes stream_copy_to_stream() return false - emitter.instruction(&format!("jmp {}", skip_seek)); // normal fd seeked successfully - emitter.label(&wrap_seek); - abi::emit_call_label(emitter, "__rt_user_wrapper_fseek"); // wrapper stream_seek(offset, SEEK_SET) - emitter.instruction("cmp rax, 0"); // did the wrapper stream_seek report success? - emitter.instruction(&format!("jne {}", seek_failed_label)); // wrapper seek failure returns PHP false - emitter.label(&skip_seek); - } - } - } - // Capped feof-gated copy loop. - emitter.label(&loop_label); - match emitter.target.arch { - Arch::AArch64 => { - if has_len { - emitter.instruction("ldr x9, [sp, #16]"); // bytes copied so far - emitter.instruction("ldr x10, [sp, #32]"); // requested max byte count - emit_branch_if_unlimited_length( - emitter, - "x10", - "x11", - &len_unlimited_label, - ); - emitter.instruction("cmp x9, x10"); // reached the requested length? - emitter.instruction(&format!("b.ge {}", done_label)); // stop once $length bytes are copied - emitter.instruction(&format!("b {}", after_len_check_label)); // finite length still has bytes to copy - emitter.label(&len_unlimited_label); - emitter.label(&after_len_check_label); - } - emitter.instruction("ldr x0, [sp, #0]"); // reload the source descriptor - emitter.instruction("mov x1, #4096"); // default request: up to 4096 bytes - if has_len { - emitter.instruction("ldr x10, [sp, #32]"); // max byte count - emit_branch_if_unlimited_length( - emitter, - "x10", - "x11", - &request_unlimited_label, - ); - emitter.instruction("ldr x9, [sp, #16]"); // bytes copied so far - emitter.instruction("sub x10, x10, x9"); // remaining = max - total (>= 1 here) - emitter.instruction("cmp x10, x1"); // is the remainder smaller than 4096? - emitter.instruction("csel x1, x10, x1, lt"); // clamp the request to the remainder - emitter.instruction(&format!("b {}", after_request_label)); // finite request size is ready - emitter.label(&request_unlimited_label); - emitter.label(&after_request_label); - } - abi::emit_call_label(emitter, "__rt_fread"); // x1=chunk ptr, x2=len - emitter.instruction(&format!("cbz x2, {}", release_eof_label)); // defensive: empty read also stops - if has_len { - emitter.instruction("ldr x10, [sp, #32]"); // max byte count - emit_branch_if_unlimited_length( - emitter, - "x10", - "x11", - &chunk_unlimited_label, - ); - emitter.instruction("ldr x9, [sp, #16]"); // bytes copied so far - emitter.instruction("sub x10, x10, x9"); // remaining bytes allowed by $length - emitter.instruction("cmp x2, x10"); // did the wrapper return more than was requested? - emitter.instruction("csel x2, x2, x10, ls"); // clamp the written chunk to the remaining length - emitter.instruction(&format!("b {}", after_chunk_label)); // finite chunk length is clamped - emitter.label(&chunk_unlimited_label); - emitter.label(&after_chunk_label); - } - emitter.instruction("str x1, [sp, #24]"); // save the chunk ptr for the later release - emitter.instruction("ldr x9, [sp, #16]"); // current total - emitter.instruction("add x9, x9, x2"); // add this chunk's length - emitter.instruction("str x9, [sp, #16]"); // store the updated total - emitter.instruction("ldr x0, [sp, #8]"); // destination fd (x1=ptr, x2=len already in place) - abi::emit_call_label(emitter, "__rt_fwrite"); // write the chunk (dispatches wrapper vs fd) - emitter.instruction("ldr x0, [sp, #24]"); // reload the chunk ptr - abi::emit_call_label(emitter, "__rt_decref_any"); // release the owned chunk, then loop - emitter.instruction(&format!("b {}", loop_label)); // copy the next chunk - emitter.label(&release_eof_label); - emitter.label(&done_label); - emitter.instruction("ldr x0, [sp, #16]"); // return the total bytes copied - emitter.instruction("add sp, sp, #48"); // release the bounded-copy frame - } - Arch::X86_64 => { - if has_len { - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // bytes copied so far - emitter.instruction("mov r9, QWORD PTR [rsp + 32]"); // requested max byte count - emit_branch_if_unlimited_length( - emitter, - "r9", - "r10", - &len_unlimited_label, - ); - emitter.instruction("cmp r8, r9"); // reached the requested length? - emitter.instruction(&format!("jge {}", done_label)); // stop once $length bytes are copied - emitter.instruction(&format!("jmp {}", after_len_check_label)); // finite length still has bytes to copy - emitter.label(&len_unlimited_label); - emitter.label(&after_len_check_label); - } - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the source descriptor - emitter.instruction("mov rsi, 4096"); // default request: up to 4096 bytes - if has_len { - emitter.instruction("mov r9, QWORD PTR [rsp + 32]"); // max byte count - emit_branch_if_unlimited_length( - emitter, - "r9", - "r10", - &request_unlimited_label, - ); - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // bytes copied so far - emitter.instruction("sub r9, r8"); // remaining = max - total (>= 1 here) - emitter.instruction("cmp r9, rsi"); // is the remainder smaller than 4096? - emitter.instruction("cmovl rsi, r9"); // clamp the request to the remainder - emitter.instruction(&format!("jmp {}", after_request_label)); // finite request size is ready - emitter.label(&request_unlimited_label); - emitter.label(&after_request_label); - } - abi::emit_call_label(emitter, "__rt_fread"); // rax=chunk ptr, rdx=len - emitter.instruction("test rdx, rdx"); // zero-length read? - emitter.instruction(&format!("jz {}", release_eof_label)); // defensive: empty read also stops - if has_len { - emitter.instruction("mov r9, QWORD PTR [rsp + 32]"); // max byte count - emit_branch_if_unlimited_length( - emitter, - "r9", - "r10", - &chunk_unlimited_label, - ); - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // bytes copied so far - emitter.instruction("sub r9, r8"); // remaining bytes allowed by $length - emitter.instruction("cmp rdx, r9"); // did the wrapper return more than was requested? - emitter.instruction("cmova rdx, r9"); // clamp the written chunk to the remaining length - emitter.instruction(&format!("jmp {}", after_chunk_label)); // finite chunk length is clamped - emitter.label(&chunk_unlimited_label); - emitter.label(&after_chunk_label); - } - emitter.instruction("mov QWORD PTR [rsp + 24], rax"); // save the chunk ptr for the later release - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // current total - emitter.instruction("add r8, rdx"); // add this chunk's length - emitter.instruction("mov QWORD PTR [rsp + 16], r8"); // store the updated total - emitter.instruction("mov rsi, rax"); // chunk ptr → second fwrite argument - emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // destination fd → first argument (rdx=len already in place) - abi::emit_call_label(emitter, "__rt_fwrite"); // write the chunk (dispatches wrapper vs fd) - emitter.instruction("mov rax, QWORD PTR [rsp + 24]"); // reload the chunk ptr - abi::emit_call_label(emitter, "__rt_decref_any"); // release the owned chunk, then loop - emitter.instruction(&format!("jmp {}", loop_label)); // copy the next chunk - emitter.label(&release_eof_label); - emitter.label(&done_label); - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // return the total bytes copied - emitter.instruction("add rsp, 48"); // release the bounded-copy frame - } - } - emit_box_current_value_as_mixed(emitter, &PhpType::Int); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", boxed_done_label)), // successful copy skips the seek-failure boxing path - Arch::X86_64 => emitter.instruction(&format!("jmp {}", boxed_done_label)), // successful copy skips the seek-failure boxing path - } - emitter.label(&seek_failed_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("add sp, sp, #48"); // release the bounded-copy frame after a failed seek - emitter.instruction("mov x0, #0"); // false payload = 0 - } - Arch::X86_64 => { - emitter.instruction("add rsp, 48"); // release the bounded-copy frame after a failed seek - emitter.instruction("xor eax, eax"); // false payload = 0 - } - } - emit_box_current_value_as_mixed(emitter, &PhpType::Bool); - emitter.label(&boxed_done_label); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/stream_filter.rs b/src/codegen/builtins/io/stream_filter.rs deleted file mode 100644 index 1fcf9de390..0000000000 --- a/src/codegen/builtins/io/stream_filter.rs +++ /dev/null @@ -1,340 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_filter_append`, `stream_filter_prepend` and -//! `stream_filter_remove` calls. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - v1 attaches one built-in filter per stream per direction. The filter name -//! is a compile-time literal mapped to a small id (1 = `string.toupper`, -//! 2 = `string.tolower`, 3 = `string.rot13`) and stored in the per-fd -//! `_stream_read_filters` / `_stream_write_filters` tables. -//! - `append` and `prepend` are equivalent in this single-filter model; both -//! return the stream re-boxed as a resource. `remove` clears both tables. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Maps a built-in stream filter name to its runtime filter id. -/// -/// Ids 1..3 are simple byte-by-byte transforms implemented inside -/// `__rt_apply_stream_filter`. Ids 4..9 are richer state-machine / -/// ratio-changing transforms, each dispatched to its own explicit -/// `__rt_asf_*` case in `runtime/io/stream_filter.rs`: 4 strip_tags, -/// 5 dechunk, 6 base64-encode, 7 base64-decode, 8 quoted-printable-encode, -/// 9 quoted-printable-decode. All are real transforms (covered by the -/// `test_stream_filter_{base64,qp,strip_tags,dechunk}*` tests) and the -/// names also round-trip through `stream_get_filters`/`stream_filter_append`. -pub(super) fn filter_id(name: &str) -> Option { - match name { - "string.toupper" => Some(1), - "string.tolower" => Some(2), - "string.rot13" => Some(3), - // Real state-machine / ratio-changing transforms (not stubs): - // dechunk parses HTTP/1.1 chunked transfer encoding; - // base64/quoted-printable change the 3:4 or 4:3 byte ratio; - // strip_tags is a tag-aware state machine. Each has an explicit - // `__rt_asf_*` case in runtime/io/stream_filter.rs. - "string.strip_tags" => Some(4), - "dechunk" => Some(5), - "convert.base64-encode" => Some(6), - "convert.base64-decode" => Some(7), - "convert.quoted-printable-encode" => Some(8), - "convert.quoted-printable-decode" => Some(9), - _ => None, - } -} - -/// Extracts a compile-time-constant integer from the 4th -/// `stream_filter_append`/`prepend` argument (`$params`), honoring both of -/// PHP's literal forms: -/// -/// - a bare int literal — `stream_filter_append($fp, 'zlib.deflate', $rw, 6)` — -/// which PHP treats as the single primary value (zlib level / bzip2 blocks); -/// returned only when `key` is the primary key (`"level"` / `"blocks"`), and -/// - the canonical array form — `['level' => 6]` / `['blocks' => 1, 'work' => 30]` -/// — from which the entry under `key` is read. -/// -/// Returns `None` for a missing arg, a non-constant expression, or an array -/// with no static int under `key`, in which case the filter keeps its default. -/// Clamps the value into `[min, max]` so an out-of-range literal cannot reach -/// the C library. `primary` marks the key a bare scalar maps to (only the -/// primary key consumes a bare int; secondary keys like `"work"` come only from -/// the array form). -pub(super) fn const_int_param( - args: &[Expr], - key: &str, - primary: bool, - min: i64, - max: i64, -) -> Option { - match args.get(3).map(|a| &a.kind) { - Some(ExprKind::IntLiteral(v)) if primary => Some((*v).clamp(min, max)), - Some(ExprKind::ArrayLiteralAssoc(items)) => items.iter().find_map(|(k, v)| match (&k.kind, &v.kind) { - (ExprKind::StringLiteral(name), ExprKind::IntLiteral(n)) if name == key => { - Some((*n).clamp(min, max)) - } - _ => None, - }), - _ => None, - } -} - -/// Emits `stream_filter_append` / `stream_filter_prepend`. In the single-filter -/// model the two are equivalent. -pub fn emit_attach( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - // The zlib.* filters call into libz, so they have dedicated emitters that - // keep the libz dependency out of programs not using them. - if let ExprKind::StringLiteral(name) = &args[1].kind { - if name == "zlib.deflate" { - return crate::codegen::builtins::io::stream_filter_zlib::emit_zlib_deflate_attach( - args, emitter, ctx, data, - ); - } - if name == "zlib.inflate" { - return crate::codegen::builtins::io::stream_filter_inflate::emit_zlib_inflate_attach( - args, emitter, ctx, data, - ); - } - if let Some(spec) = name.strip_prefix("convert.iconv.") { - // convert.iconv./ transcodes via libc iconv at attach time - // (slurp + convert + dup2), like the zlib.inflate read filter. - return crate::codegen::builtins::io::stream_filter_iconv::emit( - spec, args, emitter, ctx, data, - ); - } - if name == "bzip2.compress" { - // bzip2.compress streams writes through libbz2 (BZ2_bzCompress), - // mirroring the zlib.deflate write filter. - return crate::codegen::builtins::io::stream_filter_bzip2::emit_bzip2_compress_attach( - args, emitter, ctx, data, - ); - } - if name == "bzip2.decompress" { - // bzip2.decompress slurps + one-shot decompresses the stream at - // attach time (reusing the compress.bzip2:// read core), like - // zlib.inflate. - return crate::codegen::builtins::io::stream_filter_bzip2::emit_bzip2_decompress_attach( - args, emitter, ctx, data, - ); - } - } - // Names that are not built-in filters route into the user-filter runtime - // path: stream_filter_attach_user resolves the name through the registry, - // instantiates the class, and stores per-(fd, dir) state. - let built_in_id = match &args[1].kind { - ExprKind::StringLiteral(name) => filter_id(name), - _ => None, - }; - if built_in_id.is_none() { - return emit_attach_user(args, emitter, ctx, data); - } - emitter.comment("stream_filter_append()"); - let id = built_in_id.unwrap_or(0); - emit_stream_fd_arg("stream_filter_append", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - } else { - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #3"), // default mode STREAM_FILTER_ALL - Arch::X86_64 => emitter.instruction("mov eax, 3"), // default mode STREAM_FILTER_ALL - } - } - let skip_read = ctx.next_label("sf_skip_read"); - let skip_write = ctx.next_label("sf_skip_write"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x1"); // descriptor into x1, mode in x0 - emitter.instruction("tst x0, #1"); // STREAM_FILTER_READ bit set? - emitter.instruction(&format!("b.eq {}", skip_read)); // skip the read-filter table otherwise - abi::emit_symbol_address(emitter, "x9", "_stream_read_filters"); - emitter.instruction(&format!("mov w10, #{}", id)); // built-in filter id - emitter.instruction("strb w10, [x9, x1]"); // record the read filter for this descriptor - emitter.label(&skip_read); - emitter.instruction("tst x0, #2"); // STREAM_FILTER_WRITE bit set? - emitter.instruction(&format!("b.eq {}", skip_write)); // skip the write-filter table otherwise - abi::emit_symbol_address(emitter, "x9", "_stream_write_filters"); - emitter.instruction(&format!("mov w10, #{}", id)); // built-in filter id - emitter.instruction("strb w10, [x9, x1]"); // record the write filter for this descriptor - emitter.label(&skip_write); - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "rcx"); // descriptor into rcx, mode in rax - emitter.instruction("test rax, 1"); // STREAM_FILTER_READ bit set? - emitter.instruction(&format!("jz {}", skip_read)); // skip the read-filter table otherwise - abi::emit_symbol_address(emitter, "r9", "_stream_read_filters"); // read-filter table base - emitter.instruction(&format!("mov BYTE PTR [r9 + rcx], {}", id)); // record the read filter for this descriptor - emitter.label(&skip_read); - emitter.instruction("test rax, 2"); // STREAM_FILTER_WRITE bit set? - emitter.instruction(&format!("jz {}", skip_write)); // skip the write-filter table otherwise - abi::emit_symbol_address(emitter, "r9", "_stream_write_filters"); // write-filter table base - emitter.instruction(&format!("mov BYTE PTR [r9 + rcx], {}", id)); // record the write filter for this descriptor - emitter.label(&skip_write); - emitter.instruction("mov rdi, rcx"); // resource payload = the descriptor - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource - } - } - Some(PhpType::Mixed) -} - -/// Emits the user-filter attach path: resolves the filter name through -/// the runtime registry, instantiates the wrapper class via -/// `__rt_new_by_name`, and records per-(fd, dir) state. Returns the -/// stream re-boxed as a filter resource on success, PHP `false` -/// (boxed bool) on miss (unknown filter / class). -fn emit_attach_user( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_filter_append() — user filter"); - emit_stream_fd_arg("stream_filter_append", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor across the name + mode expressions - emit_expr(&args[1], emitter, ctx, data); // filter-name string in elephc string-result regs - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve filter-name ptr/len across the mode expression - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - } else { - emitter.instruction("mov x0, #3"); // default mode STREAM_FILTER_ALL - } - abi::emit_push_reg(emitter, "x0"); // preserve mode while materializing legacy null params - emit_legacy_user_filter_null_params(emitter); - emitter.instruction("mov x4, x0"); // pass null params to the shared attach helper - abi::emit_pop_reg(emitter, "x3"); // move mode into the helper's 4th arg - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore filter-name ptr/len → helper's 2nd/3rd args - // Peek the descriptor: it remains on the stack so we can box - // it as the filter resource after the helper returns. - emitter.instruction("ldr x0, [sp]"); // descriptor → helper's 1st arg (no pop yet) - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve filter-name ptr/len across the mode expression - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - } else { - emitter.instruction("mov eax, 3"); // default mode STREAM_FILTER_ALL - } - abi::emit_push_reg(emitter, "rax"); // preserve mode while materializing legacy null params - emit_legacy_user_filter_null_params(emitter); - emitter.instruction("mov r8, rax"); // pass null params to the shared attach helper - abi::emit_pop_reg(emitter, "rcx"); // move mode into the helper's 4th arg - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore filter-name ptr/len → helper's 2nd/3rd args - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // peek the descriptor → helper's 1st arg - } - } - abi::emit_call_label(emitter, "__rt_stream_filter_attach_user"); // returns bool: 1 = registered+attached, 0 = unknown/instantiation-fail - let fail_label = ctx.next_label("sfau_false"); - let done_label = ctx.next_label("sfau_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x0, {}", fail_label)); // PHP false on unknown filter - emitter.instruction("ldr x1, [sp]"); // peek descriptor for the filter-resource payload - abi::emit_release_temporary_stack(emitter, 16); // now drop the saved descriptor - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource (value_lo = fd) - emitter.instruction(&format!("b {}", done_label)); // continue at target label - emitter.label(&fail_label); - abi::emit_release_temporary_stack(emitter, 16); // drop the saved descriptor on the failure path too - emitter.instruction("mov x1, #0"); // bool payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box PHP false for callers that test `!== false` - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", fail_label)); // PHP false on unknown filter - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // peek descriptor for resource payload - abi::emit_release_temporary_stack(emitter, 16); // drop the saved descriptor - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource (value_lo = fd) - emitter.instruction(&format!("jmp {}", done_label)); // continue at target label - emitter.label(&fail_label); - abi::emit_release_temporary_stack(emitter, 16); // drop the saved descriptor - emitter.instruction("xor edi, edi"); // bool payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box PHP false - emitter.label(&done_label); - } - } - Some(PhpType::Mixed) -} - -/// Materializes a boxed null params value for the frozen AST backend's shared -/// user-filter attach helper call. -fn emit_legacy_user_filter_null_params(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // null params have no payload - } - Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // null params have no payload - } - } - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Void); -} - -/// Emits `stream_filter_remove`: clears the read and write filters of the -/// stream the filter resource refers to. -pub fn emit_remove( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_filter_remove()"); - emit_stream_fd_arg("stream_filter_remove", &args[0], emitter, ctx, data); - // Run onClose() on any attached user-filter instance + clear the - // _user_filter_instances slots for both directions. The helper - // takes the fd in x0 / rdi and preserves it in the standard - // int-result reg on return so the byte-table clear below still - // sees the fd. - if matches!(emitter.target.arch, Arch::X86_64) { - emitter.instruction("mov rdi, rax"); // fd → SysV first arg - } - abi::emit_call_label(emitter, "__rt_user_filter_release_fd"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", "_stream_read_filters"); - emitter.instruction("strb wzr, [x9, x0]"); // clear the read filter for this descriptor - abi::emit_symbol_address(emitter, "x9", "_stream_write_filters"); - emitter.instruction("strb wzr, [x9, x0]"); // clear the write filter for this descriptor - emitter.instruction("mov x0, #1"); // stream_filter_remove() returns true - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r9", "_stream_read_filters"); // read-filter table base - emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // clear the read filter for this descriptor - abi::emit_symbol_address(emitter, "r9", "_stream_write_filters"); // write-filter table base - emitter.instruction("mov BYTE PTR [r9 + rax], 0"); // clear the write filter for this descriptor - emitter.instruction("mov eax, 1"); // stream_filter_remove() returns true - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_filter_bzip2.rs b/src/codegen/builtins/io/stream_filter_bzip2.rs deleted file mode 100644 index 1cb3bcee06..0000000000 --- a/src/codegen/builtins/io/stream_filter_bzip2.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! Purpose: -//! Emits the `bzip2.compress` (write-direction) and `bzip2.decompress` -//! (read-direction) stream filter attachments for `stream_filter_append` / -//! `stream_filter_prepend`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::stream_filter::emit_attach()` when the -//! filter-name literal is `"bzip2.compress"` or `"bzip2.decompress"`. -//! -//! Key details: -//! - `bzip2.compress` mirrors the `zlib.deflate` write filter -//! (`stream_filter_zlib.rs`): a per-fd `bz_stream` is initialized with -//! `BZ2_bzCompressInit`, each `fwrite` streams the payload through -//! `BZ2_bzCompress(BZ_RUN)` into the shared scratch window, and `fclose` -//! flushes the tail via `BZ2_bzCompress(BZ_FINISH)` until `BZ_STREAM_END`, -//! then `BZ2_bzCompressEnd`. The libbz2 symbols are named ONLY from this -//! per-program USER asm; the shared runtime reaches them indirectly through -//! the `_bz2_fwrite_fn` / `_bz2_close_fn` slots, so non-bzip2 programs never -//! link `-lbz2`. The write-filter table entry is id 10 and the per-fd handle -//! lives in `_bzstream_handles`; the attach sets BOTH. -//! - `bzip2.decompress` reuses the already-shipped `compress.bzip2://` read core -//! (`compress_bzip2_stream::emit_arm64`/`emit_x86_64`): slurp the whole -//! compressed stream, one-shot `BZ2_bzBuffToBuffDecompress`, write to a temp -//! file, `dup2` onto the descriptor — so later `fread`/`fseek`/`feof` work -//! unchanged. Those helpers already re-box the descriptor as a resource. -//! - `bz_stream` is LP64-sized (80 bytes): next_in@0, avail_in@8(u32), -//! next_out@24, avail_out@32(u32) — the same offsets as `z_stream`. Zeroing it -//! leaves bzalloc/bzfree/opaque NULL so libbz2 uses its default allocator. The -//! struct itself is intentionally not freed on close (small documented leak, -//! matching the zlib filter). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Size of the libbz2 `bz_stream` struct on LP64 targets, in bytes. -const BZ_STREAM_SIZE: i64 = 80; -/// Capacity of the shared `_stream_filter_buf` scratch used as the compress -/// output window. -const FILTER_BUF_SIZE: i64 = 65536; -/// x86_64 owned-heap kind word: the elephc heap marker in the high 32 bits. -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits the `bzip2.compress` write-filter attachment. Returns the stream -/// re-boxed as a resource, matching `stream_filter_append`'s contract. -pub fn emit_bzip2_compress_attach( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_filter_append(bzip2.compress)"); - emit_stream_fd_arg("stream_filter_append", &args[0], emitter, ctx, data); - - // PHP's 4th `$params` arg, as either a bare int (`$rw, 1`) setting the bzip2 - // blockSize100k (1..9), or the canonical array form (`['blocks' => 1, - // 'work' => 30]`) from which `blocks` (blockSize100k) and `work` (workFactor, - // 0..250) are read. Both literal forms are honored at compile time; anything - // else keeps the defaults (blocks 9, work 0 = libbz2's default). - let block_size = super::stream_filter::const_int_param(args, "blocks", true, 1, 9).unwrap_or(9); - let work_factor = - super::stream_filter::const_int_param(args, "work", false, 0, 250).unwrap_or(0); - - let fwrite_label = ctx.next_label("bz2_compress_fwrite"); - let close_label = ctx.next_label("bz2_compress_close"); - let skip_label = ctx.next_label("bz2_compress_skip_helpers"); - - match emitter.target.arch { - Arch::AArch64 => emit_compress_arm64( - emitter, - &fwrite_label, - &close_label, - &skip_label, - block_size, - work_factor, - ), - Arch::X86_64 => emit_compress_x86_64( - emitter, - &fwrite_label, - &close_label, - &skip_label, - block_size, - work_factor, - ), - } - Some(PhpType::Mixed) -} - -/// Emits the `bzip2.decompress` read-filter attachment. The descriptor is -/// already open (from `emit_stream_fd_arg`); the shipped `compress.bzip2://` -/// read core slurps, decompresses, and `dup2`s a temp file onto it, then -/// re-boxes the descriptor as a resource. -pub fn emit_bzip2_decompress_attach( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_filter_append(bzip2.decompress)"); - emit_stream_fd_arg("stream_filter_append", &args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => super::compress_bzip2_stream::emit_arm64(emitter, |prefix| ctx.next_label(prefix)), - Arch::X86_64 => super::compress_bzip2_stream::emit_x86_64(emitter, |prefix| ctx.next_label(prefix)), - } - Some(PhpType::Mixed) -} - -/// Emits the reusable ARM64 `bzip2.decompress` read-filter body. -pub(crate) fn emit_decompress_arm64(emitter: &mut Emitter, next_label: F) -where - F: FnMut(&str) -> String, -{ - super::compress_bzip2_stream::emit_arm64(emitter, next_label); -} - -/// Emits the reusable x86_64 `bzip2.decompress` read-filter body. -pub(crate) fn emit_decompress_x86_64(emitter: &mut Emitter, next_label: F) -where - F: FnMut(&str) -> String, -{ - super::compress_bzip2_stream::emit_x86_64(emitter, next_label); -} - -/// Emits the ARM64 compress helpers, then the bz_stream initialization. -/// `work_factor` is the bzip2 workFactor (0..250, 0 = libbz2 default) from the -/// `['work' => N]` `$params` entry. -pub(crate) fn emit_compress_arm64( - emitter: &mut Emitter, - fwrite_label: &str, - close_label: &str, - skip_label: &str, - block_size: i64, - work_factor: i64, -) { - // -- jump past the helper bodies so normal flow never falls into them -- - emitter.instruction(&format!("b {}", skip_label)); // skip over the inline bzip2 helper routines - - // ================================================================ - // bzip2 compress fwrite helper. - // Input: x0 = fd, x1 = payload pointer, x2 = payload length. - // Output: x0 = the input payload length (bytes "written"). - // ================================================================ - emitter.label(fwrite_label); - emitter.instruction("sub sp, sp, #48"); // frame: [0]=fd [8]=length [16]=bz_stream ptr [32]=x29 [40]=x30 - emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #32"); // establish the helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the file descriptor for the write loop - emitter.instruction("str x2, [sp, #8]"); // save the payload length as the return value - - // -- load this descriptor's bz_stream handle and seed the input window -- - abi::emit_symbol_address(emitter, "x9", "_bzstream_handles"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // x10 = bz_stream pointer for this descriptor - emitter.instruction("str x10, [sp, #16]"); // save the bz_stream pointer across the calls - emitter.instruction("str x1, [x10, #0]"); // bz_stream.next_in = payload pointer - emitter.instruction("str w2, [x10, #8]"); // bz_stream.avail_in = payload length - - // -- compress loop: drain next_in into the scratch window and write it out -- - emitter.label(&format!("{}_loop", fwrite_label)); - emitter.instruction("ldr x10, [sp, #16]"); // reload the bz_stream pointer - abi::emit_symbol_address(emitter, "x11", "_stream_filter_buf"); - emitter.instruction("str x11, [x10, #24]"); // bz_stream.next_out = scratch window base - emitter.instruction(&format!("mov w12, #{}", FILTER_BUF_SIZE & 0xFFFF)); // low half of the scratch window capacity - emitter.instruction(&format!("movk w12, #{}, lsl #16", FILTER_BUF_SIZE >> 16)); // high half of the scratch window capacity - emitter.instruction("str w12, [x10, #32]"); // bz_stream.avail_out = scratch window capacity - emitter.instruction("mov x0, x10"); // arg 0 = bz_stream pointer - emitter.instruction("mov w1, #0"); // arg 1 = BZ_RUN (0) - emitter.bl_c("BZ2_bzCompress"); // run one compress step over the input window - // -- compute produced = capacity - avail_out and write it to the fd -- - emitter.instruction("ldr x10, [sp, #16]"); // reload the bz_stream pointer after the compress call - emitter.instruction("ldr w12, [x10, #32]"); // reload avail_out left after this compress step - emitter.instruction(&format!("mov w13, #{}", FILTER_BUF_SIZE & 0xFFFF)); // low half of the scratch window capacity - emitter.instruction(&format!("movk w13, #{}, lsl #16", FILTER_BUF_SIZE >> 16)); // high half of the scratch window capacity - emitter.instruction("sub w12, w13, w12"); // produced = capacity - avail_out - emitter.instruction("ldr x0, [sp, #0]"); // fd = the saved file descriptor - abi::emit_symbol_address(emitter, "x1", "_stream_filter_buf"); - emitter.instruction("uxtw x2, w12"); // produced byte count as the write length - emitter.syscall(4); - // -- repeat while input remains OR the output window filled completely -- - emitter.instruction("ldr x10, [sp, #16]"); // reload the bz_stream pointer after the write - emitter.instruction("ldr w14, [x10, #8]"); // reload avail_in still pending - emitter.instruction(&format!("cbnz w14, {}_loop", fwrite_label)); // more input bytes: keep compressing - emitter.instruction("ldr w12, [x10, #32]"); // reload avail_out left after this compress step - emitter.instruction(&format!("cbz w12, {}_loop", fwrite_label)); // window was filled: drain the remainder - // -- done: return the original payload length -- - emitter.instruction("ldr x0, [sp, #8]"); // return value = the saved payload length - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // release the helper frame - emitter.instruction("ret"); // return the bytes-consumed count - - // ================================================================ - // bzip2 compress close helper. - // Input: x0 = fd. Flushes the compress tail and ends the stream. - // ================================================================ - emitter.label(close_label); - emitter.instruction("sub sp, sp, #48"); // frame: [0]=fd [8]=bz_stream [16]=ret code [32]=x29 [40]=x30 - emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #32"); // establish the helper frame pointer - abi::emit_symbol_address(emitter, "x9", "_bzstream_handles"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // x10 = bz_stream pointer for this descriptor - emitter.instruction(&format!("cbz x10, {}_done", close_label)); // nothing to flush when no filter is attached - emitter.instruction("str x0, [sp, #0]"); // save the file descriptor across compress calls - emitter.instruction("str x10, [sp, #8]"); // save the bz_stream pointer - emitter.instruction("str xzr, [x10, #0]"); // bz_stream.next_in = NULL: no further input - emitter.instruction("str wzr, [x10, #8]"); // bz_stream.avail_in = 0: input is exhausted - - // -- flush loop: compress with BZ_FINISH until BZ_STREAM_END -- - emitter.label(&format!("{}_loop", close_label)); - emitter.instruction("ldr x10, [sp, #8]"); // reload the bz_stream pointer - abi::emit_symbol_address(emitter, "x11", "_stream_filter_buf"); - emitter.instruction("str x11, [x10, #24]"); // bz_stream.next_out = scratch window base - emitter.instruction(&format!("mov w12, #{}", FILTER_BUF_SIZE & 0xFFFF)); // low half of the scratch window capacity - emitter.instruction(&format!("movk w12, #{}, lsl #16", FILTER_BUF_SIZE >> 16)); // high half of the scratch window capacity - emitter.instruction("str w12, [x10, #32]"); // bz_stream.avail_out = scratch window capacity - emitter.instruction("mov x0, x10"); // arg 0 = bz_stream pointer - emitter.instruction("mov w1, #2"); // arg 1 = BZ_FINISH (2) - emitter.bl_c("BZ2_bzCompress"); // flush a chunk of the compressed tail - emitter.instruction("str x0, [sp, #16]"); // save the compress return code (4 = BZ_STREAM_END) - // -- compute produced = capacity - avail_out and write it to the fd -- - emitter.instruction("ldr x10, [sp, #8]"); // reload the bz_stream pointer - emitter.instruction("ldr w12, [x10, #32]"); // reload avail_out left after this flush step - emitter.instruction(&format!("mov w13, #{}", FILTER_BUF_SIZE & 0xFFFF)); // low half of the scratch window capacity - emitter.instruction(&format!("movk w13, #{}, lsl #16", FILTER_BUF_SIZE >> 16)); // high half of the scratch window capacity - emitter.instruction("sub w12, w13, w12"); // produced = capacity - avail_out - emitter.instruction("ldr x0, [sp, #0]"); // fd = the saved file descriptor - abi::emit_symbol_address(emitter, "x1", "_stream_filter_buf"); - emitter.instruction("uxtw x2, w12"); // produced byte count as the write length - emitter.syscall(4); - emitter.instruction("ldr x12, [sp, #16]"); // reload the saved compress return code - emitter.instruction("cmp x12, #4"); // did BZ2_bzCompress report BZ_STREAM_END? - emitter.instruction(&format!("b.ne {}_loop", close_label)); // not finished yet: flush another chunk - - // -- end the compress stream and drop the per-descriptor handle -- - emitter.instruction("ldr x0, [sp, #8]"); // arg 0 = bz_stream pointer - emitter.bl_c("BZ2_bzCompressEnd"); // release libbz2's internal compress state - emitter.instruction("ldr x0, [sp, #0]"); // reload the file descriptor - abi::emit_symbol_address(emitter, "x9", "_bzstream_handles"); - emitter.instruction("str xzr, [x9, x0, lsl #3]"); // clear this descriptor's bz_stream handle - emitter.label(&format!("{}_done", close_label)); - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // release the helper frame - emitter.instruction("ret"); // return to the fclose path - - // ================================================================ - // Initialization: allocate and register a bz_stream for this fd. - // ================================================================ - emitter.label(skip_label); - emitter.instruction("sub sp, sp, #16"); // frame: [0]=fd [8]=bz_stream pointer - emitter.instruction("str x0, [sp, #0]"); // save the file descriptor across the calls - emitter.instruction(&format!("mov x0, #{}", BZ_STREAM_SIZE)); // request a bz_stream-sized heap block - emitter.instruction("bl __rt_heap_alloc"); // allocate the bz_stream struct, x0 = payload - emitter.instruction("mov x9, #1"); // heap kind 1 = owned allocation - emitter.instruction("str x9, [x0, #-8]"); // stamp the bz_stream block as owned heap state - emitter.instruction("str x0, [sp, #8]"); // save the bz_stream pointer - - // -- zero all 80 bytes so bzalloc/bzfree/opaque are NULL and counters start clean -- - emitter.instruction("mov x9, #0"); // byte clear index - emitter.label(&format!("{}_zero", skip_label)); - emitter.instruction(&format!("cmp x9, #{}", BZ_STREAM_SIZE)); // cleared the whole bz_stream struct? - emitter.instruction(&format!("b.ge {}_zeroed", skip_label)); // the struct is fully zeroed - emitter.instruction("strb wzr, [x0, x9]"); // zero one bz_stream byte - emitter.instruction("add x9, x9, #1"); // advance the clear index - emitter.instruction(&format!("b {}_zero", skip_label)); // continue zeroing the struct - emitter.label(&format!("{}_zeroed", skip_label)); - - // -- BZ2_bzCompressInit(strm, blockSize100k, verbosity=0, workFactor) -- - emitter.instruction("ldr x0, [sp, #8]"); // arg 0 = bz_stream pointer - emitter.instruction(&format!("mov x1, #{}", block_size)); // arg 1 = blockSize100k ($params 'blocks', default 9 = max) - emitter.instruction("mov x2, #0"); // arg 2 = verbosity = 0 - emitter.instruction(&format!("mov x3, #{}", work_factor)); // arg 3 = workFactor ($params 'work', default 0 = libbz2 default) - emitter.bl_c("BZ2_bzCompressInit"); // initialize the bzip2 compress stream - - // -- register the handle and mark the descriptor's write filter as bzip2 -- - emitter.instruction("ldr x0, [sp, #0]"); // reload the file descriptor - emitter.instruction("ldr x10, [sp, #8]"); // reload the bz_stream pointer - abi::emit_symbol_address(emitter, "x9", "_bzstream_handles"); - emitter.instruction("str x10, [x9, x0, lsl #3]"); // store the bz_stream handle for this descriptor - abi::emit_symbol_address(emitter, "x9", "_stream_write_filters"); - emitter.instruction("mov w11, #10"); // write-filter id 10 = bzip2.compress - emitter.instruction("strb w11, [x9, x0]"); // record the bzip2 write filter for this descriptor - - // -- publish the helper addresses so __rt_fwrite / fclose can call them -- - abi::emit_symbol_address(emitter, "x11", fwrite_label); - abi::emit_symbol_address(emitter, "x9", "_bz2_fwrite_fn"); - emitter.instruction("str x11, [x9]"); // _bz2_fwrite_fn = the compress fwrite helper - abi::emit_symbol_address(emitter, "x11", close_label); - abi::emit_symbol_address(emitter, "x9", "_bz2_close_fn"); - emitter.instruction("str x11, [x9]"); // _bz2_close_fn = the compress close helper - - // -- re-box the descriptor as a resource, matching stream_filter_append -- - emitter.instruction("ldr x0, [sp, #0]"); // reload the file descriptor - emitter.instruction("add sp, sp, #16"); // release the initialization frame - emitter.instruction("mov x1, x0"); // resource payload = the descriptor - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} - -/// Emits the x86_64 compress helpers, then the bz_stream initialization. -/// `work_factor` is the bzip2 workFactor (0..250, 0 = libbz2 default) from the -/// `['work' => N]` `$params` entry. -pub(crate) fn emit_compress_x86_64( - emitter: &mut Emitter, - fwrite_label: &str, - close_label: &str, - skip_label: &str, - block_size: i64, - work_factor: i64, -) { - // -- jump past the helper bodies so normal flow never falls into them -- - emitter.instruction(&format!("jmp {}", skip_label)); // skip over the inline bzip2 helper routines - - // ================================================================ - // bzip2 compress fwrite helper. - // Input: rdi = fd, rsi = payload pointer, rdx = payload length. - // Output: rax = the input payload length (bytes "written"). - // ================================================================ - emitter.label(fwrite_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 32"); // frame: [-8]=fd [-16]=length [-24]=bz_stream - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the file descriptor for the write loop - emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the payload length as the return value - - // -- load this descriptor's bz_stream handle and seed the input window -- - abi::emit_symbol_address(emitter, "r9", "_bzstream_handles"); // bz_stream handle table base - emitter.instruction("mov r10, QWORD PTR [r9 + rdi*8]"); // r10 = bz_stream pointer for this descriptor - emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the bz_stream pointer - emitter.instruction("mov QWORD PTR [r10 + 0], rsi"); // bz_stream.next_in = payload pointer - emitter.instruction("mov DWORD PTR [r10 + 8], edx"); // bz_stream.avail_in = payload length - - // -- compress loop: drain next_in into the scratch window and write it out -- - emitter.label(&format!("{}_loop", fwrite_label)); - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the bz_stream pointer - abi::emit_symbol_address(emitter, "r11", "_stream_filter_buf"); // scratch window base - emitter.instruction("mov QWORD PTR [r10 + 24], r11"); // bz_stream.next_out = scratch window base - emitter.instruction(&format!("mov DWORD PTR [r10 + 32], {}", FILTER_BUF_SIZE)); // bz_stream.avail_out = scratch window capacity - emitter.instruction("mov rdi, r10"); // arg 0 = bz_stream pointer - emitter.instruction("xor esi, esi"); // arg 1 = BZ_RUN (0) - emitter.instruction("call BZ2_bzCompress"); // run one compress step over the input window - // -- compute produced = capacity - avail_out and write it to the fd -- - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the bz_stream pointer - emitter.instruction(&format!("mov eax, {}", FILTER_BUF_SIZE)); // scratch window capacity - emitter.instruction("sub eax, DWORD PTR [r10 + 32]"); // produced = capacity - avail_out - emitter.instruction("mov edx, eax"); // produced byte count as the write length - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // fd = the saved file descriptor - abi::emit_symbol_address(emitter, "rsi", "_stream_filter_buf"); // write buffer = the scratch window base - emitter.instruction("call write"); // write the compressed chunk through libc write() - // -- repeat while input remains OR the output window filled completely -- - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the bz_stream pointer - emitter.instruction("cmp DWORD PTR [r10 + 8], 0"); // any avail_in input bytes still pending? - emitter.instruction(&format!("jne {}_loop", fwrite_label)); // more input bytes: keep compressing - emitter.instruction("cmp DWORD PTR [r10 + 32], 0"); // did the output window fill completely? - emitter.instruction(&format!("je {}_loop", fwrite_label)); // window was filled: drain the remainder - // -- done: return the original payload length -- - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // return value = the saved payload length - emitter.instruction("add rsp, 32"); // release the helper frame - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return the bytes-consumed count - - // ================================================================ - // bzip2 compress close helper. - // Input: rdi = fd. Flushes the compress tail and ends the stream. - // ================================================================ - emitter.label(close_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 32"); // frame: [-8]=fd [-16]=bz_stream [-24]=ret code - abi::emit_symbol_address(emitter, "r9", "_bzstream_handles"); // bz_stream handle table base - emitter.instruction("mov r10, QWORD PTR [r9 + rdi*8]"); // r10 = bz_stream pointer for this descriptor - emitter.instruction("test r10, r10"); // is a compress stream attached to this descriptor? - emitter.instruction(&format!("jz {}_done", close_label)); // nothing to flush when no filter is attached - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the file descriptor across compress calls - emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // save the bz_stream pointer - emitter.instruction("mov QWORD PTR [r10 + 0], 0"); // bz_stream.next_in = NULL: no further input - emitter.instruction("mov DWORD PTR [r10 + 8], 0"); // bz_stream.avail_in = 0: input is exhausted - - // -- flush loop: compress with BZ_FINISH until BZ_STREAM_END -- - emitter.label(&format!("{}_loop", close_label)); - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the bz_stream pointer - abi::emit_symbol_address(emitter, "r11", "_stream_filter_buf"); // scratch window base - emitter.instruction("mov QWORD PTR [r10 + 24], r11"); // bz_stream.next_out = scratch window base - emitter.instruction(&format!("mov DWORD PTR [r10 + 32], {}", FILTER_BUF_SIZE)); // bz_stream.avail_out = scratch window capacity - emitter.instruction("mov rdi, r10"); // arg 0 = bz_stream pointer - emitter.instruction("mov esi, 2"); // arg 1 = BZ_FINISH (2) - emitter.instruction("call BZ2_bzCompress"); // flush a chunk of the compressed tail - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the compress return code (4 = BZ_STREAM_END) - // -- compute produced = capacity - avail_out and write it to the fd -- - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the bz_stream pointer - emitter.instruction(&format!("mov eax, {}", FILTER_BUF_SIZE)); // scratch window capacity - emitter.instruction("sub eax, DWORD PTR [r10 + 32]"); // produced = capacity - avail_out - emitter.instruction("mov edx, eax"); // produced byte count as the write length - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // fd = the preserved file descriptor - abi::emit_symbol_address(emitter, "rsi", "_stream_filter_buf"); // write buffer = the scratch window base - emitter.instruction("call write"); // write the compressed tail chunk through libc write() - emitter.instruction("cmp QWORD PTR [rbp - 24], 4"); // did BZ2_bzCompress report BZ_STREAM_END? - emitter.instruction(&format!("jne {}_loop", close_label)); // not finished yet: flush another chunk - - // -- end the compress stream and drop the per-descriptor handle -- - emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // arg 0 = bz_stream pointer - emitter.instruction("call BZ2_bzCompressEnd"); // release libbz2's internal compress state - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor - abi::emit_symbol_address(emitter, "r9", "_bzstream_handles"); // bz_stream handle table base - emitter.instruction("mov QWORD PTR [r9 + rdi*8], 0"); // clear this descriptor's bz_stream handle - emitter.label(&format!("{}_done", close_label)); - emitter.instruction("add rsp, 32"); // release the helper frame - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return to the fclose path - - // ================================================================ - // Initialization: allocate and register a bz_stream for this fd. - // ================================================================ - emitter.label(skip_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the initialization frame pointer - emitter.instruction("sub rsp, 24"); // frame: [-8]=fd [-16]=bz_stream ptr (24: this inline block enters rsp 16-aligned, push rbp made it 8, so +24≡8 mod 16 realigns to 0 at the libc calls) - emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the file descriptor across the calls - emitter.instruction(&format!("mov rax, {}", BZ_STREAM_SIZE)); // request a bz_stream-sized heap block - emitter.instruction("call __rt_heap_alloc"); // allocate the bz_stream struct, rax = payload - emitter.instruction(&format!( // owned-heap kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 1 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the bz_stream block as owned heap state - emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the bz_stream pointer - - // -- zero all 80 bytes so bzalloc/bzfree/opaque are NULL and counters start clean -- - emitter.instruction("xor r9, r9"); // byte clear index - emitter.label(&format!("{}_zero", skip_label)); - emitter.instruction(&format!("cmp r9, {}", BZ_STREAM_SIZE)); // cleared the whole bz_stream struct? - emitter.instruction(&format!("jge {}_zeroed", skip_label)); // the struct is fully zeroed - emitter.instruction("mov BYTE PTR [rax + r9], 0"); // zero one bz_stream byte - emitter.instruction("inc r9"); // advance the clear index - emitter.instruction(&format!("jmp {}_zero", skip_label)); // continue zeroing the struct - emitter.label(&format!("{}_zeroed", skip_label)); - - // -- BZ2_bzCompressInit(strm, blockSize100k, verbosity=0, workFactor) -- - emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // arg 0 = bz_stream pointer - emitter.instruction(&format!("mov esi, {}", block_size)); // arg 1 = blockSize100k ($params 'blocks', default 9 = max) - emitter.instruction("xor edx, edx"); // arg 2 = verbosity = 0 - emitter.instruction(&format!("mov ecx, {}", work_factor)); // arg 3 = workFactor ($params 'work', default 0 = libbz2 default) - emitter.instruction("call BZ2_bzCompressInit"); // initialize the bzip2 compress stream - - // -- register the handle and mark the descriptor's write filter as bzip2 -- - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the bz_stream pointer - abi::emit_symbol_address(emitter, "r9", "_bzstream_handles"); // bz_stream handle table base - emitter.instruction("mov QWORD PTR [r9 + rdi*8], r10"); // store the bz_stream handle for this descriptor - abi::emit_symbol_address(emitter, "r9", "_stream_write_filters"); // write-filter table base - emitter.instruction("mov BYTE PTR [r9 + rdi], 10"); // write-filter id 10 = bzip2.compress - - // -- publish the helper addresses so __rt_fwrite / fclose can call them -- - emitter.instruction(&format!("lea r10, [rip + {}]", fwrite_label)); // address of the compress fwrite helper - abi::emit_symbol_address(emitter, "r9", "_bz2_fwrite_fn"); // _bz2_fwrite_fn slot - emitter.instruction("mov QWORD PTR [r9], r10"); // _bz2_fwrite_fn = the compress fwrite helper - emitter.instruction(&format!("lea r10, [rip + {}]", close_label)); // address of the compress close helper - abi::emit_symbol_address(emitter, "r9", "_bz2_close_fn"); // _bz2_close_fn slot - emitter.instruction("mov QWORD PTR [r9], r10"); // _bz2_close_fn = the compress close helper - - // -- re-box the descriptor as a resource, matching stream_filter_append -- - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // resource payload = the descriptor - emitter.instruction("add rsp, 24"); // release the initialization frame (matches the aligned sub rsp, 24) - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} diff --git a/src/codegen/builtins/io/stream_filter_iconv.rs b/src/codegen/builtins/io/stream_filter_iconv.rs deleted file mode 100644 index 9219d78e94..0000000000 --- a/src/codegen/builtins/io/stream_filter_iconv.rs +++ /dev/null @@ -1,406 +0,0 @@ -//! Purpose: -//! Emits the `convert.iconv./` charset-conversion stream filter for -//! `stream_filter_append` / `stream_filter_prepend`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::stream_filter::emit_attach()` when the -//! filter-name literal begins with `convert.iconv.`. -//! -//! Key details: -//! - Like the `zlib.inflate` read filter, the descriptor itself is transformed -//! at attach time: the whole stream is slurped, transcoded once through libc -//! `iconv`, written to an anonymous temp file, and `dup2`'d onto the original -//! descriptor. Every later `fread`/`fseek`/`feof` then works unchanged — no -//! per-fd filter state and no `__rt_fread` change are needed. -//! - `iconv_open`/`iconv`/`iconv_close` live in libc (glibc, macOS libSystem, -//! musl), so no extra `-l` and no function-pointer indirection are needed. -//! - The `` and `` charset names are parsed from the filter name at -//! compile time and emitted as null-terminated C strings. -//! - v1 limitations: the conversion direction is applied to the descriptor (so -//! it behaves as a read transform); input is capped at the 64 KiB -//! `_stream_filter_buf` scratch and output at 4x that (min 64 KiB). A bad -//! charset pair (`iconv_open` fails) leaves the stream unconverted. musl's -//! iconv supports a limited charset set (UTF-8/UTF-16/UTF-32 are fine). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Capacity of the shared `_stream_filter_buf` scratch, reused as the input slurp buffer. -const FILTER_BUF_SIZE: i64 = 65536; -/// x86_64 owned-heap kind word: the elephc heap marker in the high 32 bits. -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits the `convert.iconv./` filter attachment. `spec` is the -/// portion after `convert.iconv.` — e.g. `UTF-8/UTF-16LE`. Returns the stream -/// re-boxed as a resource, matching `stream_filter_append`'s contract; a -/// malformed spec (no `/`) attaches no conversion. -pub fn emit( - spec: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_filter_append(convert.iconv.*)"); - emit_stream_fd_arg("stream_filter_append", &args[0], emitter, ctx, data); - - // Parse "/"; a missing slash means no usable charset pair. - let charsets = spec.split_once('/'); - let (from, to) = match charsets { - Some((f, t)) if !f.is_empty() && !t.is_empty() => (f, t), - _ => { - // No conversion: just re-box the descriptor as a resource. - return Some(rebox_fd_as_resource(emitter)); - } - }; - // Emit null-terminated C strings for iconv_open(tocode, fromcode). - let (from_sym, _) = data.add_string(format!("{}\0", from).as_bytes()); - let (to_sym, _) = data.add_string(format!("{}\0", to).as_bytes()); - - // The descriptor is in the int result register. Evaluate the read/write mode - // (args[2], default STREAM_FILTER_ALL = 3) and dispatch at runtime: a - // WRITE-only filter (mode == 2) installs a streaming per-fwrite transcoder; - // READ and ALL (the common no-arg case) keep the attach-time read transform, - // preserving all existing behavior. - let write_label = ctx.next_label("iconv_mode_write"); - let after_label = ctx.next_label("iconv_mode_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the descriptor across the mode evaluation - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); // evaluate the read/write mode into x0 - } else { - emitter.instruction("mov x0, #3"); // default mode = STREAM_FILTER_ALL - } - emitter.instruction("mov x9, x0"); // hold the mode while the descriptor is restored - emitter.instruction("ldr x0, [sp], #16"); // restore the descriptor into the result register - emitter.instruction("cmp x9, #2"); // is this a STREAM_FILTER_WRITE-only filter? - emitter.instruction(&format!("b.eq {}", write_label)); // install the streaming write transcoder - emit_read_arm64(emitter, &from_sym, &to_sym, |prefix| ctx.next_label(prefix)); // READ / ALL: attach-time read transform - emitter.instruction(&format!("b {}", after_label)); // skip the write-attach path - emitter.label(&write_label); - super::stream_filter_iconv_write::emit_iconv_write_attach(emitter, ctx, &from_sym, &to_sym); - emitter.label(&after_label); - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the descriptor across the mode evaluation - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); // evaluate the read/write mode into rax - } else { - emitter.instruction("mov eax, 3"); // default mode = STREAM_FILTER_ALL - } - emitter.instruction("mov r9, rax"); // hold the mode while the descriptor is restored - abi::emit_pop_reg(emitter, "rax"); // restore the descriptor into the result register - emitter.instruction("cmp r9, 2"); // is this a STREAM_FILTER_WRITE-only filter? - emitter.instruction(&format!("je {}", write_label)); // install the streaming write transcoder - emit_read_x86_64(emitter, &from_sym, &to_sym, |prefix| ctx.next_label(prefix)); // READ / ALL: attach-time read transform - emitter.instruction(&format!("jmp {}", after_label)); // skip the write-attach path - emitter.label(&write_label); - super::stream_filter_iconv_write::emit_iconv_write_attach(emitter, ctx, &from_sym, &to_sym); - emitter.label(&after_label); - } - } - Some(PhpType::Mixed) -} - -/// Re-boxes the descriptor (currently in the int result register) as a resource -/// Mixed cell, the value `stream_filter_append` returns. Returns `PhpType::Mixed`. -fn rebox_fd_as_resource(emitter: &mut Emitter) -> PhpType { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // resource payload = the descriptor - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // resource payload = the descriptor - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource - } - } - PhpType::Mixed -} - -/// ARM64: 160-byte scratch frame. -/// [0]=fd [8]=input len [16]=out buf [24]=out cap [32]=temp fd [40]=iconv cd -/// [48]=iconv inbuf [56]=iconv inbytesleft [64]=iconv outbuf [72]=iconv outbytesleft -/// [80]=converted len [88]=write offset; x29/x30 at [144]. -pub(crate) fn emit_read_arm64( - emitter: &mut Emitter, - from_sym: &str, - to_sym: &str, - mut next_label: F, -) -where - F: FnMut(&str) -> String, -{ - let slurp = next_label("iconv_slurp"); - let slurp_done = next_label("iconv_slurped"); - let sized = next_label("iconv_sized"); - let skip = next_label("iconv_skip"); - let write = next_label("iconv_write"); - let write_done = next_label("iconv_written"); - - emitter.instruction("sub sp, sp, #160"); // iconv scratch frame - emitter.instruction("stp x29, x30, [sp, #144]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #144"); // establish the helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the source file descriptor - - // -- slurp every byte from the descriptor into the scratch buffer -- - emitter.instruction("str xzr, [sp, #8]"); // input length = 0 - emitter.label(&slurp); - emitter.instruction("ldr x0, [sp, #0]"); // fd to read from - abi::emit_symbol_address(emitter, "x1", "_stream_filter_buf"); - emitter.instruction("ldr x9, [sp, #8]"); // current input length - emitter.instruction("add x1, x1, x9"); // write pointer = scratch base + length - emitter.instruction(&format!("mov x2, #{}", FILTER_BUF_SIZE)); // scratch capacity - emitter.instruction("sub x2, x2, x9"); // remaining scratch capacity - emitter.syscall(3); - emitter.instruction("cmp x0, #0"); // did the read hit EOF or fail? - emitter.instruction(&format!("b.le {}", slurp_done)); // stop slurping at EOF or on error - emitter.instruction("ldr x9, [sp, #8]"); // reload the input length - emitter.instruction("add x9, x9, x0"); // advance by the bytes just read - emitter.instruction("str x9, [sp, #8]"); // store the updated input length - emitter.instruction(&format!("mov x10, #{}", FILTER_BUF_SIZE)); // scratch capacity - emitter.instruction("cmp x9, x10"); // is the scratch buffer full? - emitter.instruction(&format!("b.lt {}", slurp)); // room remains: keep slurping - emitter.label(&slurp_done); - - // -- size and allocate the output buffer (4x input, min 64 KiB) -- - emitter.instruction("ldr x9, [sp, #8]"); // input length - emitter.instruction("lsl x9, x9, #2"); // budget 4x the input size - emitter.instruction(&format!("mov x10, #{}", FILTER_BUF_SIZE)); // minimum output buffer size - emitter.instruction("cmp x9, x10"); // is the 4x budget larger? - emitter.instruction(&format!("b.gt {}", sized)); // keep the larger budget - emitter.instruction(&format!("mov x9, #{}", FILTER_BUF_SIZE)); // otherwise use the minimum size - emitter.label(&sized); - emitter.instruction("str x9, [sp, #24]"); // save the output buffer capacity - emitter.instruction("mov x0, x9"); // buffer size into the allocator argument - emitter.instruction("bl __rt_heap_alloc"); // allocate the converted-data buffer - emitter.instruction("mov x9, #1"); // heap kind 1 = persisted elephc string - emitter.instruction("str x9, [x0, #-8]"); // stamp the buffer header - emitter.instruction("str x0, [sp, #16]"); // save the output buffer pointer - - // -- iconv_open(tocode, fromcode): a -1 result leaves the stream unconverted -- - abi::emit_symbol_address(emitter, "x0", to_sym); - abi::emit_symbol_address(emitter, "x1", from_sym); - emitter.bl_c("iconv_open"); // open the charset conversion descriptor - emitter.instruction("cmn x0, #1"); // is the descriptor (iconv_t)-1? - emitter.instruction(&format!("b.eq {}", skip)); // iconv_open failed → skip the conversion - emitter.instruction("str x0, [sp, #40]"); // save the iconv conversion descriptor - - // -- set up the iconv in/out cursors and remaining-byte counts -- - abi::emit_symbol_address(emitter, "x9", "_stream_filter_buf"); - emitter.instruction("str x9, [sp, #48]"); // iconv inbuf = scratch base - emitter.instruction("ldr x9, [sp, #8]"); // input length - emitter.instruction("str x9, [sp, #56]"); // iconv inbytesleft = input length - emitter.instruction("ldr x9, [sp, #16]"); // output buffer pointer - emitter.instruction("str x9, [sp, #64]"); // iconv outbuf = output buffer - emitter.instruction("ldr x9, [sp, #24]"); // output buffer capacity - emitter.instruction("str x9, [sp, #72]"); // iconv outbytesleft = output capacity - - // -- iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft) -- - emitter.instruction("ldr x0, [sp, #40]"); // conversion descriptor - emitter.instruction("add x1, sp, #48"); // &inbuf - emitter.instruction("add x2, sp, #56"); // &inbytesleft - emitter.instruction("add x3, sp, #64"); // &outbuf - emitter.instruction("add x4, sp, #72"); // &outbytesleft - emitter.bl_c("iconv"); // transcode the whole input in one pass - emitter.instruction("ldr x9, [sp, #24]"); // output capacity - emitter.instruction("ldr x10, [sp, #72]"); // bytes still free in the output buffer - emitter.instruction("sub x9, x9, x10"); // converted length = capacity - free - emitter.instruction("str x9, [sp, #80]"); // save the converted length - emitter.instruction("ldr x0, [sp, #40]"); // conversion descriptor - emitter.bl_c("iconv_close"); // release the iconv descriptor - - // -- back the descriptor with an anonymous temp file of the converted bytes -- - emitter.instruction("bl __rt_tmpfile"); // create an unlinked temp file, x0 = fd - emitter.instruction("str x0, [sp, #32]"); // save the temp-file descriptor - - // -- write loop: copy every converted byte into the temp file -- - emitter.instruction("str xzr, [sp, #88]"); // write offset = 0 - emitter.label(&write); - emitter.instruction("ldr x10, [sp, #80]"); // total converted length - emitter.instruction("ldr x9, [sp, #88]"); // current write offset - emitter.instruction("cmp x9, x10"); // copied every converted byte? - emitter.instruction(&format!("b.ge {}", write_done)); // the whole payload is written - emitter.instruction("ldr x0, [sp, #32]"); // temp-file descriptor - emitter.instruction("ldr x1, [sp, #16]"); // output buffer pointer - emitter.instruction("add x1, x1, x9"); // write pointer = buffer + offset - emitter.instruction("sub x2, x10, x9"); // remaining bytes to write - emitter.syscall(4); - emitter.instruction("cmp x0, #0"); // did the write make progress? - emitter.instruction(&format!("b.le {}", write_done)); // stop on a write error - emitter.instruction("ldr x9, [sp, #88]"); // reload the write offset - emitter.instruction("add x9, x9, x0"); // advance by the bytes just written - emitter.instruction("str x9, [sp, #88]"); // store the updated write offset - emitter.instruction(&format!("b {}", write)); // continue writing the payload - emitter.label(&write_done); - - // -- lseek(temp, 0, SEEK_SET): rewind so reads start at the converted bytes -- - emitter.instruction("ldr x0, [sp, #32]"); // temp-file descriptor - emitter.instruction("mov x1, #0"); // offset = 0 - emitter.instruction("mov x2, #0"); // whence = SEEK_SET - emitter.syscall(199); - - // -- dup2(temp, fd): the descriptor now serves the converted bytes -- - emitter.instruction("ldr x0, [sp, #32]"); // oldfd = temp file - emitter.instruction("ldr x1, [sp, #0]"); // newfd = the stream descriptor - emitter.bl_c("dup2"); // redirect the descriptor onto the temp file - - // -- close the now-redundant temp-file descriptor -- - emitter.instruction("ldr x0, [sp, #32]"); // the temp-file descriptor - emitter.syscall(6); - - emitter.label(&skip); - // -- re-box the descriptor as a resource, matching stream_filter_append -- - emitter.instruction("ldr x0, [sp, #0]"); // reload the stream descriptor - emitter.instruction("ldp x29, x30, [sp, #144]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #160"); // release the scratch frame - emitter.instruction("mov x1, x0"); // resource payload = the descriptor - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} - -/// x86_64: same 160-byte sp-relative scratch layout as the AArch64 path. -pub(crate) fn emit_read_x86_64( - emitter: &mut Emitter, - from_sym: &str, - to_sym: &str, - mut next_label: F, -) -where - F: FnMut(&str) -> String, -{ - let slurp = next_label("iconv_slurp"); - let slurp_done = next_label("iconv_slurped"); - let sized = next_label("iconv_sized"); - let skip = next_label("iconv_skip"); - let write = next_label("iconv_write"); - let write_done = next_label("iconv_written"); - - emitter.instruction("sub rsp, 160"); // iconv scratch frame - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the source file descriptor - - // -- slurp every byte from the descriptor into the scratch buffer -- - emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // input length = 0 - emitter.label(&slurp); - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // fd to read from - abi::emit_symbol_address(emitter, "rsi", "_stream_filter_buf"); // scratch base address - emitter.instruction("add rsi, QWORD PTR [rsp + 8]"); // write pointer = scratch base + length - emitter.instruction(&format!("mov rdx, {}", FILTER_BUF_SIZE)); // scratch capacity - emitter.instruction("sub rdx, QWORD PTR [rsp + 8]"); // remaining scratch capacity - emitter.instruction("call read"); // read bytes through libc read() - emitter.instruction("cmp rax, 0"); // did the read hit EOF or fail? - emitter.instruction(&format!("jle {}", slurp_done)); // stop slurping at EOF or on error - emitter.instruction("mov r9, QWORD PTR [rsp + 8]"); // reload the input length - emitter.instruction("add r9, rax"); // advance by the bytes just read - emitter.instruction("mov QWORD PTR [rsp + 8], r9"); // store the updated input length - emitter.instruction(&format!("cmp r9, {}", FILTER_BUF_SIZE)); // is the scratch buffer full? - emitter.instruction(&format!("jl {}", slurp)); // room remains: keep slurping - emitter.label(&slurp_done); - - // -- size and allocate the output buffer (4x input, min 64 KiB) -- - emitter.instruction("mov r9, QWORD PTR [rsp + 8]"); // input length - emitter.instruction("shl r9, 2"); // budget 4x the input size - emitter.instruction(&format!("cmp r9, {}", FILTER_BUF_SIZE)); // is the 4x budget above the minimum? - emitter.instruction(&format!("jge {}", sized)); // keep the larger budget - emitter.instruction(&format!("mov r9, {}", FILTER_BUF_SIZE)); // otherwise use the minimum size - emitter.label(&sized); - emitter.instruction("mov QWORD PTR [rsp + 24], r9"); // save the output buffer capacity - emitter.instruction("mov rax, r9"); // buffer size into the allocator argument - emitter.instruction("call __rt_heap_alloc"); // allocate the converted-data buffer - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 1)); // owned-string heap-kind word - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the buffer header - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // save the output buffer pointer - - // -- iconv_open(tocode, fromcode): a -1 result leaves the stream unconverted -- - abi::emit_symbol_address(emitter, "rdi", &to_sym); // arg 0 = tocode - abi::emit_symbol_address(emitter, "rsi", &from_sym); // arg 1 = fromcode - emitter.instruction("call iconv_open"); // open the charset conversion descriptor - emitter.instruction("cmp rax, -1"); // is the descriptor (iconv_t)-1? - emitter.instruction(&format!("je {}", skip)); // iconv_open failed → skip the conversion - emitter.instruction("mov QWORD PTR [rsp + 40], rax"); // save the iconv conversion descriptor - - // -- set up the iconv in/out cursors and remaining-byte counts -- - abi::emit_symbol_address(emitter, "r9", "_stream_filter_buf"); // scratch base address - emitter.instruction("mov QWORD PTR [rsp + 48], r9"); // iconv inbuf = scratch base - emitter.instruction("mov r9, QWORD PTR [rsp + 8]"); // input length - emitter.instruction("mov QWORD PTR [rsp + 56], r9"); // iconv inbytesleft = input length - emitter.instruction("mov r9, QWORD PTR [rsp + 16]"); // output buffer pointer - emitter.instruction("mov QWORD PTR [rsp + 64], r9"); // iconv outbuf = output buffer - emitter.instruction("mov r9, QWORD PTR [rsp + 24]"); // output buffer capacity - emitter.instruction("mov QWORD PTR [rsp + 72], r9"); // iconv outbytesleft = output capacity - - // -- iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft) -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 40]"); // conversion descriptor - emitter.instruction("lea rsi, [rsp + 48]"); // &inbuf - emitter.instruction("lea rdx, [rsp + 56]"); // &inbytesleft - emitter.instruction("lea rcx, [rsp + 64]"); // &outbuf - emitter.instruction("lea r8, [rsp + 72]"); // &outbytesleft - emitter.instruction("call iconv"); // transcode the whole input in one pass - emitter.instruction("mov r9, QWORD PTR [rsp + 24]"); // output capacity - emitter.instruction("sub r9, QWORD PTR [rsp + 72]"); // converted length = capacity - free - emitter.instruction("mov QWORD PTR [rsp + 80], r9"); // save the converted length - emitter.instruction("mov rdi, QWORD PTR [rsp + 40]"); // conversion descriptor - emitter.instruction("call iconv_close"); // release the iconv descriptor - - // -- back the descriptor with an anonymous temp file of the converted bytes -- - emitter.instruction("call __rt_tmpfile"); // create an unlinked temp file, rax = fd - emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // save the temp-file descriptor - - // -- write loop: copy every converted byte into the temp file -- - emitter.instruction("mov QWORD PTR [rsp + 88], 0"); // write offset = 0 - emitter.label(&write); - emitter.instruction("mov r10, QWORD PTR [rsp + 80]"); // total converted length - emitter.instruction("mov r9, QWORD PTR [rsp + 88]"); // current write offset - emitter.instruction("cmp r9, r10"); // copied every converted byte? - emitter.instruction(&format!("jge {}", write_done)); // the whole payload is written - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // temp-file descriptor - emitter.instruction("mov rsi, QWORD PTR [rsp + 16]"); // output buffer pointer - emitter.instruction("add rsi, r9"); // write pointer = buffer + offset - emitter.instruction("mov rdx, r10"); // total converted length - emitter.instruction("sub rdx, r9"); // remaining bytes to write - emitter.instruction("call write"); // write the converted bytes via libc write() - emitter.instruction("cmp rax, 0"); // did the write make progress? - emitter.instruction(&format!("jle {}", write_done)); // stop on a write error - emitter.instruction("mov r9, QWORD PTR [rsp + 88]"); // reload the write offset - emitter.instruction("add r9, rax"); // advance by the bytes just written - emitter.instruction("mov QWORD PTR [rsp + 88], r9"); // store the updated write offset - emitter.instruction(&format!("jmp {}", write)); // continue writing the payload - emitter.label(&write_done); - - // -- lseek(temp, 0, SEEK_SET): rewind so reads start at the converted bytes -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // temp-file descriptor - emitter.instruction("xor esi, esi"); // offset = 0 - emitter.instruction("xor edx, edx"); // whence = SEEK_SET - emitter.instruction("call lseek"); // rewind the temp file - - // -- dup2(temp, fd): the descriptor now serves the converted bytes -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // oldfd = temp file - emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // newfd = the stream descriptor - emitter.instruction("call dup2"); // redirect the descriptor onto the temp file - - // -- close the now-redundant temp-file descriptor -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // the temp-file descriptor - emitter.instruction("call close"); // release the redundant descriptor - - emitter.label(&skip); - // -- re-box the descriptor as a resource, matching stream_filter_append -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the stream descriptor - emitter.instruction("add rsp, 160"); // release the scratch frame - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} diff --git a/src/codegen/builtins/io/stream_filter_iconv_write.rs b/src/codegen/builtins/io/stream_filter_iconv_write.rs deleted file mode 100644 index 68919d1ec6..0000000000 --- a/src/codegen/builtins/io/stream_filter_iconv_write.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Purpose: -//! Emits the WRITE-direction `convert.iconv./` stream filter: a -//! streaming per-`fwrite` transcoder installed when `stream_filter_append` is -//! called with `STREAM_FILTER_WRITE`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::stream_filter_iconv::emit()` when the -//! runtime mode is `STREAM_FILTER_WRITE` (2). READ / ALL keep the attach-time -//! read transform in `stream_filter_iconv`. -//! -//! Key details: -//! - Mirrors the `zlib.deflate` / `bzip2.compress` write-filter mechanism: a -//! per-fd handle (`_iconv_handles[fd]` holds the `iconv_t`), write-filter id -//! 12 in `_stream_write_filters[fd]`, and two per-program USER-asm helpers -//! whose addresses are published into `_iconv_fwrite_fn` / `_iconv_close_fn`. -//! The shared runtime (`__rt_fwrite`, `fclose`) reaches libc `iconv` ONLY -//! through those pointers, so it never names an iconv symbol — keeping the -//! macOS `-liconv` dependency to programs that actually attach the filter. -//! - The fwrite helper loops `iconv(cd, &in, &inleft, &out, &outleft)` into the -//! shared `_stream_grow_scratch` (64 KiB) window, writing each produced chunk -//! to the fd, until the input is drained. The `iconv_t` is persistent, so -//! shift state carries across writes. v1: a write that ends mid-multibyte -//! sequence (no progress, output empty) stops to avoid a spin — acceptable -//! for whole-string writes of complete text. -//! - The close helper `iconv_close`s the descriptor and clears the handle. -//! - The init block is spliced INLINE (entered with rsp 16-aligned), so on -//! x86_64 it reserves `sub rsp, 24` (push rbp + 24 ≡ 0 mod 16) to keep rsp -//! 16-aligned at the libc calls; the call-entered helpers use `sub rsp, 64`. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; - -/// Capacity of the shared `_stream_grow_scratch` window used as the iconv output buffer. -const ICONV_SCRATCH: i64 = 65536; - -/// Emits the iconv WRITE-filter attachment through the legacy codegen context. -pub fn emit_iconv_write_attach( - emitter: &mut Emitter, - ctx: &mut Context, - from_sym: &str, - to_sym: &str, -) { - emit_iconv_write_attach_with_labels(emitter, from_sym, to_sym, |prefix| ctx.next_label(prefix)); -} - -/// Emits the iconv WRITE-filter attachment. The descriptor is in the int result -/// register at entry; on return the stream is re-boxed as a resource Mixed cell. -pub(crate) fn emit_iconv_write_attach_with_labels( - emitter: &mut Emitter, - from_sym: &str, - to_sym: &str, - mut next_label: F, -) -where - F: FnMut(&str) -> String, -{ - let fwrite_label = next_label("iconv_w_fwrite"); - let close_label = next_label("iconv_w_close"); - let skip_label = next_label("iconv_w_skip_helpers"); - match emitter.target.arch { - Arch::AArch64 => emit_arm64( - emitter, - from_sym, - to_sym, - &fwrite_label, - &close_label, - &skip_label, - &mut next_label, - ), - Arch::X86_64 => emit_x86_64( - emitter, - from_sym, - to_sym, - &fwrite_label, - &close_label, - &skip_label, - &mut next_label, - ), - } -} - -/// ARM64 helpers + inline init. -fn emit_arm64( - emitter: &mut Emitter, - from_sym: &str, - to_sym: &str, - fwrite_label: &str, - close_label: &str, - skip_label: &str, - next_label: &mut F, -) -where - F: FnMut(&str) -> String, -{ - let loop_label = next_label("iconv_w_loop"); - let after_write = next_label("iconv_w_after_write"); - let done_label = next_label("iconv_w_done"); - let skip_store = next_label("iconv_w_skip_store"); - - emitter.instruction(&format!("b {}", skip_label)); // skip over the inline iconv helper routines - - // ================================================================ - // iconv write helper. Input: x0 = fd, x1 = payload ptr, x2 = payload len. - // Output: x0 = the input payload length (bytes "written"). - // Frame: [0]=fd [8]=retlen [16]=inbuf [24]=inleft [32]=outbuf [40]=outleft. - // ================================================================ - emitter.label(fwrite_label); - emitter.instruction("sub sp, sp, #64"); // helper frame - emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #48"); // establish the helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the file descriptor - emitter.instruction("str x2, [sp, #8]"); // save the payload length as the return value - emitter.instruction("str x1, [sp, #16]"); // iconv inbuf = payload pointer - emitter.instruction("str x2, [sp, #24]"); // iconv inbytesleft = payload length - - emitter.label(&loop_label); - abi::emit_symbol_address(emitter, "x9", "_stream_grow_scratch"); - emitter.instruction("str x9, [sp, #32]"); // iconv outbuf = scratch window base - emitter.instruction(&format!("mov w10, #{}", ICONV_SCRATCH & 0xFFFF)); // low half of the scratch capacity - emitter.instruction(&format!("movk w10, #{}, lsl #16", ICONV_SCRATCH >> 16)); // high half of the scratch capacity - emitter.instruction("str x10, [sp, #40]"); // iconv outbytesleft = scratch capacity - emitter.instruction("ldr x0, [sp, #0]"); // reload the descriptor to index the handle table - abi::emit_symbol_address(emitter, "x9", "_iconv_handles"); - emitter.instruction("ldr x0, [x9, x0, lsl #3]"); // arg 0 = the iconv_t for this descriptor - emitter.instruction("add x1, sp, #16"); // arg 1 = &inbuf - emitter.instruction("add x2, sp, #24"); // arg 2 = &inbytesleft - emitter.instruction("add x3, sp, #32"); // arg 3 = &outbuf - emitter.instruction("add x4, sp, #40"); // arg 4 = &outbytesleft - emitter.bl_c("iconv"); // transcode a chunk of the payload - // produced = scratch capacity - remaining outbytesleft - emitter.instruction(&format!("mov w10, #{}", ICONV_SCRATCH & 0xFFFF)); // low half of the scratch capacity - emitter.instruction(&format!("movk w10, #{}, lsl #16", ICONV_SCRATCH >> 16)); // high half of the scratch capacity - emitter.instruction("ldr x11, [sp, #40]"); // remaining outbytesleft after iconv - emitter.instruction("sub x12, x10, x11"); // produced = capacity - remaining - emitter.instruction(&format!("cbz x12, {}", after_write)); // nothing produced: skip the write - emitter.instruction("ldr x0, [sp, #0]"); // fd = the saved descriptor - abi::emit_symbol_address(emitter, "x1", "_stream_grow_scratch"); - emitter.instruction("mov x2, x12"); // produced byte count as the write length - emitter.syscall(4); - emitter.label(&after_write); - emitter.instruction("ldr x11, [sp, #24]"); // remaining inbytesleft - emitter.instruction(&format!("cbz x11, {}", done_label)); // all input consumed: done - emitter.instruction(&format!("cbz x12, {}", done_label)); // no progress (incomplete/invalid): stop to avoid a spin - emitter.instruction(&format!("b {}", loop_label)); // output filled: transcode the remainder - emitter.label(&done_label); - emitter.instruction("ldr x0, [sp, #8]"); // return value = the saved payload length - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #64"); // release the helper frame - emitter.instruction("ret"); // return the bytes-consumed count - - // ================================================================ - // iconv close helper. Input: x0 = fd. Closes the iconv descriptor. - // ================================================================ - emitter.label(close_label); - emitter.instruction("sub sp, sp, #16"); // helper frame: [0]=fd [8]=x30 - emitter.instruction("str x30, [sp, #8]"); // save the return address - abi::emit_symbol_address(emitter, "x9", "_iconv_handles"); - emitter.instruction("ldr x1, [x9, x0, lsl #3]"); // load this descriptor's iconv_t - emitter.instruction(&format!("cbz x1, {}_done", close_label)); // nothing attached: nothing to close - emitter.instruction("str x0, [sp, #0]"); // save the descriptor across iconv_close - emitter.instruction("mov x0, x1"); // arg 0 = the iconv_t - emitter.bl_c("iconv_close"); // release the iconv descriptor - emitter.instruction("ldr x0, [sp, #0]"); // reload the descriptor - abi::emit_symbol_address(emitter, "x9", "_iconv_handles"); - emitter.instruction("str xzr, [x9, x0, lsl #3]"); // clear this descriptor's iconv handle - emitter.label(&format!("{}_done", close_label)); - emitter.instruction("ldr x30, [sp, #8]"); // restore the return address - emitter.instruction("add sp, sp, #16"); // release the helper frame - emitter.instruction("ret"); // return to the fclose path - - // ================================================================ - // Initialization (inline): iconv_open + register the handle for this fd. - // ================================================================ - emitter.label(skip_label); - emitter.instruction("sub sp, sp, #16"); // frame: [0]=fd [8]=x30 - emitter.instruction("str x30, [sp, #8]"); // save the return address across iconv_open - emitter.instruction("str x0, [sp, #0]"); // save the file descriptor - abi::emit_symbol_address(emitter, "x0", to_sym); // arg 0 = tocode - abi::emit_symbol_address(emitter, "x1", from_sym); // arg 1 = fromcode - emitter.bl_c("iconv_open"); // open the charset conversion descriptor - emitter.instruction("cmn x0, #1"); // is the descriptor (iconv_t)-1? - emitter.instruction(&format!("b.eq {}", skip_store)); // iconv_open failed → attach no filter - emitter.instruction("ldr x1, [sp, #0]"); // reload the file descriptor - abi::emit_symbol_address(emitter, "x9", "_iconv_handles"); - emitter.instruction("str x0, [x9, x1, lsl #3]"); // store the iconv_t for this descriptor - abi::emit_symbol_address(emitter, "x9", "_stream_write_filters"); - emitter.instruction("mov w10, #12"); // write-filter id 12 = convert.iconv write - emitter.instruction("strb w10, [x9, x1]"); // record the iconv write filter for this descriptor - abi::emit_symbol_address(emitter, "x10", fwrite_label); - abi::emit_symbol_address(emitter, "x9", "_iconv_fwrite_fn"); - emitter.instruction("str x10, [x9]"); // _iconv_fwrite_fn = the iconv write helper - abi::emit_symbol_address(emitter, "x10", close_label); - abi::emit_symbol_address(emitter, "x9", "_iconv_close_fn"); - emitter.instruction("str x10, [x9]"); // _iconv_close_fn = the iconv close helper - emitter.label(&skip_store); - emitter.instruction("ldr x0, [sp, #0]"); // reload the file descriptor - emitter.instruction("ldr x30, [sp, #8]"); // restore the return address - emitter.instruction("add sp, sp, #16"); // release the initialization frame - emitter.instruction("mov x1, x0"); // resource payload = the descriptor - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} - -/// x86_64 helpers + inline init. -fn emit_x86_64( - emitter: &mut Emitter, - from_sym: &str, - to_sym: &str, - fwrite_label: &str, - close_label: &str, - skip_label: &str, - next_label: &mut F, -) -where - F: FnMut(&str) -> String, -{ - let loop_label = next_label("iconv_w_loop"); - let after_write = next_label("iconv_w_after_write"); - let done_label = next_label("iconv_w_done"); - let skip_store = next_label("iconv_w_skip_store"); - - emitter.instruction(&format!("jmp {}", skip_label)); // skip over the inline iconv helper routines - - // ================================================================ - // iconv write helper. Input: rdi = fd, rsi = payload ptr, rdx = payload len. - // Output: rax = the input payload length (bytes "written"). - // Frame: [-8]=fd [-16]=retlen [-24]=inbuf [-32]=inleft [-40]=outbuf [-48]=outleft. - // ================================================================ - emitter.label(fwrite_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 64"); // helper frame (0 mod 16: aligned at the libc calls) - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the file descriptor - emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the payload length as the return value - emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // iconv inbuf = payload pointer - emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // iconv inbytesleft = payload length - - emitter.label(&loop_label); - abi::emit_symbol_address(emitter, "r9", "_stream_grow_scratch"); // scratch window base - emitter.instruction("mov QWORD PTR [rbp - 40], r9"); // iconv outbuf = scratch window base - emitter.instruction(&format!("mov QWORD PTR [rbp - 48], {}", ICONV_SCRATCH)); // iconv outbytesleft = scratch capacity - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the descriptor to index the handle table - abi::emit_symbol_address(emitter, "r9", "_iconv_handles"); // iconv handle table base - emitter.instruction("mov rdi, QWORD PTR [r9 + rdi*8]"); // arg 0 = the iconv_t for this descriptor - emitter.instruction("lea rsi, [rbp - 24]"); // arg 1 = &inbuf - emitter.instruction("lea rdx, [rbp - 32]"); // arg 2 = &inbytesleft - emitter.instruction("lea rcx, [rbp - 40]"); // arg 3 = &outbuf - emitter.instruction("lea r8, [rbp - 48]"); // arg 4 = &outbytesleft - emitter.instruction("call iconv"); // transcode a chunk of the payload - // produced = scratch capacity - remaining outbytesleft - emitter.instruction(&format!("mov rax, {}", ICONV_SCRATCH)); // scratch capacity - emitter.instruction("sub rax, QWORD PTR [rbp - 48]"); // produced = capacity - remaining - emitter.instruction("test rax, rax"); // anything produced this pass? - emitter.instruction(&format!("jz {}", after_write)); // nothing produced: skip the write - emitter.instruction("mov rdx, rax"); // produced byte count as the write length - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // fd = the saved descriptor - abi::emit_symbol_address(emitter, "rsi", "_stream_grow_scratch"); // write buffer = the scratch window base - emitter.instruction("call write"); // write the transcoded chunk through libc write() - emitter.label(&after_write); - emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // remaining inbytesleft? - emitter.instruction(&format!("je {}", done_label)); // all input consumed: done - emitter.instruction(&format!("mov rax, {}", ICONV_SCRATCH)); // recompute produced to test for progress - emitter.instruction("sub rax, QWORD PTR [rbp - 48]"); // produced this pass - emitter.instruction(&format!("jz {}", done_label)); // no progress (incomplete/invalid): stop to avoid a spin - emitter.instruction(&format!("jmp {}", loop_label)); // output filled: transcode the remainder - emitter.label(&done_label); - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // return value = the saved payload length - emitter.instruction("add rsp, 64"); // release the helper frame - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return the bytes-consumed count - - // ================================================================ - // iconv close helper. Input: rdi = fd. Closes the iconv descriptor. - // ================================================================ - emitter.label(close_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 16"); // helper frame: [-8]=fd - abi::emit_symbol_address(emitter, "r9", "_iconv_handles"); // iconv handle table base - emitter.instruction("mov rsi, QWORD PTR [r9 + rdi*8]"); // load this descriptor's iconv_t - emitter.instruction("test rsi, rsi"); // anything attached? - emitter.instruction(&format!("jz {}_done", close_label)); // nothing attached: nothing to close - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the descriptor across iconv_close - emitter.instruction("mov rdi, rsi"); // arg 0 = the iconv_t - emitter.instruction("call iconv_close"); // release the iconv descriptor - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the descriptor - abi::emit_symbol_address(emitter, "r9", "_iconv_handles"); // iconv handle table base - emitter.instruction("mov QWORD PTR [r9 + rdi*8], 0"); // clear this descriptor's iconv handle - emitter.label(&format!("{}_done", close_label)); - emitter.instruction("add rsp, 16"); // release the helper frame - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return to the fclose path - - // ================================================================ - // Initialization (inline): iconv_open + register the handle for this fd. - // ================================================================ - emitter.label(skip_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the initialization frame pointer - emitter.instruction("sub rsp, 24"); // frame: [-8]=fd (24: inline entry rsp 16-aligned, push rbp made it 8, +24≡8 mod 16 realigns to 0 at the libc calls) - emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the file descriptor - abi::emit_symbol_address(emitter, "rdi", &to_sym); // arg 0 = tocode - abi::emit_symbol_address(emitter, "rsi", &from_sym); // arg 1 = fromcode - emitter.instruction("call iconv_open"); // open the charset conversion descriptor - emitter.instruction("cmp rax, -1"); // is the descriptor (iconv_t)-1? - emitter.instruction(&format!("je {}", skip_store)); // iconv_open failed → attach no filter - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor - abi::emit_symbol_address(emitter, "r9", "_iconv_handles"); // iconv handle table base - emitter.instruction("mov QWORD PTR [r9 + rdi*8], rax"); // store the iconv_t for this descriptor - abi::emit_symbol_address(emitter, "r9", "_stream_write_filters"); // write-filter table base - emitter.instruction("mov BYTE PTR [r9 + rdi], 12"); // write-filter id 12 = convert.iconv write - emitter.instruction(&format!("lea r10, [rip + {}]", fwrite_label)); // address of the iconv write helper - abi::emit_symbol_address(emitter, "r9", "_iconv_fwrite_fn"); // _iconv_fwrite_fn slot - emitter.instruction("mov QWORD PTR [r9], r10"); // _iconv_fwrite_fn = the iconv write helper - emitter.instruction(&format!("lea r10, [rip + {}]", close_label)); // address of the iconv close helper - abi::emit_symbol_address(emitter, "r9", "_iconv_close_fn"); // _iconv_close_fn slot - emitter.instruction("mov QWORD PTR [r9], r10"); // _iconv_close_fn = the iconv close helper - emitter.label(&skip_store); - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // resource payload = the descriptor - emitter.instruction("add rsp, 24"); // release the initialization frame - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} diff --git a/src/codegen/builtins/io/stream_filter_inflate.rs b/src/codegen/builtins/io/stream_filter_inflate.rs deleted file mode 100644 index 17b2d11649..0000000000 --- a/src/codegen/builtins/io/stream_filter_inflate.rs +++ /dev/null @@ -1,321 +0,0 @@ -//! Purpose: -//! Emits the `zlib.inflate` read-direction stream filter attachment for -//! `stream_filter_append` / `stream_filter_prepend`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::stream_filter::emit_attach()` when the -//! filter-name literal is `"zlib.inflate"`. -//! -//! Key details: -//! - Unlike the 1:1 `string.*` filters and the streaming `zlib.deflate` write -//! filter, `zlib.inflate` is implemented by transforming the descriptor -//! itself: at attach time the whole compressed stream is slurped, inflated -//! once, written to an anonymous temp file, and `dup2`'d onto the original -//! descriptor. Every later `fread`/`fseek`/`feof` then works unchanged — no -//! per-fd filter state and no `__rt_fread` change are needed. -//! - The libz symbols (`inflateInit2_`, `inflate`, `inflateEnd`) are referenced -//! only from this builtin's USER asm, so the shared runtime stays libz-free. -//! - v1 caps the compressed input at the 64 KiB `_stream_filter_buf` scratch -//! and sizes the inflate output at 256x the input (min 64 KiB). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Capacity of the shared `_stream_filter_buf` scratch, reused here as the -/// compressed-input slurp buffer. -const FILTER_BUF_SIZE: i64 = 65536; -/// x86_64 owned-heap kind word: the elephc heap marker in the high 32 bits. -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits the `zlib.inflate` read-filter attachment. Returns the stream re-boxed -/// as a resource, matching `stream_filter_append`'s contract. -pub fn emit_zlib_inflate_attach( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_filter_append(zlib.inflate)"); - emit_stream_fd_arg("stream_filter_append", &args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => emit_arm64(emitter, |prefix| ctx.next_label(prefix)), - Arch::X86_64 => emit_x86_64(emitter, |prefix| ctx.next_label(prefix)), - } - Some(PhpType::Mixed) -} - -/// ARM64: a 176-byte scratch frame holds the 112-byte `z_stream` at `[sp, #0]` -/// plus the descriptor, lengths, and buffers at `[sp, #112..168)`. -pub(crate) fn emit_arm64(emitter: &mut Emitter, mut next_label: F) -where - F: FnMut(&str) -> String, -{ - let slurp = next_label("zlib_inflate_slurp"); - let slurp_done = next_label("zlib_inflate_slurped"); - let zero = next_label("zlib_inflate_zero"); - let zeroed = next_label("zlib_inflate_zeroed"); - let write = next_label("zlib_inflate_write"); - let write_done = next_label("zlib_inflate_written"); - - emitter.instruction("sub sp, sp, #176"); // z_stream frame plus saved values - emitter.instruction("str x0, [sp, #112]"); // save the source file descriptor - - // -- slurp every compressed byte from the descriptor into the scratch -- - emitter.instruction("str xzr, [sp, #120]"); // slurp offset = 0 - emitter.label(&slurp); - emitter.instruction("ldr x0, [sp, #112]"); // fd to read compressed bytes from - abi::emit_symbol_address(emitter, "x1", "_stream_filter_buf"); - emitter.instruction("ldr x9, [sp, #120]"); // current slurp offset - emitter.instruction("add x1, x1, x9"); // write pointer = scratch base + offset - emitter.instruction(&format!("mov x2, #{}", FILTER_BUF_SIZE)); // scratch capacity - emitter.instruction("sub x2, x2, x9"); // remaining scratch capacity - emitter.syscall(3); - emitter.instruction("cmp x0, #0"); // did the read hit EOF or fail? - emitter.instruction(&format!("b.le {}", slurp_done)); // stop slurping at EOF or on error - emitter.instruction("ldr x9, [sp, #120]"); // reload the slurp offset - emitter.instruction("add x9, x9, x0"); // advance by the bytes just read - emitter.instruction("str x9, [sp, #120]"); // store the updated compressed length - emitter.instruction(&format!("mov x10, #{}", FILTER_BUF_SIZE)); // scratch capacity - emitter.instruction("cmp x9, x10"); // is the scratch buffer full? - emitter.instruction(&format!("b.lt {}", slurp)); // room remains: keep slurping - emitter.label(&slurp_done); - - // -- size and allocate the inflate output buffer (256x input, min 64 KiB) -- - emitter.instruction("ldr x9, [sp, #120]"); // compressed length - emitter.instruction("lsl x9, x9, #8"); // budget 256x the compressed size - emitter.instruction(&format!("mov x10, #{}", FILTER_BUF_SIZE)); // minimum output buffer size - emitter.instruction("cmp x9, x10"); // is the 256x budget larger? - emitter.instruction("csel x9, x9, x10, gt"); // pick the larger buffer size - emitter.instruction("str x9, [sp, #152]"); // save the output buffer capacity - emitter.instruction("mov x0, x9"); // buffer size into the allocator argument - emitter.instruction("bl __rt_heap_alloc"); // allocate the decompressed-data buffer - emitter.instruction("mov x9, #1"); // heap kind 1 = persisted elephc string - emitter.instruction("str x9, [x0, #-8]"); // stamp the buffer as an owned string - emitter.instruction("str x0, [sp, #128]"); // save the decompressed buffer pointer - - // -- zero the 112-byte z_stream so zalloc/zfree start NULL -- - emitter.instruction("mov x9, #0"); // z_stream byte clear index - emitter.label(&zero); - emitter.instruction("cmp x9, #112"); // cleared the whole z_stream struct? - emitter.instruction(&format!("b.ge {}", zeroed)); // the struct is fully zeroed - emitter.instruction("strb wzr, [sp, x9]"); // zero one z_stream byte - emitter.instruction("add x9, x9, #1"); // advance the clear index - emitter.instruction(&format!("b {}", zero)); // continue zeroing the struct - emitter.label(&zeroed); - - // -- inflateInit2_(strm, -15, version, size): -15 selects raw inflate -- - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.instruction("mov x1, #-15"); // arg 1 = windowBits -15: raw inflate - abi::emit_symbol_address(emitter, "x2", "_zlib_version"); - emitter.instruction("mov x3, #112"); // arg 3 = sizeof(z_stream) for the ABI check - emitter.bl_c("inflateInit2_"); // initialize a raw-inflate zlib stream - - // -- point the stream at the slurped input and the output buffer -- - abi::emit_symbol_address(emitter, "x9", "_stream_filter_buf"); - emitter.instruction("str x9, [sp, #0]"); // z_stream.next_in = scratch base - emitter.instruction("ldr x9, [sp, #120]"); // compressed length - emitter.instruction("str w9, [sp, #8]"); // z_stream.avail_in = compressed length - emitter.instruction("ldr x9, [sp, #128]"); // decompressed buffer pointer - emitter.instruction("str x9, [sp, #24]"); // z_stream.next_out = decompressed buffer - emitter.instruction("ldr x9, [sp, #152]"); // output buffer capacity - emitter.instruction("str w9, [sp, #32]"); // z_stream.avail_out = output capacity - - // -- inflate the whole input in a single Z_FINISH pass -- - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.instruction("mov x1, #4"); // arg 1 = Z_FINISH - emitter.bl_c("inflate"); // decompress the entire input at once - emitter.instruction("ldr x9, [sp, #40]"); // z_stream.total_out = decompressed length - emitter.instruction("str x9, [sp, #136]"); // save the decompressed length - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.bl_c("inflateEnd"); // release zlib's internal inflate state - - // -- back the descriptor with an anonymous temp file of the plain bytes -- - emitter.instruction("bl __rt_tmpfile"); // create an unlinked temp file, x0 = fd - emitter.instruction("str x0, [sp, #144]"); // save the temp-file descriptor - - // -- write loop: copy every decompressed byte into the temp file -- - emitter.instruction("str xzr, [sp, #160]"); // write offset = 0 - emitter.label(&write); - emitter.instruction("ldr x10, [sp, #136]"); // total decompressed length - emitter.instruction("ldr x9, [sp, #160]"); // current write offset - emitter.instruction("cmp x9, x10"); // copied every decompressed byte? - emitter.instruction(&format!("b.ge {}", write_done)); // the whole payload is written - emitter.instruction("ldr x0, [sp, #144]"); // temp-file descriptor - emitter.instruction("ldr x1, [sp, #128]"); // decompressed buffer pointer - emitter.instruction("add x1, x1, x9"); // write pointer = buffer + offset - emitter.instruction("sub x2, x10, x9"); // remaining bytes to write - emitter.syscall(4); - emitter.instruction("cmp x0, #0"); // did the write make progress? - emitter.instruction(&format!("b.le {}", write_done)); // stop on a write error - emitter.instruction("ldr x9, [sp, #160]"); // reload the write offset - emitter.instruction("add x9, x9, x0"); // advance by the bytes just written - emitter.instruction("str x9, [sp, #160]"); // store the updated write offset - emitter.instruction(&format!("b {}", write)); // continue writing the payload - emitter.label(&write_done); - - // -- lseek(temp, 0, SEEK_SET): rewind so reads start at the plain bytes -- - emitter.instruction("ldr x0, [sp, #144]"); // temp-file descriptor - emitter.instruction("mov x1, #0"); // offset = 0 - emitter.instruction("mov x2, #0"); // whence = SEEK_SET - emitter.syscall(199); - - // -- dup2(temp, fd): the descriptor now serves the decompressed bytes -- - emitter.instruction("ldr x0, [sp, #144]"); // oldfd = temp file - emitter.instruction("ldr x1, [sp, #112]"); // newfd = the stream descriptor - emitter.bl_c("dup2"); // redirect the descriptor onto the temp file - - // -- close the now-redundant temp-file descriptor -- - emitter.instruction("ldr x0, [sp, #144]"); // the temp-file descriptor - emitter.syscall(6); - - // -- re-box the descriptor as a resource, matching stream_filter_append -- - emitter.instruction("ldr x0, [sp, #112]"); // reload the stream descriptor - emitter.instruction("add sp, sp, #176"); // release the scratch frame - emitter.instruction("mov x1, x0"); // resource payload = the descriptor - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} - -/// x86_64: same 176-byte scratch layout; `inflateInit2_` takes four register -/// arguments, so no stack-argument shuffling is needed. -pub(crate) fn emit_x86_64(emitter: &mut Emitter, mut next_label: F) -where - F: FnMut(&str) -> String, -{ - let slurp = next_label("zlib_inflate_slurp"); - let slurp_done = next_label("zlib_inflate_slurped"); - let sized = next_label("zlib_inflate_sized"); - let zero = next_label("zlib_inflate_zero"); - let zeroed = next_label("zlib_inflate_zeroed"); - let write = next_label("zlib_inflate_write"); - let write_done = next_label("zlib_inflate_written"); - - emitter.instruction("sub rsp, 176"); // z_stream frame plus saved values - emitter.instruction("mov QWORD PTR [rsp + 112], rax"); // save the source file descriptor - - // -- slurp every compressed byte from the descriptor into the scratch -- - emitter.instruction("mov QWORD PTR [rsp + 120], 0"); // slurp offset = 0 - emitter.label(&slurp); - emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // fd to read compressed bytes from - abi::emit_symbol_address(emitter, "rsi", "_stream_filter_buf"); // scratch base address - emitter.instruction("add rsi, QWORD PTR [rsp + 120]"); // write pointer = scratch base + offset - emitter.instruction(&format!("mov rdx, {}", FILTER_BUF_SIZE)); // scratch capacity - emitter.instruction("sub rdx, QWORD PTR [rsp + 120]"); // remaining scratch capacity - emitter.instruction("call read"); // read compressed bytes through libc read() - emitter.instruction("cmp rax, 0"); // did the read hit EOF or fail? - emitter.instruction(&format!("jle {}", slurp_done)); // stop slurping at EOF or on error - emitter.instruction("mov r9, QWORD PTR [rsp + 120]"); // reload the slurp offset - emitter.instruction("add r9, rax"); // advance by the bytes just read - emitter.instruction("mov QWORD PTR [rsp + 120], r9"); // store the updated compressed length - emitter.instruction(&format!("cmp r9, {}", FILTER_BUF_SIZE)); // is the scratch buffer full? - emitter.instruction(&format!("jl {}", slurp)); // room remains: keep slurping - emitter.label(&slurp_done); - - // -- size and allocate the inflate output buffer (256x input, min 64 KiB) -- - emitter.instruction("mov r9, QWORD PTR [rsp + 120]"); // compressed length - emitter.instruction("shl r9, 8"); // budget 256x the compressed size - emitter.instruction(&format!("cmp r9, {}", FILTER_BUF_SIZE)); // is the 256x budget above the minimum? - emitter.instruction(&format!("jge {}", sized)); // keep the larger budget - emitter.instruction(&format!("mov r9, {}", FILTER_BUF_SIZE)); // otherwise use the minimum buffer size - emitter.label(&sized); - emitter.instruction("mov QWORD PTR [rsp + 152], r9"); // save the output buffer capacity - emitter.instruction("mov rax, r9"); // buffer size into the allocator argument - emitter.instruction("call __rt_heap_alloc"); // allocate the decompressed-data buffer - emitter.instruction(&format!( // owned-string heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 1 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the buffer as an owned string - emitter.instruction("mov QWORD PTR [rsp + 128], rax"); // save the decompressed buffer pointer - - // -- zero the 112-byte z_stream so zalloc/zfree start NULL -- - emitter.instruction("xor r9, r9"); // z_stream byte clear index - emitter.label(&zero); - emitter.instruction("cmp r9, 112"); // cleared the whole z_stream struct? - emitter.instruction(&format!("jge {}", zeroed)); // the struct is fully zeroed - emitter.instruction("mov BYTE PTR [rsp + r9], 0"); // zero one z_stream byte - emitter.instruction("inc r9"); // advance the clear index - emitter.instruction(&format!("jmp {}", zero)); // continue zeroing the struct - emitter.label(&zeroed); - - // -- inflateInit2_(strm, -15, version, size): -15 selects raw inflate -- - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("mov esi, -15"); // arg 1 = windowBits -15: raw inflate - abi::emit_symbol_address(emitter, "rdx", "_zlib_version"); // arg 2 = the zlib version string - emitter.instruction("mov ecx, 112"); // arg 3 = sizeof(z_stream) for the ABI check - emitter.instruction("call inflateInit2_"); // initialize a raw-inflate zlib stream - - // -- point the stream at the slurped input and the output buffer -- - abi::emit_symbol_address(emitter, "r9", "_stream_filter_buf"); // scratch base address - emitter.instruction("mov QWORD PTR [rsp + 0], r9"); // z_stream.next_in = scratch base - emitter.instruction("mov r9, QWORD PTR [rsp + 120]"); // compressed length - emitter.instruction("mov DWORD PTR [rsp + 8], r9d"); // z_stream.avail_in = compressed length - emitter.instruction("mov r9, QWORD PTR [rsp + 128]"); // decompressed buffer pointer - emitter.instruction("mov QWORD PTR [rsp + 24], r9"); // z_stream.next_out = decompressed buffer - emitter.instruction("mov r9, QWORD PTR [rsp + 152]"); // output buffer capacity - emitter.instruction("mov DWORD PTR [rsp + 32], r9d"); // z_stream.avail_out = output capacity - - // -- inflate the whole input in a single Z_FINISH pass -- - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("mov esi, 4"); // arg 1 = Z_FINISH - emitter.instruction("call inflate"); // decompress the entire input at once - emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // z_stream.total_out = decompressed length - emitter.instruction("mov QWORD PTR [rsp + 136], rax"); // save the decompressed length - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("call inflateEnd"); // release zlib's internal inflate state - - // -- back the descriptor with an anonymous temp file of the plain bytes -- - emitter.instruction("call __rt_tmpfile"); // create an unlinked temp file, rax = fd - emitter.instruction("mov QWORD PTR [rsp + 144], rax"); // save the temp-file descriptor - - // -- write loop: copy every decompressed byte into the temp file -- - emitter.instruction("mov QWORD PTR [rsp + 160], 0"); // write offset = 0 - emitter.label(&write); - emitter.instruction("mov r10, QWORD PTR [rsp + 136]"); // total decompressed length - emitter.instruction("mov r9, QWORD PTR [rsp + 160]"); // current write offset - emitter.instruction("cmp r9, r10"); // copied every decompressed byte? - emitter.instruction(&format!("jge {}", write_done)); // the whole payload is written - emitter.instruction("mov rdi, QWORD PTR [rsp + 144]"); // temp-file descriptor - emitter.instruction("mov rsi, QWORD PTR [rsp + 128]"); // decompressed buffer pointer - emitter.instruction("add rsi, r9"); // write pointer = buffer + offset - emitter.instruction("mov rdx, r10"); // total decompressed length - emitter.instruction("sub rdx, r9"); // remaining bytes to write - emitter.instruction("call write"); // write the plain bytes through libc write() - emitter.instruction("cmp rax, 0"); // did the write make progress? - emitter.instruction(&format!("jle {}", write_done)); // stop on a write error - emitter.instruction("mov r9, QWORD PTR [rsp + 160]"); // reload the write offset - emitter.instruction("add r9, rax"); // advance by the bytes just written - emitter.instruction("mov QWORD PTR [rsp + 160], r9"); // store the updated write offset - emitter.instruction(&format!("jmp {}", write)); // continue writing the payload - emitter.label(&write_done); - - // -- lseek(temp, 0, SEEK_SET): rewind so reads start at the plain bytes -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 144]"); // temp-file descriptor - emitter.instruction("xor esi, esi"); // offset = 0 - emitter.instruction("xor edx, edx"); // whence = SEEK_SET - emitter.instruction("call lseek"); // rewind the temp file - - // -- dup2(temp, fd): the descriptor now serves the decompressed bytes -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 144]"); // oldfd = temp file - emitter.instruction("mov rsi, QWORD PTR [rsp + 112]"); // newfd = the stream descriptor - emitter.instruction("call dup2"); // redirect the descriptor onto the temp file - - // -- close the now-redundant temp-file descriptor -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 144]"); // the temp-file descriptor - emitter.instruction("call close"); // release the redundant descriptor - - // -- re-box the descriptor as a resource, matching stream_filter_append -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 112]"); // resource payload = the descriptor - emitter.instruction("add rsp, 176"); // release the scratch frame - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} diff --git a/src/codegen/builtins/io/stream_filter_register.rs b/src/codegen/builtins/io/stream_filter_register.rs deleted file mode 100644 index 28ac55fe71..0000000000 --- a/src/codegen/builtins/io/stream_filter_register.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_filter_register` calls. -//! Records a `(filter_name, class_name)` pair in the runtime user-filter -//! registry that `stream_filter_append`/`prepend` consult on attachment -//! and `__rt_apply_stream_filter` dispatches into on read/write. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The registry stores up to 128 registrations. -//! On success the runtime helper returns `true`; on a full table it -//! returns `false`. The wrapper class is invoked through the per-class -//! `_user_filter_vtable_` (slot 0 = filter, 1 = onCreate, -//! 2 = onClose) — the elephc v1 contract is `filter(string): string`, -//! not the PHP bucket-brigade signature. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_filter_register()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_filter_register()"); - // PHP evaluates the filter name first, then the class name. The two - // strings are handed to the runtime helper. - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the filter-name string ptr/len - emit_expr(&args[1], emitter, ctx, data); - // After emit_expr, x1/x2 hold the class-name string. The helper - // expects x0=name_ptr x1=name_len x2=class_ptr x3=class_len. - // Move class_len into x3 first so the class_ptr → x2 mov does - // not clobber the original x2. - emitter.instruction("mov x3, x2"); // class-name length into x3 - emitter.instruction("mov x2, x1"); // class-name pointer into x2 - abi::emit_pop_reg_pair(emitter, "x0", "x1"); // restore filter-name ptr/len - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the filter-name string ptr/len - emit_expr(&args[1], emitter, ctx, data); - // x86_64 helper expects rdi=name_ptr rsi=name_len rdx=class_ptr - // rcx=class_len. The class string is in rax/rdx after emit_expr. - emitter.instruction("mov rcx, rdx"); // class-name length into rcx - emitter.instruction("mov rdx, rax"); // class-name pointer into rdx - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore filter-name ptr/len - } - } - abi::emit_call_label(emitter, "__rt_stream_filter_register"); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_filter_zlib.rs b/src/codegen/builtins/io/stream_filter_zlib.rs deleted file mode 100644 index c0dc21fbaa..0000000000 --- a/src/codegen/builtins/io/stream_filter_zlib.rs +++ /dev/null @@ -1,421 +0,0 @@ -//! Purpose: -//! Emits the `zlib.deflate` write-direction stream filter attachment for -//! `stream_filter_append` / `stream_filter_prepend`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::stream_filter::emit_attach()` when the -//! filter-name literal is `"zlib.deflate"`. -//! -//! Key details: -//! - The libz symbols (`deflate`, `deflateEnd`, `deflateInit_`) are referenced -//! only from this builtin's USER asm. The shared runtime object never names a -//! libz symbol, so non-zlib programs still link without `-lz`. -//! - Two per-program helper routines (`fwrite` and `close`) are emitted inline, -//! skipped over by an unconditional branch, and their addresses stored into -//! the `_zlib_fwrite_fn` / `_zlib_close_fn` globals. `__rt_fwrite` and the -//! `fclose` builtin reach libz indirectly through those function pointers. -//! - Per-descriptor `z_stream` state lives in the `_zstream_handles` table, -//! indexed by file descriptor. The write-filter table entry is set to id 4. -//! - The `z_stream` struct is LP64-sized (112 bytes); zeroing it leaves -//! `zalloc`/`zfree` NULL so zlib uses its own allocator. The struct itself is -//! intentionally not freed on close — a small, documented v1 leak. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Size of the libz `z_stream` struct on LP64 targets, in bytes. -const Z_STREAM_SIZE: i64 = 112; -/// Capacity of the shared `_stream_filter_buf` scratch used as the deflate -/// output window. -const FILTER_BUF_SIZE: i64 = 65536; -/// x86_64 owned-heap kind word: the elephc heap marker in the high 32 bits. -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits the `zlib.deflate` write-filter attachment. Returns the stream -/// re-boxed as a resource, matching `stream_filter_append`'s contract. -pub fn emit_zlib_deflate_attach( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_filter_append(zlib.deflate)"); - emit_stream_fd_arg("stream_filter_append", &args[0], emitter, ctx, data); - - // PHP's 4th `$params` arg sets the deflate compression level (-1..9), as - // either a bare int (`$rw, 6`) or the canonical array form (`['level' => 6]`). - // Both literal forms are honored at compile time; anything else keeps the - // default. The `window` (fixed at -15 for the raw-deflate round-trip with - // compress.zlib://) and `memory` sub-options are not exposed. - let level = super::stream_filter::const_int_param(args, "level", true, -1, 9).unwrap_or(-1); - - let fwrite_label = ctx.next_label("zlib_deflate_fwrite"); - let close_label = ctx.next_label("zlib_deflate_close"); - let skip_label = ctx.next_label("zlib_deflate_skip_helpers"); - - match emitter.target.arch { - Arch::AArch64 => emit_arm64(emitter, &fwrite_label, &close_label, &skip_label, level), - Arch::X86_64 => emit_x86_64(emitter, &fwrite_label, &close_label, &skip_label, level), - } - Some(PhpType::Mixed) -} - -/// Emits the ARM64 helpers, then the deflate-stream initialization. -pub(crate) fn emit_arm64( - emitter: &mut Emitter, - fwrite_label: &str, - close_label: &str, - skip_label: &str, - level: i64, -) { - // -- jump past the helper bodies so normal flow never falls into them -- - emitter.instruction(&format!("b {}", skip_label)); // skip over the inline zlib helper routines - - // ================================================================ - // zlib deflate fwrite helper. - // Input: x0 = fd, x1 = payload pointer, x2 = payload length. - // Output: x0 = the input payload length (bytes "written"). - // ================================================================ - emitter.label(fwrite_label); - emitter.instruction("sub sp, sp, #48"); // frame: [0]=fd [8]=length [16]=z_stream ptr [32]=x29 [40]=x30 - emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #32"); // establish the helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the file descriptor for the write loop - emitter.instruction("str x2, [sp, #8]"); // save the payload length as the return value - - // -- load this descriptor's z_stream handle and seed the input window -- - abi::emit_symbol_address(emitter, "x9", "_zstream_handles"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // x10 = z_stream pointer for this descriptor - emitter.instruction("str x10, [sp, #16]"); // save the z_stream pointer across the calls - emitter.instruction("str x1, [x10, #0]"); // z_stream.next_in = payload pointer - emitter.instruction("str w2, [x10, #8]"); // z_stream.avail_in = payload length - - // -- deflate loop: drain next_in into the scratch window and write it out -- - emitter.label(&format!("{}_loop", fwrite_label)); - emitter.instruction("ldr x10, [sp, #16]"); // reload the z_stream pointer - abi::emit_symbol_address(emitter, "x11", "_stream_filter_buf"); - emitter.instruction("str x11, [x10, #24]"); // z_stream.next_out = scratch window base - emitter.instruction(&format!("mov w12, #{}", FILTER_BUF_SIZE & 0xFFFF)); // low half of the scratch window capacity - emitter.instruction(&format!("movk w12, #{}, lsl #16", FILTER_BUF_SIZE >> 16)); // high half of the scratch window capacity - emitter.instruction("str w12, [x10, #32]"); // z_stream.avail_out = scratch window capacity - emitter.instruction("mov x0, x10"); // arg 0 = z_stream pointer - emitter.instruction("mov w1, #0"); // arg 1 = Z_NO_FLUSH (0) - emitter.bl_c("deflate"); // run one deflate step over the input window - // -- compute produced = capacity - avail_out and write it to the fd -- - emitter.instruction("ldr x10, [sp, #16]"); // reload the z_stream pointer after the deflate call - emitter.instruction("ldr w12, [x10, #32]"); // reload avail_out left after this deflate step - emitter.instruction(&format!("mov w13, #{}", FILTER_BUF_SIZE & 0xFFFF)); // low half of the scratch window capacity - emitter.instruction(&format!("movk w13, #{}, lsl #16", FILTER_BUF_SIZE >> 16)); // high half of the scratch window capacity - emitter.instruction("sub w12, w13, w12"); // produced = capacity - avail_out - emitter.instruction("ldr x0, [sp, #0]"); // fd = the saved file descriptor - abi::emit_symbol_address(emitter, "x1", "_stream_filter_buf"); - emitter.instruction("uxtw x2, w12"); // produced byte count as the write length - emitter.syscall(4); - // -- repeat while input remains OR the output window filled completely -- - emitter.instruction("ldr x10, [sp, #16]"); // reload the z_stream pointer after the write - emitter.instruction("ldr w14, [x10, #8]"); // reload avail_in still pending - emitter.instruction(&format!("cbnz w14, {}_loop", fwrite_label)); // more input bytes: keep deflating - emitter.instruction("ldr w12, [x10, #32]"); // reload avail_out left after this deflate step - emitter.instruction(&format!("cbz w12, {}_loop", fwrite_label)); // window was filled: drain the remainder - // -- done: return the original payload length -- - emitter.instruction("ldr x0, [sp, #8]"); // return value = the saved payload length - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // release the helper frame - emitter.instruction("ret"); // return the bytes-consumed count - - // ================================================================ - // zlib deflate close helper. - // Input: x0 = fd. Flushes the deflate tail and ends the stream. - // ================================================================ - emitter.label(close_label); - emitter.instruction("sub sp, sp, #48"); // frame: [0]=fd [8]=z_stream [16]=ret code [32]=x29 [40]=x30 - emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #32"); // establish the helper frame pointer - abi::emit_symbol_address(emitter, "x9", "_zstream_handles"); - emitter.instruction("ldr x10, [x9, x0, lsl #3]"); // x10 = z_stream pointer for this descriptor - emitter.instruction(&format!("cbz x10, {}_done", close_label)); // nothing to flush when no filter is attached - emitter.instruction("str x0, [sp, #0]"); // save the file descriptor across deflate calls - emitter.instruction("str x10, [sp, #8]"); // save the z_stream pointer - emitter.instruction("str xzr, [x10, #0]"); // z_stream.next_in = NULL: no further input - emitter.instruction("str wzr, [x10, #8]"); // z_stream.avail_in = 0: input is exhausted - - // -- flush loop: deflate with Z_FINISH until Z_STREAM_END -- - emitter.label(&format!("{}_loop", close_label)); - emitter.instruction("ldr x10, [sp, #8]"); // reload the z_stream pointer - abi::emit_symbol_address(emitter, "x11", "_stream_filter_buf"); - emitter.instruction("str x11, [x10, #24]"); // z_stream.next_out = scratch window base - emitter.instruction(&format!("mov w12, #{}", FILTER_BUF_SIZE & 0xFFFF)); // low half of the scratch window capacity - emitter.instruction(&format!("movk w12, #{}, lsl #16", FILTER_BUF_SIZE >> 16)); // high half of the scratch window capacity - emitter.instruction("str w12, [x10, #32]"); // z_stream.avail_out = scratch window capacity - emitter.instruction("mov x0, x10"); // arg 0 = z_stream pointer - emitter.instruction("mov w1, #4"); // arg 1 = Z_FINISH (4) - emitter.bl_c("deflate"); // flush a chunk of the compressed tail - emitter.instruction("str x0, [sp, #16]"); // save the deflate return code (1 = Z_STREAM_END) - // -- compute produced = capacity - avail_out and write it to the fd -- - emitter.instruction("ldr x10, [sp, #8]"); // reload the z_stream pointer - emitter.instruction("ldr w12, [x10, #32]"); // reload avail_out left after this flush step - emitter.instruction(&format!("mov w13, #{}", FILTER_BUF_SIZE & 0xFFFF)); // low half of the scratch window capacity - emitter.instruction(&format!("movk w13, #{}, lsl #16", FILTER_BUF_SIZE >> 16)); // high half of the scratch window capacity - emitter.instruction("sub w12, w13, w12"); // produced = capacity - avail_out - emitter.instruction("ldr x0, [sp, #0]"); // fd = the saved file descriptor - abi::emit_symbol_address(emitter, "x1", "_stream_filter_buf"); - emitter.instruction("uxtw x2, w12"); // produced byte count as the write length - emitter.syscall(4); - emitter.instruction("ldr x12, [sp, #16]"); // reload the saved deflate return code - emitter.instruction("cmp x12, #1"); // did deflate report Z_STREAM_END? - emitter.instruction(&format!("b.ne {}_loop", close_label)); // not finished yet: flush another chunk - - // -- end the deflate stream and drop the per-descriptor handle -- - emitter.instruction("ldr x0, [sp, #8]"); // arg 0 = z_stream pointer - emitter.bl_c("deflateEnd"); // release zlib's internal deflate state - emitter.instruction("ldr x0, [sp, #0]"); // reload the file descriptor - abi::emit_symbol_address(emitter, "x9", "_zstream_handles"); - emitter.instruction("str xzr, [x9, x0, lsl #3]"); // clear this descriptor's z_stream handle - emitter.label(&format!("{}_done", close_label)); - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // release the helper frame - emitter.instruction("ret"); // return to the fclose path - - // ================================================================ - // Initialization: allocate and register a z_stream for this fd. - // ================================================================ - emitter.label(skip_label); - emitter.instruction("sub sp, sp, #16"); // frame: [0]=fd [8]=z_stream pointer - emitter.instruction("str x0, [sp, #0]"); // save the file descriptor across the calls - emitter.instruction(&format!("mov x0, #{}", Z_STREAM_SIZE)); // request a z_stream-sized heap block - emitter.instruction("bl __rt_heap_alloc"); // allocate the z_stream struct, x0 = payload - emitter.instruction("mov x9, #1"); // heap kind 1 = owned allocation - emitter.instruction("str x9, [x0, #-8]"); // stamp the z_stream block as owned heap state - emitter.instruction("str x0, [sp, #8]"); // save the z_stream pointer - - // -- zero all 112 bytes so zalloc/zfree are NULL and counters start clean -- - emitter.instruction("mov x9, #0"); // byte clear index - emitter.label(&format!("{}_zero", skip_label)); - emitter.instruction(&format!("cmp x9, #{}", Z_STREAM_SIZE)); // cleared the whole z_stream struct? - emitter.instruction(&format!("b.ge {}_zeroed", skip_label)); // the struct is fully zeroed - emitter.instruction("strb wzr, [x0, x9]"); // zero one z_stream byte - emitter.instruction("add x9, x9, #1"); // advance the clear index - emitter.instruction(&format!("b {}_zero", skip_label)); // continue zeroing the struct - emitter.label(&format!("{}_zeroed", skip_label)); - - // -- deflateInit2_(strm, level, Z_DEFLATED, -15, memLevel, strategy, ...) -- - // windowBits -15 selects raw deflate (no zlib header), matching PHP's - // zlib.deflate stream filter. - emitter.instruction("ldr x0, [sp, #8]"); // arg 0 = z_stream pointer - emitter.instruction(&format!("mov x1, #{}", level)); // arg 1 = compression level ($params, default Z_DEFAULT_COMPRESSION -1) - emitter.instruction("mov x2, #8"); // arg 2 = Z_DEFLATED method - emitter.instruction("mov x3, #-15"); // arg 3 = windowBits -15: raw deflate, no header - emitter.instruction("mov x4, #8"); // arg 4 = default memLevel - emitter.instruction("mov x5, #0"); // arg 5 = Z_DEFAULT_STRATEGY - abi::emit_symbol_address(emitter, "x6", "_zlib_version"); - emitter.instruction(&format!("mov x7, #{}", Z_STREAM_SIZE)); // arg 7 = sizeof(z_stream) for the ABI check - emitter.bl_c("deflateInit2_"); // initialize a raw-deflate zlib stream - - // -- register the handle and mark the descriptor's write filter as zlib -- - emitter.instruction("ldr x0, [sp, #0]"); // reload the file descriptor - emitter.instruction("ldr x10, [sp, #8]"); // reload the z_stream pointer - abi::emit_symbol_address(emitter, "x9", "_zstream_handles"); - emitter.instruction("str x10, [x9, x0, lsl #3]"); // store the z_stream handle for this descriptor - abi::emit_symbol_address(emitter, "x9", "_stream_write_filters"); - emitter.instruction("mov w11, #4"); // write-filter id 4 = zlib.deflate - emitter.instruction("strb w11, [x9, x0]"); // record the zlib write filter for this descriptor - - // -- publish the helper addresses so __rt_fwrite / fclose can call them -- - abi::emit_symbol_address(emitter, "x11", fwrite_label); - abi::emit_symbol_address(emitter, "x9", "_zlib_fwrite_fn"); - emitter.instruction("str x11, [x9]"); // _zlib_fwrite_fn = the deflate fwrite helper - abi::emit_symbol_address(emitter, "x11", close_label); - abi::emit_symbol_address(emitter, "x9", "_zlib_close_fn"); - emitter.instruction("str x11, [x9]"); // _zlib_close_fn = the deflate close helper - - // -- re-box the descriptor as a resource, matching stream_filter_append -- - emitter.instruction("ldr x0, [sp, #0]"); // reload the file descriptor - emitter.instruction("add sp, sp, #16"); // release the initialization frame - emitter.instruction("mov x1, x0"); // resource payload = the descriptor - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} - -/// Emits the x86_64 helpers, then the deflate-stream initialization. -pub(crate) fn emit_x86_64( - emitter: &mut Emitter, - fwrite_label: &str, - close_label: &str, - skip_label: &str, - level: i64, -) { - // -- jump past the helper bodies so normal flow never falls into them -- - emitter.instruction(&format!("jmp {}", skip_label)); // skip over the inline zlib helper routines - - // ================================================================ - // zlib deflate fwrite helper. - // Input: rdi = fd, rsi = payload pointer, rdx = payload length. - // Output: rax = the input payload length (bytes "written"). - // ================================================================ - emitter.label(fwrite_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 32"); // frame: [-8]=fd [-16]=length [-24]=z_stream - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the file descriptor for the write loop - emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the payload length as the return value - - // -- load this descriptor's z_stream handle and seed the input window -- - abi::emit_symbol_address(emitter, "r9", "_zstream_handles"); // z_stream handle table base - emitter.instruction("mov r10, QWORD PTR [r9 + rdi*8]"); // r10 = z_stream pointer for this descriptor - emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the z_stream pointer - emitter.instruction("mov QWORD PTR [r10 + 0], rsi"); // z_stream.next_in = payload pointer - emitter.instruction("mov DWORD PTR [r10 + 8], edx"); // z_stream.avail_in = payload length - - // -- deflate loop: drain next_in into the scratch window and write it out -- - emitter.label(&format!("{}_loop", fwrite_label)); - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the z_stream pointer - abi::emit_symbol_address(emitter, "r11", "_stream_filter_buf"); // scratch window base - emitter.instruction("mov QWORD PTR [r10 + 24], r11"); // z_stream.next_out = scratch window base - emitter.instruction(&format!("mov DWORD PTR [r10 + 32], {}", FILTER_BUF_SIZE)); // z_stream.avail_out = scratch window capacity - emitter.instruction("mov rdi, r10"); // arg 0 = z_stream pointer - emitter.instruction("xor esi, esi"); // arg 1 = Z_NO_FLUSH (0) - emitter.instruction("call deflate"); // run one deflate step over the input window - // -- compute produced = capacity - avail_out and write it to the fd -- - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the z_stream pointer - emitter.instruction(&format!("mov eax, {}", FILTER_BUF_SIZE)); // scratch window capacity - emitter.instruction("sub eax, DWORD PTR [r10 + 32]"); // produced = capacity - avail_out - emitter.instruction("mov edx, eax"); // produced byte count as the write length - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // fd = the saved file descriptor - abi::emit_symbol_address(emitter, "rsi", "_stream_filter_buf"); // write buffer = the scratch window base - emitter.instruction("call write"); // write the compressed chunk through libc write() - // -- repeat while input remains OR the output window filled completely -- - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the z_stream pointer - emitter.instruction("cmp DWORD PTR [r10 + 8], 0"); // any avail_in input bytes still pending? - emitter.instruction(&format!("jne {}_loop", fwrite_label)); // more input bytes: keep deflating - emitter.instruction("cmp DWORD PTR [r10 + 32], 0"); // did the output window fill completely? - emitter.instruction(&format!("je {}_loop", fwrite_label)); // window was filled: drain the remainder - // -- done: return the original payload length -- - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // return value = the saved payload length - emitter.instruction("add rsp, 32"); // release the helper frame - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return the bytes-consumed count - - // ================================================================ - // zlib deflate close helper. - // Input: rdi = fd. Flushes the deflate tail and ends the stream. - // ================================================================ - emitter.label(close_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 32"); // frame: [-8]=fd [-16]=z_stream [-24]=ret code - abi::emit_symbol_address(emitter, "r9", "_zstream_handles"); // z_stream handle table base - emitter.instruction("mov r10, QWORD PTR [r9 + rdi*8]"); // r10 = z_stream pointer for this descriptor - emitter.instruction("test r10, r10"); // is a deflate stream attached to this descriptor? - emitter.instruction(&format!("jz {}_done", close_label)); // nothing to flush when no filter is attached - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the file descriptor across deflate calls - emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // save the z_stream pointer - emitter.instruction("mov QWORD PTR [r10 + 0], 0"); // z_stream.next_in = NULL: no further input - emitter.instruction("mov DWORD PTR [r10 + 8], 0"); // z_stream.avail_in = 0: input is exhausted - - // -- flush loop: deflate with Z_FINISH until Z_STREAM_END -- - emitter.label(&format!("{}_loop", close_label)); - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the z_stream pointer - abi::emit_symbol_address(emitter, "r11", "_stream_filter_buf"); // scratch window base - emitter.instruction("mov QWORD PTR [r10 + 24], r11"); // z_stream.next_out = scratch window base - emitter.instruction(&format!("mov DWORD PTR [r10 + 32], {}", FILTER_BUF_SIZE)); // z_stream.avail_out = scratch window capacity - emitter.instruction("mov rdi, r10"); // arg 0 = z_stream pointer - emitter.instruction("mov esi, 4"); // arg 1 = Z_FINISH (4) - emitter.instruction("call deflate"); // flush a chunk of the compressed tail - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the deflate return code (1 = Z_STREAM_END) - // -- compute produced = capacity - avail_out and write it to the fd -- - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the z_stream pointer - emitter.instruction(&format!("mov eax, {}", FILTER_BUF_SIZE)); // scratch window capacity - emitter.instruction("sub eax, DWORD PTR [r10 + 32]"); // produced = capacity - avail_out - emitter.instruction("mov edx, eax"); // produced byte count as the write length - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // fd = the preserved file descriptor - abi::emit_symbol_address(emitter, "rsi", "_stream_filter_buf"); // write buffer = the scratch window base - emitter.instruction("call write"); // write the compressed tail chunk through libc write() - emitter.instruction("cmp QWORD PTR [rbp - 24], 1"); // did deflate report Z_STREAM_END? - emitter.instruction(&format!("jne {}_loop", close_label)); // not finished yet: flush another chunk - - // -- end the deflate stream and drop the per-descriptor handle -- - emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // arg 0 = z_stream pointer - emitter.instruction("call deflateEnd"); // release zlib's internal deflate state - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor - abi::emit_symbol_address(emitter, "r9", "_zstream_handles"); // z_stream handle table base - emitter.instruction("mov QWORD PTR [r9 + rdi*8], 0"); // clear this descriptor's z_stream handle - emitter.label(&format!("{}_done", close_label)); - emitter.instruction("add rsp, 32"); // release the helper frame - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return to the fclose path - - // ================================================================ - // Initialization: allocate and register a z_stream for this fd. - // ================================================================ - emitter.label(skip_label); - emitter.instruction("push rbp"); // preserve the caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish the initialization frame pointer - emitter.instruction("sub rsp, 24"); // frame: [-8]=fd [-16]=z_stream ptr (24 keeps rsp 16-aligned) - emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the file descriptor across the calls - emitter.instruction(&format!("mov rax, {}", Z_STREAM_SIZE)); // request a z_stream-sized heap block - emitter.instruction("call __rt_heap_alloc"); // allocate the z_stream struct, rax = payload - emitter.instruction(&format!( // owned-heap kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 1 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the z_stream block as owned heap state - emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the z_stream pointer - - // -- zero all 112 bytes so zalloc/zfree are NULL and counters start clean -- - emitter.instruction("xor r9, r9"); // byte clear index - emitter.label(&format!("{}_zero", skip_label)); - emitter.instruction(&format!("cmp r9, {}", Z_STREAM_SIZE)); // cleared the whole z_stream struct? - emitter.instruction(&format!("jge {}_zeroed", skip_label)); // the struct is fully zeroed - emitter.instruction("mov BYTE PTR [rax + r9], 0"); // zero one z_stream byte - emitter.instruction("inc r9"); // advance the clear index - emitter.instruction(&format!("jmp {}_zero", skip_label)); // continue zeroing the struct - emitter.label(&format!("{}_zeroed", skip_label)); - - // -- deflateInit2_(strm, level, Z_DEFLATED, -15, memLevel, strategy, ...) -- - // windowBits -15 selects raw deflate (no zlib header), matching PHP's - // zlib.deflate stream filter. The version/size args 7-8 go on the stack. - emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // arg 0 = z_stream pointer - emitter.instruction(&format!("mov esi, {}", level)); // arg 1 = compression level ($params, default Z_DEFAULT_COMPRESSION -1) - emitter.instruction("mov edx, 8"); // arg 2 = Z_DEFLATED method - emitter.instruction("mov ecx, -15"); // arg 3 = windowBits -15: raw deflate, no header - emitter.instruction("mov r8d, 8"); // arg 4 = default memLevel - emitter.instruction("xor r9d, r9d"); // arg 5 = Z_DEFAULT_STRATEGY - emitter.instruction("sub rsp, 16"); // reserve the two stack arguments (kept 16-aligned) - abi::emit_symbol_address(emitter, "rax", "_zlib_version"); // the zlib version string - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // stack arg 6 = version - emitter.instruction(&format!("mov QWORD PTR [rsp + 8], {}", Z_STREAM_SIZE)); // stack arg 7 = sizeof(z_stream) - emitter.instruction("call deflateInit2_"); // initialize a raw-deflate zlib stream - emitter.instruction("add rsp, 16"); // release the stack-argument space - - // -- register the handle and mark the descriptor's write filter as zlib -- - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the z_stream pointer - abi::emit_symbol_address(emitter, "r9", "_zstream_handles"); // z_stream handle table base - emitter.instruction("mov QWORD PTR [r9 + rdi*8], r10"); // store the z_stream handle for this descriptor - abi::emit_symbol_address(emitter, "r9", "_stream_write_filters"); // write-filter table base - emitter.instruction("mov BYTE PTR [r9 + rdi], 4"); // write-filter id 4 = zlib.deflate - - // -- publish the helper addresses so __rt_fwrite / fclose can call them -- - emitter.instruction(&format!("lea r10, [rip + {}]", fwrite_label)); // address of the deflate fwrite helper - abi::emit_symbol_address(emitter, "r9", "_zlib_fwrite_fn"); // _zlib_fwrite_fn slot - emitter.instruction("mov QWORD PTR [r9], r10"); // _zlib_fwrite_fn = the deflate fwrite helper - emitter.instruction(&format!("lea r10, [rip + {}]", close_label)); // address of the deflate close helper - abi::emit_symbol_address(emitter, "r9", "_zlib_close_fn"); // _zlib_close_fn slot - emitter.instruction("mov QWORD PTR [r9], r10"); // _zlib_close_fn = the deflate close helper - - // -- re-box the descriptor as a resource, matching stream_filter_append -- - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // resource payload = the descriptor - emitter.instruction("add rsp, 24"); // release the initialization frame - emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // re-box the stream as the filter resource -} diff --git a/src/codegen/builtins/io/stream_get_contents.rs b/src/codegen/builtins/io/stream_get_contents.rs deleted file mode 100644 index ac44e779b2..0000000000 --- a/src/codegen/builtins/io/stream_get_contents.rs +++ /dev/null @@ -1,375 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_get_contents` calls. -//! Reads bytes from a stream resource into an elephc string, honoring the -//! optional `$length` (maximum bytes) and `$offset` (seek-before-read) args. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - With no finite `$length`, a normal descriptor delegates to the TLS-aware -//! `__rt_stream_get_contents` read-all helper, while a synthetic user-wrapper -//! descriptor (`>= 0x40000000`) is drained by a feof-gated compiled loop -//! (see `emit_read_all_from_fd`). Checking feof FIRST avoids the corrupting -//! empty read at EOF that frees the caller's resource cell. -//! - A finite positive `$length` delegates to `__rt_stream_get_contents_bounded`, -//! which loops through `__rt_fread` until the requested byte count is filled, -//! EOF is reached, or an empty read is produced. Dynamic `null` / negative -//! lengths are checked at run time and fall back to the read-all path, -//! matching PHP's default `-1` contract. -//! - `$offset >= 0` seeks the descriptor before reading (lseek for a normal fd, -//! the wrapper's `stream_seek` for a synthetic fd); a failed seek boxes PHP -//! `false`. Successful reads are also boxed so `string|false` keeps one -//! runtime representation. A literal `null`/negative `$length` means "read to -//! EOF" and `$offset < 0`/omitted means "do not seek". - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::driver_support::emit_box_current_value_as_mixed; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::codegen::NULL_SENTINEL; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - - -/// Returns true when a `$length`/`$offset` argument is a compile-time literal -/// meaning "read to EOF" / "do not seek" — i.e. `null` or a negative integer -/// literal (`-1`, the PHP default; the parser models `-1` as `Negate(IntLiteral)`). -/// Such literals have no side effects, so the caller can skip evaluating them and -/// treat the parameter as absent. Shared with `stream_copy_to_stream`. -pub(super) fn is_read_all_or_no_seek(expr: &Expr) -> bool { - match &expr.kind { - ExprKind::Null => true, - ExprKind::IntLiteral(n) => *n < 0, - ExprKind::Negate(inner) => matches!(inner.kind, ExprKind::IntLiteral(n) if n > 0), - _ => false, - } -} - -/// Branches to `target_label` when a runtime length register means "unlimited": -/// PHP `null` (elephc's null sentinel) or a negative integer such as `-1`. -pub(super) fn emit_branch_if_unlimited_length( - emitter: &mut Emitter, - length_reg: &str, - scratch_reg: &str, - target_label: &str, -) { - abi::emit_load_int_immediate(emitter, scratch_reg, NULL_SENTINEL); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, {}", length_reg, scratch_reg)); // is the requested length PHP null? - emitter.instruction(&format!("b.eq {}", target_label)); // null length means read/copy until EOF - emitter.instruction(&format!("cmp {}, #0", length_reg)); // is the requested length negative? - emitter.instruction(&format!("b.lt {}", target_label)); // negative length means read/copy until EOF - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", length_reg, scratch_reg)); // is the requested length PHP null? - emitter.instruction(&format!("je {}", target_label)); // null length means read/copy until EOF - emitter.instruction(&format!("cmp {}, 0", length_reg)); // is the requested length negative? - emitter.instruction(&format!("jl {}", target_label)); // negative length means read/copy until EOF - } - } -} - -/// Emits codegen for PHP `stream_get_contents()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_get_contents()"); - emit_stream_fd_arg("stream_get_contents", &args[0], emitter, ctx, data); - - let has_len = args.len() >= 2 && !is_read_all_or_no_seek(&args[1]); - let has_off = args.len() >= 3 && !is_read_all_or_no_seek(&args[2]); - - if !has_len && !has_off { - // Fast path: read every remaining byte from the current position. - emit_read_all_from_fd(emitter, ctx); - emit_box_current_value_as_mixed(emitter, &PhpType::Str); - return Some(PhpType::Mixed); - } - - // General path: stash the fd, evaluate $length then $offset (PHP source - // order), optionally seek, then read. The 32-byte frame stays 16-aligned - // so the x86_64 `call lseek` below lands on an aligned stack. - let skip_seek = ctx.next_label("sgc_skip_seek"); - let wrap_seek = ctx.next_label("sgc_wrap_seek"); - let seek_failed = ctx.next_label("sgc_seek_failed"); - let read_all = ctx.next_label("sgc_read_all"); - let done = ctx.next_label("sgc_general_done"); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("sub sp, sp, #32"), // frame: [sp,#0]=fd, [sp,#8]=max_len (16-aligned) - Arch::X86_64 => emitter.instruction("sub rsp, 32"), // frame: [rsp+0]=fd, [rsp+8]=max_len (16-aligned) - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("str x0, [sp, #0]"), // save the stream fd - Arch::X86_64 => emitter.instruction("mov QWORD PTR [rsp + 0], rax"), // save the stream fd - } - if has_len { - emit_expr(&args[1], emitter, ctx, data); // evaluate $length first (source order) - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("str x0, [sp, #8]"), // save the requested max byte count - Arch::X86_64 => emitter.instruction("mov QWORD PTR [rsp + 8], rax"), // save the requested max byte count - } - } - if has_off { - emit_expr(&args[2], emitter, ctx, data); // evaluate $offset after $length - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // a negative offset means "do not seek" - emitter.instruction(&format!("b.lt {}", skip_seek)); // skip the seek on a negative offset - emitter.instruction("mov x1, x0"); // offset → seek arg1 - emitter.instruction("mov x2, #0"); // whence = SEEK_SET - emitter.instruction("ldr x0, [sp, #0]"); // reload the fd → seek arg0 - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 - emitter.instruction("cmp x0, x9"); // synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrap_seek)); // wrapper: dispatch stream_seek - emitter.syscall(199); // lseek(fd, offset, SEEK_SET) - if emitter.platform.needs_cmp_before_error_branch() { - emitter.instruction("cmp x0, #0"); // Linux reports lseek failure as a negative result - } - emitter.instruction(&emitter.platform.branch_on_syscall_success(&skip_seek)); // continue only when lseek succeeded - emitter.instruction(&format!("b {}", seek_failed)); // seek failure makes stream_get_contents() return false - emitter.label(&wrap_seek); - abi::emit_call_label(emitter, "__rt_user_wrapper_fseek"); // wrapper stream_seek(offset, SEEK_SET) - emitter.instruction("cmp x0, #0"); // did the wrapper stream_seek report success? - emitter.instruction(&format!("b.ne {}", seek_failed)); // wrapper seek failure returns PHP false - emitter.label(&skip_seek); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // a negative offset means "do not seek" - emitter.instruction(&format!("jl {}", skip_seek)); // skip the seek on a negative offset - emitter.instruction("mov rsi, rax"); // offset → seek arg1 - emitter.instruction("mov rdx, 0"); // whence = SEEK_SET - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the fd → seek arg0 - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rdi, r9"); // synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrap_seek)); // wrapper: dispatch stream_seek - emitter.instruction("call lseek"); // lseek(fd, offset, SEEK_SET) - emitter.instruction("cmp rax, 0"); // did libc lseek return a non-negative offset? - emitter.instruction(&format!("jl {}", seek_failed)); // seek failure makes stream_get_contents() return false - emitter.instruction(&format!("jmp {}", skip_seek)); // normal fd seeked successfully - emitter.label(&wrap_seek); - abi::emit_call_label(emitter, "__rt_user_wrapper_fseek"); // wrapper stream_seek(offset, SEEK_SET) - emitter.instruction("cmp rax, 0"); // did the wrapper stream_seek report success? - emitter.instruction(&format!("jne {}", seek_failed)); // wrapper seek failure returns PHP false - emitter.label(&skip_seek); - } - } - } - if has_len { - // Positive finite length: fill up to $length bytes. Dynamic null/negative - // lengths take the read-all path below, matching PHP's default `-1`. - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp, #8]"); // reload max_len for the runtime unlimited check - emit_branch_if_unlimited_length(emitter, "x9", "x10", &read_all); - emitter.instruction("ldr x0, [sp, #0]"); // reload the fd for the bounded read - emitter.instruction("mov x1, x9"); // finite byte count for the bounded read - emitter.instruction("add sp, sp, #32"); // release the argument-evaluation frame - abi::emit_call_label(emitter, "__rt_stream_get_contents_bounded"); // loop through fread until the cap is filled or EOF - emit_box_current_value_as_mixed(emitter, &PhpType::Str); - } - Arch::X86_64 => { - emitter.instruction("mov r9, QWORD PTR [rsp + 8]"); // reload max_len for the runtime unlimited check - emit_branch_if_unlimited_length(emitter, "r9", "r10", &read_all); - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // reload the fd for the bounded read - emitter.instruction("mov rdi, rax"); // fd argument for the bounded helper - emitter.instruction("mov rsi, r9"); // finite byte count for the bounded read - emitter.instruction("add rsp, 32"); // release the argument-evaluation frame - abi::emit_call_label(emitter, "__rt_stream_get_contents_bounded"); // loop through fread until the cap is filled or EOF - emit_box_current_value_as_mixed(emitter, &PhpType::Str); - } - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", done)), // bounded positive length is complete - Arch::X86_64 => emitter.instruction(&format!("jmp {}", done)), // bounded positive length is complete - } - } else { - // $offset only: reload the fd and read every remaining byte. - emitter.label(&read_all); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #0]"); // reload the fd for the read-all path - emitter.instruction("add sp, sp, #32"); // release the frame - } - Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // reload the fd for the read-all path - emitter.instruction("add rsp, 32"); // release the frame - } - } - emit_read_all_from_fd(emitter, ctx); - emit_box_current_value_as_mixed(emitter, &PhpType::Str); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", done)), // successful read skips the seek-failure boxing path - Arch::X86_64 => emitter.instruction(&format!("jmp {}", done)), // successful read skips the seek-failure boxing path - } - } - if has_len { - emitter.label(&read_all); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #0]"); // reload the fd for unlimited-length reads - emitter.instruction("add sp, sp, #32"); // release the frame before the read-all path - } - Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // reload the fd for unlimited-length reads - emitter.instruction("add rsp, 32"); // release the frame before the read-all path - } - } - emit_read_all_from_fd(emitter, ctx); - emit_box_current_value_as_mixed(emitter, &PhpType::Str); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", done)), // successful read skips the seek-failure boxing path - Arch::X86_64 => emitter.instruction(&format!("jmp {}", done)), // successful read skips the seek-failure boxing path - } - } - emitter.label(&seek_failed); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("add sp, sp, #32"); // release the argument-evaluation frame after a failed seek - emitter.instruction("mov x0, #0"); // false payload = 0 - } - Arch::X86_64 => { - emitter.instruction("add rsp, 32"); // release the argument-evaluation frame after a failed seek - emitter.instruction("xor eax, eax"); // false payload = 0 - } - } - emit_box_current_value_as_mixed(emitter, &PhpType::Bool); - emitter.label(&done); - Some(PhpType::Mixed) -} - -/// Reads every remaining byte from the descriptor in the int-result register -/// (`x0`/`rax`) into an elephc string, returning the pointer/length in the -/// standard string registers (`x1`/`x2` on AArch64, `rax`/`rdx` on x86_64). -/// -/// A normal fd delegates to the TLS-aware `__rt_stream_get_contents` read-all -/// loop. A synthetic user-wrapper fd (`>= 0x40000000`) is drained by a -/// **feof-gated** compiled loop: each iteration checks `__rt_feof` first and -/// stops at EOF, then `__rt_fread`s one chunk and copies it into -/// `_user_wrapper_drain_buf`. Checking feof first mirrors the only safe drain -/// form (`while(!feof($f)) $b .= fread($f,N)`); a read-then-check-empty loop -/// forces an extra read at EOF whose empty `substr` result frees the caller's -/// resource cell. Each owned chunk is released via `__rt_decref_any`. -fn emit_read_all_from_fd(emitter: &mut Emitter, ctx: &mut Context) { - let wrapper_label = ctx.next_label("sgc_wrapper"); - let loop_label = ctx.next_label("sgc_wrap_loop"); - let copy_label = ctx.next_label("sgc_wrap_copy"); - let release_label = ctx.next_label("sgc_wrap_release"); - let release_eof_label = ctx.next_label("sgc_wrap_release_eof"); - let wdone_label = ctx.next_label("sgc_wrap_done"); - let done_label = ctx.next_label("sgc_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // high half of USER_WRAPPER_FD_BASE - emitter.instruction("lsl w9, w9, #16"); // form 0x40000000 in w9 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper_label)); // wrappers drain via the feof-gated fread loop below - abi::emit_call_label(emitter, "__rt_stream_get_contents"); // normal fd: TLS-aware read-all helper (x1=ptr, x2=len) - emitter.instruction(&format!("b {}", done_label)); // skip the wrapper loop on the normal path - - emitter.label(&wrapper_label); - emitter.instruction("sub sp, sp, #16"); // scratch: [sp,#0]=fd, [sp,#8]=accumulated total - emitter.instruction("str x0, [sp, #0]"); // save the synthetic wrapper fd - emitter.instruction("str xzr, [sp, #8]"); // accumulated byte total = 0 - emitter.label(&loop_label); - emitter.instruction("ldr x0, [sp, #0]"); // reload the wrapper fd - abi::emit_call_label(emitter, "__rt_feof"); // check the wrapper's stream_eof FIRST (x0 = 1 at EOF) - emitter.instruction(&format!("cbnz x0, {}", wdone_label)); // at EOF: stop WITHOUT reading (avoids the corrupting empty read) - emitter.instruction("ldr x0, [sp, #0]"); // reload the wrapper fd - emitter.instruction("mov x1, #4096"); // request up to 4096 bytes - abi::emit_call_label(emitter, "__rt_fread"); // compiled-context fread → x1=chunk ptr, x2=len - emitter.instruction(&format!("cbz x2, {}", release_eof_label)); // defensive: empty read also stops - emitter.instruction("ldr x9, [sp, #8]"); // current accumulated total - emitter.instruction("movz x10, #0x10, lsl #16"); // drain buffer capacity = 1 MiB - emitter.instruction("subs x10, x10, x9"); // remaining capacity - emitter.instruction(&format!("b.le {}", release_eof_label)); // buffer full: release the chunk, then finish - emitter.instruction("cmp x2, x10"); // does this chunk exceed the remaining capacity? - emitter.instruction("csel x2, x2, x10, ls"); // clamp the chunk to the remaining capacity - abi::emit_symbol_address(emitter, "x11", "_user_wrapper_drain_buf"); - emitter.instruction("add x11, x11, x9"); // destination = drain buffer + total - emitter.instruction("mov x12, #0"); // byte-copy index - emitter.label(©_label); - emitter.instruction("ldrb w13, [x1, x12]"); // load the next source byte - emitter.instruction("strb w13, [x11, x12]"); // store it into the drain buffer - emitter.instruction("add x12, x12, #1"); // advance the copy index - emitter.instruction("cmp x12, x2"); // copied the whole chunk yet? - emitter.instruction(&format!("b.lt {}", copy_label)); // keep copying until the chunk is done - emitter.instruction("ldr x9, [sp, #8]"); // reload the accumulated total - emitter.instruction("add x9, x9, x2"); // add the copied byte count - emitter.instruction("str x9, [sp, #8]"); // store the updated total - emitter.label(&release_label); - emitter.instruction("mov x0, x1"); // the owned wrapper stream_read result - abi::emit_call_label(emitter, "__rt_decref_any"); // release it, then loop back to the feof check - emitter.instruction(&format!("b {}", loop_label)); // read the next chunk - emitter.label(&release_eof_label); - emitter.instruction("mov x0, x1"); // the final (empty/uncopied) owned result - abi::emit_call_label(emitter, "__rt_decref_any"); // release it (heap strings freed; non-heap skipped) - emitter.label(&wdone_label); - abi::emit_symbol_address(emitter, "x1", "_user_wrapper_drain_buf"); // result string pointer - emitter.instruction("ldr x2, [sp, #8]"); // result length = accumulated total - emitter.instruction("add sp, sp, #16"); // release the scratch frame - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rax, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper_label)); // wrappers drain via the feof-gated fread loop below - emitter.instruction("mov rdi, rax"); // normal fd: pass the descriptor to the helper - abi::emit_call_label(emitter, "__rt_stream_get_contents"); // TLS-aware read-all helper (rax=ptr, rdx=len) - emitter.instruction(&format!("jmp {}", done_label)); // skip the wrapper loop on the normal path - - emitter.label(&wrapper_label); - emitter.instruction("sub rsp, 16"); // scratch: [rsp+0]=fd, [rsp+8]=accumulated total - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the synthetic wrapper fd - emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // accumulated byte total = 0 - emitter.label(&loop_label); - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the wrapper fd - abi::emit_call_label(emitter, "__rt_feof"); // check the wrapper's stream_eof FIRST (rax = 1 at EOF) - emitter.instruction("test rax, rax"); // at EOF? - emitter.instruction(&format!("jnz {}", wdone_label)); // at EOF: stop WITHOUT reading (avoids the corrupting empty read) - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // reload the wrapper fd - emitter.instruction("mov rsi, 4096"); // request up to 4096 bytes - abi::emit_call_label(emitter, "__rt_fread"); // compiled-context fread → rax=chunk ptr, rdx=len - emitter.instruction("test rdx, rdx"); // zero-length read? - emitter.instruction(&format!("jz {}", release_eof_label)); // defensive: empty read also stops - emitter.instruction("mov r8, QWORD PTR [rsp + 8]"); // current accumulated total - emitter.instruction("mov r9, 0x100000"); // drain buffer capacity = 1 MiB - emitter.instruction("sub r9, r8"); // remaining capacity - emitter.instruction(&format!("jle {}", release_eof_label)); // buffer full: release the chunk, then finish - emitter.instruction("cmp rdx, r9"); // does this chunk exceed the remaining capacity? - emitter.instruction("cmova rdx, r9"); // clamp the chunk to the remaining capacity - abi::emit_symbol_address(emitter, "r10", "_user_wrapper_drain_buf"); // drain buffer base - emitter.instruction("add r10, r8"); // destination = drain buffer + total - emitter.instruction("xor rcx, rcx"); // byte-copy index - emitter.label(©_label); - emitter.instruction("mov r11b, BYTE PTR [rax + rcx]"); // load the next source byte - emitter.instruction("mov BYTE PTR [r10 + rcx], r11b"); // store it into the drain buffer - emitter.instruction("inc rcx"); // advance the copy index - emitter.instruction("cmp rcx, rdx"); // copied the whole chunk yet? - emitter.instruction(&format!("jl {}", copy_label)); // keep copying until the chunk is done - emitter.instruction("mov r8, QWORD PTR [rsp + 8]"); // reload the accumulated total - emitter.instruction("add r8, rdx"); // add the copied byte count - emitter.instruction("mov QWORD PTR [rsp + 8], r8"); // store the updated total - emitter.label(&release_label); - abi::emit_call_label(emitter, "__rt_decref_any"); // release the owned chunk (rax=ptr), then loop - emitter.instruction(&format!("jmp {}", loop_label)); // read the next chunk - emitter.label(&release_eof_label); - abi::emit_call_label(emitter, "__rt_decref_any"); // release the final (empty/uncopied) result (rax=ptr) - emitter.label(&wdone_label); - abi::emit_symbol_address(emitter, "rax", "_user_wrapper_drain_buf"); // result string pointer - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // result length = accumulated total - emitter.instruction("add rsp, 16"); // release the scratch frame - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/stream_get_line.rs b/src/codegen/builtins/io/stream_get_line.rs deleted file mode 100644 index 5e31deadd3..0000000000 --- a/src/codegen/builtins/io/stream_get_line.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_get_line` calls. -//! Reads from a stream up to a byte budget or an ending delimiter. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Marshals the descriptor, length, and optional ending delimiter into the -//! four `__rt_stream_get_line` argument registers; the delimiter is consumed -//! and stripped from the returned string. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `stream_get_line()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_get_line()"); - emit_stream_fd_arg("stream_get_line", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the maximum length - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, x2"); // ending-delimiter length into argument 3 - emitter.instruction("mov x2, x1"); // ending-delimiter pointer into argument 2 - abi::emit_pop_reg(emitter, "x1"); // maximum length into argument 1 - abi::emit_pop_reg(emitter, "x0"); // descriptor into argument 0 - } - Arch::X86_64 => { - emitter.instruction("mov rcx, rdx"); // ending-delimiter length into argument 3 - emitter.instruction("mov rdx, rax"); // ending-delimiter pointer into argument 2 - abi::emit_pop_reg(emitter, "rsi"); // maximum length into argument 1 - abi::emit_pop_reg(emitter, "rdi"); // descriptor into argument 0 - } - } - } else { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x2, #0"); // no ending-delimiter pointer - emitter.instruction("mov x3, #0"); // no ending-delimiter length - abi::emit_pop_reg(emitter, "x1"); // maximum length into argument 1 - abi::emit_pop_reg(emitter, "x0"); // descriptor into argument 0 - } - Arch::X86_64 => { - emitter.instruction("xor edx, edx"); // no ending-delimiter pointer - emitter.instruction("xor ecx, ecx"); // no ending-delimiter length - abi::emit_pop_reg(emitter, "rsi"); // maximum length into argument 1 - abi::emit_pop_reg(emitter, "rdi"); // descriptor into argument 0 - } - } - } - abi::emit_call_label(emitter, "__rt_stream_get_line"); - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/stream_get_meta_data.rs b/src/codegen/builtins/io/stream_get_meta_data.rs deleted file mode 100644 index e2a4e74494..0000000000 --- a/src/codegen/builtins/io/stream_get_meta_data.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_get_meta_data` calls. -//! Yields the metadata associative array describing an open stream resource. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The descriptor is unboxed from the stream resource and handed to the -//! `__rt_stream_get_meta_data` runtime helper, which builds a `{string => -//! mixed}` hash. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `stream_get_meta_data()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_get_meta_data()"); - emit_stream_fd_arg("stream_get_meta_data", &args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // descriptor into the runtime-helper argument register - } - abi::emit_call_label(emitter, "__rt_stream_get_meta_data"); - Some(PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Mixed), - }) -} diff --git a/src/codegen/builtins/io/stream_introspection.rs b/src/codegen/builtins/io/stream_introspection.rs deleted file mode 100644 index a848f51272..0000000000 --- a/src/codegen/builtins/io/stream_introspection.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! Purpose: -//! Emits the PHP stream-introspection stub builtins `stream_is_local`, -//! `stream_supports_lock`, `stream_get_wrappers`, `stream_get_transports`, -//! and `stream_get_filters`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Argument expressions are still evaluated so their side effects and any -//! resource TypeError stay observable; the returned values are fixed. - -use crate::codegen::builtins::io::stream_arg::emit_stream_fd_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::span::Span; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_introspection()` stream and I/O builtin calls. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "stream_supports_lock" => emit_true(name, &args[0], true, emitter, ctx, data), - "stream_is_local" => emit_true(name, &args[0], false, emitter, ctx, data), - "stream_get_wrappers" => emit_string_array( - // Static list of built-in wrappers compiled into the runtime. - // User wrappers registered through stream_wrapper_register are - // not surfaced here in v1 — PHP code that needs to enumerate - // them would have to track them application-side. - &[ - "file", "php", "data", "ftp", "http", "https", "ftps", - "compress.zlib", "compress.bzip2", "phar", "glob", - ], - emitter, - ctx, - data, - ), - "stream_get_filters" => emit_string_array( - // Built-in filters. User filters registered via - // stream_filter_register are not yet enumerated here. - &[ - "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", - ], - emitter, - ctx, - data, - ), - "stream_get_transports" => emit_string_array( - // Transports recognised by stream_socket_client / server. `tls` - // and `ssl` are available through stream_socket_enable_crypto - // promoting a connected tcp:// socket, so they belong in this - // list per PHP's conventions. tlsv1.x / sslv2 / sslv3 are - // surfaced as aliases — they all route through the same - // openssl-backed enable_crypto path with default version - // negotiation. - &[ - "tcp", "udp", "unix", "udg", - "tls", "ssl", "sslv2", "sslv3", - "tlsv1.0", "tlsv1.1", "tlsv1.2", "tlsv1.3", - ], - emitter, - ctx, - data, - ), - _ => None, - } -} - -/// Evaluates the argument (preserving side effects) and yields a fixed `true`. -fn emit_true( - name: &str, - arg: &Expr, - validate_resource: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}()", name)); - if validate_resource { - emit_stream_fd_arg(name, arg, emitter, ctx, data); - } else { - emit_expr(arg, emitter, ctx, data); - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // every elephc stream satisfies this predicate - } - Arch::X86_64 => { - emitter.instruction("mov rax, 1"); // every elephc stream satisfies this predicate - } - } - Some(PhpType::Bool) -} - -/// Builds an indexed PHP array of string literals by lowering a synthesized -/// `ArrayLiteral`, reusing the regular array-literal codegen path. -fn emit_string_array( - items: &[&str], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let elements: Vec = items.iter().map(|item| Expr::string_lit(*item)).collect(); - let array = Expr::new(ExprKind::ArrayLiteral(elements), Span::dummy()); - emit_expr(&array, emitter, ctx, data); - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/io/stream_isatty.rs b/src/codegen/builtins/io/stream_isatty.rs deleted file mode 100644 index 404f2bdd0c..0000000000 --- a/src/codegen/builtins/io/stream_isatty.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_isatty` calls. -//! Resolves the stream resource to its descriptor and asks the runtime whether it is a terminal. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Delegates the `ioctl` terminal probe to the `__rt_stream_isatty` runtime helper. - -use crate::codegen::abi; -use crate::codegen::builtins::io::stream_arg::emit_stream_fd_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_isatty()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_isatty()"); - // Resolve the stream resource to its underlying file descriptor; the helper - // validates the argument and leaves the descriptor in the result register. - emit_stream_fd_arg("stream_isatty", &args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the descriptor into the runtime-helper argument register - } - abi::emit_call_label(emitter, "__rt_stream_isatty"); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_notification.rs b/src/codegen/builtins/io/stream_notification.rs deleted file mode 100644 index cd7bd1102e..0000000000 --- a/src/codegen/builtins/io/stream_notification.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Purpose: -//! Captures a stream context's `notification` callback at codegen time into the -//! `_stream_notification_callback` global so `__rt_http_open` can fire it at the -//! `STREAM_NOTIFY_*` transfer milestones. Shared by `stream_context_create` and -//! `stream_context_set_params`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::stream_context_create::emit()`. -//! - `crate::codegen::builtins::io::stream_context_set_params::emit()`. -//! -//! Key details: -//! - v1 captures ONLY a literal `['notification' => ]` entry of a literal params array. The captured value must be an -//! expression that evaluates to a callable descriptor (closures and -//! first-class callables do); a string / `[object, method]` / variable -//! callback does not produce a descriptor with the invoker at -//! `CALLABLE_DESC_INVOKER_OFFSET`, so it is not fired in v1 and the global is -//! cleared instead. The single-global model matches `_stream_context_options` -//! (one active context at a time). -//! - The captured descriptor is retained via `emit_retain_current_descriptor` -//! (a null/rodata-safe incref) so the global slot owns a reference that -//! survives the surrounding owner's scope-exit cleanup. -//! - The params expression is always emitted for its full side effects before -//! capture, preserving the prior `emit_expr(&args[..])`-for-side-effects -//! behavior; a capturable closure entry is then re-emitted to materialize the -//! descriptor stored into the global. - -use crate::codegen::abi; -use crate::codegen::callable_descriptor::emit_retain_current_descriptor; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::{Expr, ExprKind}; - -/// Emits the params expression for side effects, then captures a literal -/// `notification` closure / first-class callable into -/// `_stream_notification_callback` (or clears the global when none is present). -/// -/// `params` is the optional second argument of `stream_context_create` / the -/// second argument of `stream_context_set_params`. When `params` is `None` -/// (omitted) the global is left untouched, so a bare `stream_context_create([])` -/// does not disturb a previously registered callback. -pub(super) fn capture_notification_callback( - params: Option<&Expr>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let Some(params) = params else { - return; - }; - - // Evaluate the full params expression for its side effects (and to build the - // array), matching the prior side-effect-only behavior. The result is discarded. - emit_expr(params, emitter, ctx, data); - - if let ExprKind::ArrayLiteralAssoc(entries) = ¶ms.kind { - if let Some(value) = find_notification_value(entries) { - if value_is_capturable_callable(value) { - // Re-emit the callable to materialize a fresh descriptor in the - // result register, retain it for the global, and store it. - emit_expr(value, emitter, ctx, data); - emit_retain_current_descriptor(emitter); - emit_store_descriptor_to_global(emitter); - return; - } - } - } - - // No capturable notification closure → ensure a stale callback is cleared so - // a later HTTP transfer on this context does not fire a previous callback. - emit_clear_notification_global(emitter); -} - -/// Returns the value expression for the last literal `'notification'` key in an -/// associative-array literal (PHP last-wins for duplicate keys), or `None`. -fn find_notification_value(entries: &[(Expr, Expr)]) -> Option<&Expr> { - let mut found = None; - for (key, value) in entries { - if let ExprKind::StringLiteral(name) = &key.kind { - if name == "notification" { - found = Some(value); - } - } - } - found -} - -/// Returns true when `value` is a literal closure or first-class callable, the -/// expression kinds that reliably evaluate to a callable descriptor with an -/// invoker at `CALLABLE_DESC_INVOKER_OFFSET`. -fn value_is_capturable_callable(value: &Expr) -> bool { - matches!( - value.kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ) -} - -/// Stores the callable descriptor currently in the integer result register into -/// the `_stream_notification_callback` global. -fn emit_store_descriptor_to_global(emitter: &mut Emitter) { - let addr_reg = abi::symbol_scratch_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - abi::emit_symbol_address(emitter, addr_reg, "_stream_notification_callback"); - abi::emit_store_to_address(emitter, result_reg, addr_reg, 0); -} - -/// Clears the `_stream_notification_callback` global so no callback is fired. -fn emit_clear_notification_global(emitter: &mut Emitter) { - let addr_reg = abi::symbol_scratch_reg(emitter); - let zero_reg = abi::secondary_scratch_reg(emitter); - abi::emit_symbol_address(emitter, addr_reg, "_stream_notification_callback"); - abi::emit_load_int_immediate(emitter, zero_reg, 0); - abi::emit_store_to_address(emitter, zero_reg, addr_reg, 0); -} diff --git a/src/codegen/builtins/io/stream_resolve_include_path.rs b/src/codegen/builtins/io/stream_resolve_include_path.rs deleted file mode 100644 index e857eba1a6..0000000000 --- a/src/codegen/builtins/io/stream_resolve_include_path.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_resolve_include_path($filename)` calls. Resolves a -//! filename through PHP's `include_path` and returns the resolved path, -//! or false if the file does not exist on any include_path entry. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - elephc has no runtime `include_path` (includes are pre-resolved at -//! compile time), so this builtin is functionally equivalent to -//! `realpath($filename)`: if the path resolves on disk, return its -//! canonical form; otherwise return Mixed(false). -//! - Return type is `Mixed` because PHP's contract is `string|false`. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_resolve_include_path()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_resolve_include_path()"); - // Evaluate the filename arg → string result (x1/x2 on ARM64, rax/rdx on x86_64). - emit_expr(&args[0], emitter, ctx, data); - let is_false = ctx.next_label("srip_false"); - let done = ctx.next_label("srip_done"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_call_label(emitter, "__rt_realpath"); // x1/x2 = canonical path or empty for false - emitter.instruction(&format!("cbz x2, {}", is_false)); // len 0 → false - // Mixed(string) - emitter.instruction("mov x0, #1"); // tag = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("b {}", done)); // continue at target label - emitter.label(&is_false); - emitter.instruction("mov x0, #3"); // tag = bool - emitter.instruction("mov x1, #0"); // value = false - emitter.instruction("mov x2, #0"); // prepare AArch64 call argument - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done); - } - Arch::X86_64 => { - // String-result pair is in rax/rdx; realpath helper takes the - // same pair as input on x86_64 too. - abi::emit_call_label(emitter, "__rt_realpath"); // rax/rdx = canonical or empty - emitter.instruction("test rdx, rdx"); // check whether the runtime value is zero - emitter.instruction(&format!("jz {}", is_false)); // branch when the checked value is zero or equal - // Mixed(string): __rt_mixed_from_value takes (rax=tag, rdi=lo, rsi=hi). - emitter.instruction("mov rdi, rax"); // string ptr → payload lo - emitter.instruction("mov rsi, rdx"); // string len → payload hi - emitter.instruction("mov rax, 1"); // tag = string - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("jmp {}", done)); // continue at target label - emitter.label(&is_false); - emitter.instruction("xor edi, edi"); // clear register value - emitter.instruction("xor esi, esi"); // clear register value - emitter.instruction("mov rax, 3"); // tag = bool, value 0 = false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done); - } - } - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/stream_select.rs b/src/codegen/builtins/io/stream_select.rs deleted file mode 100644 index cc4e0321a1..0000000000 --- a/src/codegen/builtins/io/stream_select.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_select` calls. -//! Waits for readiness across three resource arrays and reports the ready count. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The three array arguments are by-reference: `__rt_stream_select` compacts -//! each in place to its ready subset (no reallocation), so the caller's -//! variables observe the result without a pointer write-back. -//! - Arguments are evaluated in source order, then materialized into the -//! five `__rt_stream_select` argument registers. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_select()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_select()"); - let result = abi::int_result_reg(emitter); - - // -- evaluate the five arguments in source order, preserving each -- - emit_expr(&args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, result); // preserve the read array - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, result); // preserve the write array - emit_expr(&args[2], emitter, ctx, data); - abi::emit_push_reg(emitter, result); // preserve the except array - emit_expr(&args[3], emitter, ctx, data); - abi::emit_push_reg(emitter, result); // preserve the seconds timeout - if args.len() >= 5 { - emit_expr(&args[4], emitter, ctx, data); - } else { - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #0"), // omitted microseconds default to 0 - Arch::X86_64 => emitter.instruction("xor eax, eax"), // omitted microseconds default to 0 - } - } - - // -- materialize the arguments into the runtime-helper registers -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x4, x0"); // microseconds become the fifth helper argument - abi::emit_pop_reg(emitter, "x3"); // seconds become the fourth helper argument - abi::emit_pop_reg(emitter, "x2"); // except array becomes the third helper argument - abi::emit_pop_reg(emitter, "x1"); // write array becomes the second helper argument - abi::emit_pop_reg(emitter, "x0"); // read array becomes the first helper argument - } - Arch::X86_64 => { - emitter.instruction("mov r8, rax"); // microseconds become the fifth SysV helper argument - abi::emit_pop_reg(emitter, "rcx"); // seconds become the fourth SysV helper argument - abi::emit_pop_reg(emitter, "rdx"); // except array becomes the third SysV helper argument - abi::emit_pop_reg(emitter, "rsi"); // write array becomes the second SysV helper argument - abi::emit_pop_reg(emitter, "rdi"); // read array becomes the first SysV helper argument - } - } - abi::emit_call_label(emitter, "__rt_stream_select"); - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/stream_set_blocking.rs b/src/codegen/builtins/io/stream_set_blocking.rs deleted file mode 100644 index 339e2fcf75..0000000000 --- a/src/codegen/builtins/io/stream_set_blocking.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_set_blocking` calls. -//! Toggles a stream's blocking mode through the runtime fcntl helper, or — for a -//! synthetic userspace-wrapper descriptor — through the wrapper's -//! `stream_set_option(STREAM_OPTION_BLOCKING, $mode, 0)`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Unboxes the stream resource to its descriptor, evaluates the blocking -//! flag, and delegates to `__rt_stream_set_blocking` (fcntl) for a normal fd. -//! - A descriptor `>= USER_WRAPPER_FD_BASE` (0x40000000) is a userspace wrapper -//! handle, so the call is routed to `__rt_user_wrapper_set_option` (vtable slot -//! 13) with option `STREAM_OPTION_BLOCKING` and the blocking flag as `$arg1`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// PHP `STREAM_OPTION_BLOCKING` option value passed to `stream_set_option`. -const STREAM_OPTION_BLOCKING: usize = 1; - -/// Emits the `stream_set_blocking(resource $stream, bool $enable)` builtin. -/// -/// Materializes the descriptor (x0 / rdi) and the blocking flag (x1 / rsi), then -/// dispatches: a synthetic wrapper fd (`>= 0x40000000`) calls -/// `__rt_user_wrapper_set_option(fd, STREAM_OPTION_BLOCKING, flag, 0)`; any other -/// fd calls the libc `__rt_stream_set_blocking(fd, flag)`. Returns `PhpType::Bool`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_set_blocking()"); - emit_stream_fd_arg("stream_set_blocking", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - emit_expr(&args[1], emitter, ctx, data); - let wrapper = ctx.next_label("set_blocking_wrapper"); - let after = ctx.next_label("set_blocking_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // blocking flag into the second helper argument - abi::emit_pop_reg(emitter, "x0"); // descriptor into the first helper argument - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper)); // dispatch into the wrapper's stream_set_option - abi::emit_call_label(emitter, "__rt_stream_set_blocking"); // normal fd: fcntl O_NONBLOCK toggle - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov x2, x1"); // arg1 = blocking flag - emitter.instruction(&format!("mov x1, #{}", STREAM_OPTION_BLOCKING)); // option = STREAM_OPTION_BLOCKING - emitter.instruction("mov x3, #0"); // arg2 = 0 (unused for blocking) - abi::emit_call_label(emitter, "__rt_user_wrapper_set_option"); // call the wrapper's stream_set_option($option, $arg1, $arg2) - emitter.label(&after); - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // blocking flag into the second SysV argument - abi::emit_pop_reg(emitter, "rdi"); // descriptor into the first SysV argument - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rdi, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper)); // dispatch into the wrapper's stream_set_option - abi::emit_call_label(emitter, "__rt_stream_set_blocking"); // normal fd: fcntl O_NONBLOCK toggle - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rdx, rsi"); // arg1 = blocking flag - emitter.instruction(&format!("mov rsi, {}", STREAM_OPTION_BLOCKING)); // option = STREAM_OPTION_BLOCKING - emitter.instruction("xor ecx, ecx"); // arg2 = 0 (unused for blocking) - abi::emit_call_label(emitter, "__rt_user_wrapper_set_option"); // call the wrapper's stream_set_option($option, $arg1, $arg2) - emitter.label(&after); - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_set_buffer.rs b/src/codegen/builtins/io/stream_set_buffer.rs deleted file mode 100644 index 6f6d277da6..0000000000 --- a/src/codegen/builtins/io/stream_set_buffer.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_set_chunk_size` / `stream_set_read_buffer` / -//! `stream_set_write_buffer` calls. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - `stream_set_chunk_size($stream, $size): int` tracks a per-fd chunk size in -//! the `_stream_chunk_size` table (indexed by raw fd up to 256, default 8192) -//! and returns the PREVIOUS value — PHP's observable contract for save/restore -//! patterns. Out-of-range / synthetic fds report the default and are not -//! stored. The size does not currently change read granularity (reads return -//! identical data); only the returned previous value is meaningful. -//! - `stream_set_read_buffer` / `stream_set_write_buffer` return `0` ("success"). -//! elephc streams are unbuffered (direct read/write syscalls), so the buffer -//! size has no effect — `0` is the correct PHP result for an unbuffered stream -//! (`stream_set_write_buffer($s, 0)` is exactly the unbuffered mode elephc uses). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `stream_set_buffer()` stream and I/O builtin calls. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}()", name)); - if name == "stream_set_chunk_size" && args.len() == 2 { - return emit_chunk_size(args, emitter, ctx, data); - } - // stream_set_read_buffer / stream_set_write_buffer: evaluate args for side - // effects and report success (0). elephc streams are unbuffered. - for arg in args { - emit_expr(arg, emitter, ctx, data); - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #0"), // return 0 (success — elephc streams are unbuffered) - Arch::X86_64 => emitter.instruction("xor eax, eax"), // return 0 (success — elephc streams are unbuffered) - } - Some(PhpType::Int) -} - -/// Emits `stream_set_chunk_size($stream, $size)`: store `$size` in the per-fd -/// `_stream_chunk_size` table and return the previous chunk size (default 8192). -/// Out-of-range fds report 8192 without storing. -fn emit_chunk_size( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let default_label = ctx.next_label("scs_default"); - let done_label = ctx.next_label("scs_done"); - // Unique label: a program may call stream_set_chunk_size more than once, so - // a fixed label would be defined twice and fail to assemble. - let have_old_label = ctx.next_label("scs_have_old"); - - // -- fd from the stream arg, preserved across the size evaluation -- - emit_stream_fd_arg("stream_set_chunk_size", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save fd across the $size evaluation - emit_expr(&args[1], emitter, ctx, data); // $size → result reg - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // x1 = new chunk size - abi::emit_pop_reg(emitter, "x2"); // x2 = fd - emitter.instruction("cmp x2, #0"); // negative fd? - emitter.instruction(&format!("b.lt {}", default_label)); // → report the default without storing - emitter.instruction("cmp x2, #256"); // fd outside the per-fd table? - emitter.instruction(&format!("b.ge {}", default_label)); // → report the default without storing - abi::emit_symbol_address(emitter, "x9", "_stream_chunk_size"); - emitter.instruction("ldr x10, [x9, x2, lsl #3]"); // x10 = previous chunk size (0 = unset) - emitter.instruction(&format!("cbnz x10, {}", have_old_label)); // a stored value exists → use it - emitter.instruction("mov x10, #8192"); // unset → PHP default chunk size - emitter.label(&have_old_label); - emitter.instruction("str x1, [x9, x2, lsl #3]"); // store the new chunk size for this fd - emitter.instruction("mov x0, x10"); // return the previous chunk size - emitter.instruction(&format!("b {}", done_label)); // continue at target label - emitter.label(&default_label); - emitter.instruction("mov x0, #8192"); // out-of-range fd → report the default chunk size - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // rsi = new chunk size - abi::emit_pop_reg(emitter, "rdi"); // rdi = fd - emitter.instruction("cmp rdi, 0"); // negative fd? - emitter.instruction(&format!("jl {}", default_label)); // → report the default without storing - emitter.instruction("cmp rdi, 256"); // fd outside the per-fd table? - emitter.instruction(&format!("jge {}", default_label)); // → report the default without storing - abi::emit_symbol_address(emitter, "r9", "_stream_chunk_size"); // base of the per-fd chunk-size table - emitter.instruction("mov rax, QWORD PTR [r9 + rdi * 8]"); // rax = previous chunk size (0 = unset) - emitter.instruction("test rax, rax"); // a stored value exists? - emitter.instruction(&format!("jnz {}", have_old_label)); // → use it - emitter.instruction("mov eax, 8192"); // unset → PHP default chunk size - emitter.label(&have_old_label); - emitter.instruction("mov QWORD PTR [r9 + rdi * 8], rsi"); // store the new chunk size for this fd - emitter.instruction(&format!("jmp {}", done_label)); // rax holds the previous chunk size - emitter.label(&default_label); - emitter.instruction("mov eax, 8192"); // out-of-range fd → report the default chunk size - emitter.label(&done_label); - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/stream_set_timeout.rs b/src/codegen/builtins/io/stream_set_timeout.rs deleted file mode 100644 index 8363aa8d98..0000000000 --- a/src/codegen/builtins/io/stream_set_timeout.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_set_timeout()` calls. -//! Sets a stream's read timeout through the runtime helper, or — for a synthetic -//! userspace-wrapper descriptor — through the wrapper's -//! `stream_set_option(STREAM_OPTION_READ_TIMEOUT, $seconds, $microseconds)`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Marshals the descriptor, seconds, and optional microseconds, and calls -//! `__rt_stream_set_timeout` (setsockopt SO_RCVTIMEO) for a normal fd. The -//! microseconds argument defaults to 0 when omitted. -//! - A descriptor `>= USER_WRAPPER_FD_BASE` (0x40000000) is a userspace wrapper -//! handle, so the call is routed to `__rt_user_wrapper_set_option` (vtable slot -//! 13) with option `STREAM_OPTION_READ_TIMEOUT`, the seconds as `$arg1`, and -//! the microseconds as `$arg2`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// PHP `STREAM_OPTION_READ_TIMEOUT` option value passed to `stream_set_option`. -const STREAM_OPTION_READ_TIMEOUT: usize = 4; - -/// Emits the `stream_set_timeout(resource $stream, int $seconds, int $usec = 0)` -/// builtin. -/// -/// Materializes the descriptor, seconds, and (optional) microseconds, then -/// dispatches: a synthetic wrapper fd (`>= 0x40000000`) calls -/// `__rt_user_wrapper_set_option(fd, STREAM_OPTION_READ_TIMEOUT, seconds, usec)`; -/// any other fd calls the libc `__rt_stream_set_timeout(fd, seconds, usec)`. -/// Returns `PhpType::Bool`. -/// -/// Register layout at the dispatch point (after the args are materialized): -/// AArch64 fd=x0, seconds=x1, usec=x2; x86_64 fd=rdi, seconds=rsi, usec=rdx. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_set_timeout()"); - emit_stream_fd_arg("stream_set_timeout", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the seconds value - // -- materialize fd / seconds / usec into the libc-call registers -- - match emitter.target.arch { - Arch::AArch64 => { - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction("mov x2, x0"); // microseconds into argument 2 - } else { - emitter.instruction("mov x2, #0"); // no microseconds argument: default to 0 - } - abi::emit_pop_reg(emitter, "x1"); // seconds into argument 1 - abi::emit_pop_reg(emitter, "x0"); // descriptor into argument 0 - } - Arch::X86_64 => { - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction("mov rdx, rax"); // microseconds into argument 2 - } else { - emitter.instruction("xor edx, edx"); // no microseconds argument: default to 0 - } - abi::emit_pop_reg(emitter, "rsi"); // seconds into argument 1 - abi::emit_pop_reg(emitter, "rdi"); // descriptor into argument 0 - } - } - // -- dispatch: synthetic wrapper fd → stream_set_option, else libc setsockopt -- - let wrapper = ctx.next_label("set_timeout_wrapper"); - let after = ctx.next_label("set_timeout_after"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w9, #0x4000"); // load the high half of USER_WRAPPER_FD_BASE = 0x40000000 - emitter.instruction("lsl w9, w9, #16"); // shift into bits 30..16 to form 0x40000000 - emitter.instruction("cmp x0, x9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("b.ge {}", wrapper)); // dispatch into the wrapper's stream_set_option - abi::emit_call_label(emitter, "__rt_stream_set_timeout"); // normal fd: setsockopt SO_RCVTIMEO(fd, seconds, usec) - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - // remap libc (fd=x0, sec=x1, usec=x2) → set_option(fd, option, arg1=sec, arg2=usec) - emitter.instruction("mov x3, x2"); // arg2 = microseconds - emitter.instruction("mov x2, x1"); // arg1 = seconds - emitter.instruction(&format!("mov x1, #{}", STREAM_OPTION_READ_TIMEOUT)); // option = STREAM_OPTION_READ_TIMEOUT - abi::emit_call_label(emitter, "__rt_user_wrapper_set_option"); // call the wrapper's stream_set_option($option, $arg1, $arg2) - emitter.label(&after); - } - Arch::X86_64 => { - emitter.instruction("mov r9d, 0x40000000"); // USER_WRAPPER_FD_BASE - emitter.instruction("cmp rdi, r9"); // is this a synthetic user-wrapper fd? - emitter.instruction(&format!("jge {}", wrapper)); // dispatch into the wrapper's stream_set_option - abi::emit_call_label(emitter, "__rt_stream_set_timeout"); // normal fd: setsockopt SO_RCVTIMEO(fd, seconds, usec) - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - // remap libc (fd=rdi, sec=rsi, usec=rdx) → set_option(fd, option, arg1=sec, arg2=usec) - emitter.instruction("mov rcx, rdx"); // arg2 = microseconds - emitter.instruction("mov rdx, rsi"); // arg1 = seconds - emitter.instruction(&format!("mov rsi, {}", STREAM_OPTION_READ_TIMEOUT)); // option = STREAM_OPTION_READ_TIMEOUT - abi::emit_call_label(emitter, "__rt_user_wrapper_set_option"); // call the wrapper's stream_set_option($option, $arg1, $arg2) - emitter.label(&after); - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_socket_accept.rs b/src/codegen/builtins/io/stream_socket_accept.rs deleted file mode 100644 index 465362a38a..0000000000 --- a/src/codegen/builtins/io/stream_socket_accept.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_accept` calls. -//! Accepts a pending connection on a listening socket, optionally with a -//! timeout, and captures the peer address for the by-reference -//! `$peer_name` out-parameter. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Marshals (fd, timeout_us) into the helper. A missing/null timeout -//! becomes `-1`, signalling an infinite wait. A numeric timeout is -//! multiplied by `1_000_000` so the helper can call `select()` / -//! `pselect6()` with a single integer microsecond argument. -//! - The accepted descriptor (or `-1`) is boxed by the shared -//! `box_socket_result` helper into a Mixed cell. When the caller passed -//! a `&$peer_name` variable the address stashed in -//! `_accept_peer_ptr` / `_accept_peer_len` is copied into its slot. - -use crate::codegen::builtins::io::stream_arg::emit_stream_fd_arg; -use crate::codegen::builtins::io::stream_socket_server::box_socket_result; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_socket_accept()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_accept()"); - emit_stream_fd_arg("stream_socket_accept", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - emit_timeout_us(args.get(1), emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // timeout_us into argument 1 - abi::emit_pop_reg(emitter, "x0"); // descriptor into argument 0 - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // timeout_us into argument 1 - abi::emit_pop_reg(emitter, "rdi"); // descriptor into argument 0 - } - } - abi::emit_call_label(emitter, "__rt_stream_socket_accept"); - box_socket_result(emitter, ctx); - if let Some(peer_arg) = args.get(2) { - emit_store_peer_name(peer_arg, emitter, ctx); - } - Some(PhpType::Mixed) -} - -/// Evaluates the optional timeout argument and leaves an i64 microsecond -/// count in the int result register. A missing or `null` timeout lowers to -/// `-1` (the helper's "infinite wait" sentinel). A numeric timeout is taken -/// as a count of seconds and converted to microseconds with a 1_000_000 -/// multiplier so PHP code can pass either an int or a float-shaped int. -fn emit_timeout_us( - timeout: Option<&Expr>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let infinite_sentinel = |emitter: &mut Emitter| match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #-1"), // sentinel: infinite wait - Arch::X86_64 => emitter.instruction("mov rax, -1"), // sentinel: infinite wait - }; - let Some(expr) = timeout else { - infinite_sentinel(emitter); - return; - }; - if matches!(&expr.kind, ExprKind::Null) { - infinite_sentinel(emitter); - return; - } - emit_expr(expr, emitter, ctx, data); - // PHP exposes the timeout as a (possibly fractional) seconds value; elephc - // ints round toward zero, which matches PHP integer behaviour when the - // caller passes (int)$timeout. Scale to microseconds for the helper. - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x9, #0x4240"); // low 16 bits of 1_000_000 (0xF4240) - emitter.instruction("movk x9, #0xF, lsl #16"); // upper bits make x9 = 1_000_000 (one second in us) - emitter.instruction("mul x0, x0, x9"); // timeout_us = timeout_sec * 1_000_000 - } - Arch::X86_64 => { - emitter.instruction("imul rax, rax, 1000000"); // timeout_us = timeout_sec * 1_000_000 - } - } -} - -/// Copies the peer address (stashed by `__rt_stream_socket_accept` in the -/// `_accept_peer_*` globals) into the by-reference `$peer_name` variable. -/// Modelled on `stream_socket_recvfrom`'s storage-class dispatch. -fn emit_store_peer_name(arg: &Expr, emitter: &mut Emitter, ctx: &mut Context) { - let ExprKind::Variable(name) = &arg.kind else { - return; - }; - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, "x0"); // preserve the boxed accept result - abi::emit_symbol_address(emitter, "x9", "_accept_peer_ptr"); - emitter.instruction("ldr x10, [x9]"); // load the stashed peer address pointer - abi::emit_symbol_address(emitter, "x9", "_accept_peer_len"); - emitter.instruction("ldr x11, [x9]"); // load the stashed peer address length - emit_store_peer_slot(name, emitter, ctx); - abi::emit_pop_reg(emitter, "x0"); // restore the boxed accept result - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the boxed accept result - abi::emit_symbol_address(emitter, "r9", "_accept_peer_ptr"); // address of the stashed-pointer global - emitter.instruction("mov r10, QWORD PTR [r9]"); // load the stashed peer address pointer - abi::emit_symbol_address(emitter, "r9", "_accept_peer_len"); // address of the stashed-length global - emitter.instruction("mov r11, QWORD PTR [r9]"); // load the stashed peer address length - emit_store_peer_slot(name, emitter, ctx); - abi::emit_pop_reg(emitter, "rax"); // restore the boxed accept result - } - } - ctx.update_var_type_and_ownership(name, PhpType::Str, HeapOwnership::Owned); -} - -/// Stores the peer address (pointer in x10/r10, length in x11/r11) into the -/// `$peer_name` variable's 16-byte string slot, dispatching on storage class. -fn emit_store_peer_slot(name: &str, emitter: &mut Emitter, ctx: &Context) { - let is_global = - ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)); - if is_global { - let label = format!("_gvar_{}", name); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", &label); // load page of the global address variable - emitter.instruction("str x10, [x9]"); // store the address string pointer - emitter.instruction("str x11, [x9, #8]"); // store the address string length - } - Arch::X86_64 => { - abi::emit_store_reg_to_symbol(emitter, "r10", &label, 0); // store the address string pointer - abi::emit_store_reg_to_symbol(emitter, "r11", &label, 8); // store the address string length - } - } - return; - } - if ctx.ref_params.contains(name) { - let offset = ctx - .variables - .get(name) - .expect("codegen bug: missing ref-param slot for accept $peer_name") - .stack_offset; - match emitter.target.arch { - Arch::AArch64 => { - abi::load_at_offset(emitter, "x9", offset); // load the referenced address storage pointer - emitter.instruction("str x10, [x9]"); // store the address string pointer - emitter.instruction("str x11, [x9, #8]"); // store the address string length - } - Arch::X86_64 => { - abi::load_at_offset(emitter, "r9", offset); // load the referenced address storage pointer - abi::emit_store_to_address(emitter, "r10", "r9", 0); // store the address string pointer - abi::emit_store_to_address(emitter, "r11", "r9", 8); // store the address string length - } - } - return; - } - if let Some(offset) = ctx.variables.get(name).map(|var| var.stack_offset) { - // A local string slot keeps the pointer at `offset` and the length at - // `offset - 8`, matching `abi::emit_store`/`emit_load` for `PhpType::Str`. - match emitter.target.arch { - Arch::AArch64 => { - abi::store_at_offset(emitter, "x10", offset); // store the address string pointer - abi::store_at_offset(emitter, "x11", offset - 8); // store the address string length - } - Arch::X86_64 => { - abi::store_at_offset(emitter, "r10", offset); // store the address string pointer - abi::store_at_offset(emitter, "r11", offset - 8); // store the address string length - } - } - } -} diff --git a/src/codegen/builtins/io/stream_socket_client.rs b/src/codegen/builtins/io/stream_socket_client.rs deleted file mode 100644 index 227d723696..0000000000 --- a/src/codegen/builtins/io/stream_socket_client.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_client` calls. -//! Opens a connected TCP socket and yields it as a PHP stream resource. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_stream_socket_client` helper returns the connected descriptor or -//! -1; the result is boxed by the shared `box_socket_result` helper. - -use crate::codegen::builtins::io::stream_socket_server::box_socket_result; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_socket_client()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_client()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - // Preserve the address (ptr/len) across the connect so the connected - // fd can be paired with its transport host for TLS SNI defaulting. - emitter.instruction("sub sp, sp, #16"); // scratch: [sp,#0] addr ptr, [sp,#8] addr len - emitter.instruction("str x1, [sp, #0]"); // save the address pointer - emitter.instruction("str x2, [sp, #8]"); // save the address length - emitter.instruction("mov x0, x1"); // address pointer becomes the first helper argument - emitter.instruction("mov x1, x2"); // address length becomes the second helper argument - abi::emit_call_label(emitter, "__rt_stream_socket_client"); - // -- stash the transport host for this fd (passthrough: fd in x0 out x0) -- - emitter.instruction("ldr x1, [sp, #0]"); // reload the address pointer - emitter.instruction("ldr x2, [sp, #8]"); // reload the address length - emitter.instruction("add sp, sp, #16"); // release the address scratch frame - abi::emit_call_label(emitter, "__rt_stash_connect_host"); // record _stream_connect_host[fd], returns fd in x0 - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // scratch: [rsp+0] addr ptr, [rsp+8] addr len - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the address pointer - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the address length - emitter.instruction("mov rdi, rax"); // address pointer becomes the first SysV argument - emitter.instruction("mov rsi, rdx"); // address length becomes the second SysV argument - abi::emit_call_label(emitter, "__rt_stream_socket_client"); - // -- stash the transport host for this fd (passthrough: fd in rdi out rax) -- - emitter.instruction("mov rdi, rax"); // connected fd becomes the stash arg0 - emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // reload the address pointer - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // reload the address length - emitter.instruction("add rsp, 16"); // release the address scratch frame - abi::emit_call_label(emitter, "__rt_stash_connect_host"); // record _stream_connect_host[fd], returns fd in rax - } - } - box_socket_result(emitter, ctx); - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/io/stream_socket_enable_crypto.rs b/src/codegen/builtins/io/stream_socket_enable_crypto.rs deleted file mode 100644 index bd58ef976c..0000000000 --- a/src/codegen/builtins/io/stream_socket_enable_crypto.rs +++ /dev/null @@ -1,294 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_enable_crypto` calls. -//! -//! When `$enable` is true, the helper invokes `elephc_tls_attach_fd` via -//! the runtime function-pointer slot, stores the returned handle in -//! `_tls_sessions[fd]`, and reports success. Subsequent fread/fwrite -//! consult that table and route through `elephc_tls_read_fn` / -//! `elephc_tls_write_fn` instead of the raw read/write syscalls. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - SNI / cert-name is taken from the active stream context's -//! `['ssl']['peer_name']` (via `__rt_get_ssl_peer_name`). When no context -//! peer-name is set, the SNI defaults to the transport host that -//! `stream_socket_client` recorded for this fd in `_stream_connect_host[fd]` -//! (matching PHP, which defaults the peer name to the connection host); the -//! `localhost` constant (`_tls_peer_name_default`) is used only when neither a -//! context peer-name nor a recorded connection host is available. With a -//! peer-name set, real TLS to named hosts works end to end (verified against a -//! public HTTPS host — see `test_stream_socket_enable_crypto_real_tls_handshake`). -//! - The 3rd ($crypto_method) and 4th ($session_stream) PHP args are -//! evaluated for side effects but otherwise ignored — elephc relies on -//! rustls's default TLS protocol negotiation. -//! - $enable=false (mid-stream TLS shutdown) reloads the fd and calls the -//! shared `fclose::emit_tls_session_teardown`, which sends `close_notify` -//! via `_elephc_tls_close_fn` and clears `_tls_sessions[fd]` (a no-op when -//! no session is attached), leaving the fd as a plain TCP socket; it then -//! reports `true`. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::https_stream::publish_tls_function_pointers; -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `stream_socket_enable_crypto()` stream and I/O builtin calls. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_enable_crypto()"); - // -- evaluate the stream arg → fd in x0/rax, save on the stack -- - emit_stream_fd_arg(name, &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // [sp+0] = fd, preserved across the remaining arg evaluations and the attach call - // -- evaluate $enable; branch on its value -- - let enable_label = ctx.next_label("ssec_enable"); - let done_label = ctx.next_label("ssec_done"); - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve $enable while ignored optional args are evaluated - for arg in &args[2..] { - emit_expr(arg, emitter, ctx, data); // side effects only - } - match emitter.target.arch { - Arch::AArch64 => abi::emit_pop_reg(emitter, "x0"), - Arch::X86_64 => abi::emit_pop_reg(emitter, "rax"), - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbnz x0, {}", enable_label)); // enable=true enters the TLS attach path - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the caller request TLS enablement? - emitter.instruction(&format!("jnz {}", enable_label)); // enable=true enters the TLS attach path - } - } - // -- disable path: unwind any live TLS session on the fd, then report - // success. The fd is reloaded from the stashed slot; the teardown sends - // close_notify and clears _tls_sessions[fd], and is a no-op when no TLS - // session is attached. After this the fd is a plain TCP socket again. -- - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("ldr x0, [sp]"), // reload the stashed fd for the teardown - Arch::X86_64 => emitter.instruction("mov rax, QWORD PTR [rsp]"), // reload the stashed fd for the teardown - } - super::fclose::emit_tls_session_teardown(emitter, ctx); - abi::emit_release_temporary_stack(emitter, 16); // drop the stashed fd - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #1"), // mid-stream crypto disable succeeded - Arch::X86_64 => emitter.instruction("mov eax, 1"), // mid-stream crypto disable succeeded - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {}", done_label)), // return without attaching TLS - Arch::X86_64 => emitter.instruction(&format!("jmp {}", done_label)), // return without attaching TLS - } - - // -- enable path: publish tls fn pointers, attach the fd, record session -- - emitter.label(&enable_label); - publish_tls_function_pointers(emitter); - let fail_label = ctx.next_label("ssec_attach_fail"); - // Uniquified per call so a program may invoke stream_socket_enable_crypto - // more than once (e.g. enable then disable) without a duplicate label. - let peer_ok = ctx.next_label("ssec_peer_ok"); - match emitter.target.arch { - Arch::AArch64 => { - // -- look up the SSL peer-name from the stream context. Stack - // slots [sp+16] / [sp+24] receive (ptr, len) on success; - // the fall-back hardcoded "localhost" pair is written if - // the lookup misses. The fd stays in [sp+0] across all of - // this. -- - // 64 B spill: [0]/[8] = peer-name ptr/len, [16]/[24] = ssl.local_cert - // ptr/len, [32]/[40] = ssl.local_pk ptr/len, [48]/[56] = padding. The - // saved fd sits at [sp+64] (shifted by this push). - emitter.instruction("sub sp, sp, #64"); // peer-name + client-cert/key spill (extends the frame above the fd slot) - emitter.instruction("add x0, sp, #0"); // out_ptr address - emitter.instruction("add x1, sp, #8"); // out_len address - emitter.instruction("bl __rt_get_ssl_peer_name"); // x0 = 1 hit / 0 miss - emitter.instruction(&format!("cbnz x0, {}", peer_ok)); // hit: use the loaded (ptr, len) - // -- miss: default the SNI to the connection host recorded by - // stream_socket_client (_stream_connect_host[fd]) before falling - // back to the hardcoded "localhost". The fd sits at [sp+64] - // (saved-fd slot, shifted by the peer-name push above). -- - let host_default = ctx.next_label("ssec_host_default"); - emitter.instruction("ldr x10, [sp, #64]"); // reload fd for the connect-host table index - abi::emit_symbol_address(emitter, "x9", "_stream_connect_host"); - emitter.instruction("add x9, x9, x10, lsl #4"); // &_stream_connect_host[fd] (16-byte ptr/len slots) - emitter.instruction("ldr x11, [x9, #8]"); // stashed host length (0 = unset) - emitter.instruction(&format!("cbz x11, {}", host_default)); // no stashed host → use the "localhost" default - emitter.instruction("ldr x12, [x9, #0]"); // stashed host pointer - emitter.instruction("str x12, [sp, #0]"); // peer_name ptr = connection host - emitter.instruction("str x11, [sp, #8]"); // peer_name len = connection host length - emitter.instruction(&format!("b {}", peer_ok)); // host defaulted from the connection — skip localhost - emitter.label(&host_default); - abi::emit_symbol_address(emitter, "x9", "_tls_peer_name_default"); - emitter.instruction("str x9, [sp, #0]"); // fall back to "localhost" ptr - emitter.instruction("mov x9, #9"); // strlen("localhost") - emitter.instruction("str x9, [sp, #8]"); // fall back to "localhost" len - emitter.label(&peer_ok); - // -- look up ssl.local_cert / ssl.local_pk for mutual-TLS client - // auth. The getter leaves the out slots untouched on a miss, so - // pre-zero the length slots: a zero length selects the plain - // (no client cert) attach variant. -- - let plain_attach = ctx.next_label("ssec_plain_attach"); - let do_attach = ctx.next_label("ssec_do_attach"); - emitter.instruction("str xzr, [sp, #24]"); // ssl.local_cert length = 0 (no client cert by default) - emitter.instruction("str xzr, [sp, #40]"); // ssl.local_pk length = 0 - abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); - emitter.instruction("mov x1, #3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "x2", "_ssl_local_cert_key_str"); - emitter.instruction("mov x3, #10"); // strlen("local_cert") - emitter.instruction("add x4, sp, #16"); // local_cert out_ptr address - emitter.instruction("add x5, sp, #24"); // local_cert out_len address - emitter.instruction("bl __rt_get_string_context_option"); // fill [sp+16]/[sp+24] on hit - abi::emit_symbol_address(emitter, "x0", "_ssl_key_str"); - emitter.instruction("mov x1, #3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "x2", "_ssl_local_pk_key_str"); - emitter.instruction("mov x3, #8"); // strlen("local_pk") - emitter.instruction("add x4, sp, #32"); // local_pk out_ptr address - emitter.instruction("add x5, sp, #40"); // local_pk out_len address - emitter.instruction("bl __rt_get_string_context_option"); // fill [sp+32]/[sp+40] on hit - // -- common attach args + variant selection -- - emitter.instruction("ldr x0, [sp, #64]"); // reload fd → 1st arg - emitter.instruction("ldr x1, [sp, #0]"); // peer_name ptr → 2nd arg - emitter.instruction("ldr x2, [sp, #8]"); // peer_name len → 3rd arg - emitter.instruction("ldr x9, [sp, #24]"); // local_cert length - emitter.instruction(&format!("cbz x9, {}", plain_attach)); // no client cert → plain attach - emitter.instruction("ldr x9, [sp, #40]"); // local_pk length - emitter.instruction(&format!("cbz x9, {}", plain_attach)); // missing key → plain attach - emitter.instruction("ldr x3, [sp, #16]"); // local_cert path ptr → 4th arg - emitter.instruction("ldr x4, [sp, #24]"); // local_cert path len → 5th arg - emitter.instruction("ldr x5, [sp, #32]"); // local_pk path ptr → 6th arg - emitter.instruction("ldr x6, [sp, #40]"); // local_pk path len → 7th arg - abi::emit_symbol_address(emitter, "x9", "_elephc_tls_attach_fd_client_cert_fn"); - emitter.instruction("ldr x9, [x9]"); // mutual-TLS attach variant - emitter.instruction(&format!("b {}", do_attach)); // call the selected mutual-TLS attach function - emitter.label(&plain_attach); - abi::emit_symbol_address(emitter, "x9", "_elephc_tls_attach_fd_fn"); - emitter.instruction("ldr x9, [x9]"); // server-auth-only attach variant - emitter.label(&do_attach); - emitter.instruction("blr x9"); // x0 = handle (>=1) or -1 - emitter.instruction("ldr x10, [sp, #64]"); // reload fd - abi::emit_release_temporary_stack(emitter, 64); // pop the peer-name + cert/key spill area - abi::emit_release_temporary_stack(emitter, 16); // pop the saved fd - emitter.instruction("cmp x0, #0"); // did TLS attach return a failure handle? - emitter.instruction(&format!("b.lt {}", fail_label)); // report false when attach failed - abi::emit_symbol_address(emitter, "x11", "_tls_sessions"); - emitter.instruction("str x0, [x11, x10, lsl #3]"); // _tls_sessions[fd] = handle - emitter.instruction("mov x0, #1"); // report successful TLS enablement - emitter.instruction(&format!("b {}", done_label)); // skip the failure result - emitter.label(&fail_label); - emitter.instruction("mov x0, #0"); // report failed TLS enablement - } - Arch::X86_64 => { - // Same peer-name lookup as the AArch64 branch. `emit_push_reg` - // on x86_64 reserves 16 bytes (sub rsp,16 + mov), so rsp at - // this point is 0-mod-16; the spill area below must therefore - // also be 0-mod-16 in size so the two SysV `call`s land on - // an aligned rsp. 32 bytes covers (ptr, len) + the required - // alignment padding. - // 64 B spill: [0]/[8] = peer-name ptr/len, [16]/[24] = ssl.local_cert - // ptr/len, [32]/[40] = ssl.local_pk ptr/len, [48]/[56] = padding. The - // saved fd sits at [rsp+64]. 64 is 0-mod-16, so the SysV calls below - // land on an aligned rsp. - emitter.instruction("sub rsp, 64"); // peer-name + client-cert/key spill (0-mod-16) - emitter.instruction("lea rdi, [rsp + 0]"); // out_ptr address - emitter.instruction("lea rsi, [rsp + 8]"); // out_len address - emitter.instruction("call __rt_get_ssl_peer_name"); // rax = 1 hit / 0 miss - emitter.instruction("test rax, rax"); // did the context provide ssl.peer_name? - emitter.instruction(&format!("jnz {}", peer_ok)); // use the loaded peer-name when present - // -- miss: default the SNI to the connection host recorded by - // stream_socket_client (_stream_connect_host[fd]) before falling - // back to the hardcoded "localhost". The fd sits at [rsp+64]. -- - let host_default = ctx.next_label("ssec_host_default"); - emitter.instruction("mov r10, QWORD PTR [rsp + 64]"); // reload fd for the connect-host table index - abi::emit_symbol_address(emitter, "r9", "_stream_connect_host"); // base of the per-fd connect-host table - emitter.instruction("shl r10, 4"); // fd * 16 (ptr/len slot stride) - emitter.instruction("add r9, r10"); // &_stream_connect_host[fd] - emitter.instruction("mov r11, QWORD PTR [r9 + 8]"); // stashed host length (0 = unset) - emitter.instruction("test r11, r11"); // is a connection host recorded? - emitter.instruction(&format!("jz {}", host_default)); // no stashed host → use the "localhost" default - emitter.instruction("mov r10, QWORD PTR [r9 + 0]"); // stashed host pointer - emitter.instruction("mov QWORD PTR [rsp + 0], r10"); // peer_name ptr = connection host - emitter.instruction("mov QWORD PTR [rsp + 8], r11"); // peer_name len = connection host length - emitter.instruction(&format!("jmp {}", peer_ok)); // host defaulted from the connection — skip localhost - emitter.label(&host_default); - abi::emit_symbol_address(emitter, "r9", "_tls_peer_name_default"); // fallback peer-name literal - emitter.instruction("mov QWORD PTR [rsp + 0], r9"); // peer_name ptr = "localhost" - emitter.instruction("mov r9, 9"); // route the immediate through a register so the assembler always emits a 64-bit store - emitter.instruction("mov QWORD PTR [rsp + 8], r9"); // peer_name len = strlen("localhost") - emitter.label(&peer_ok); - // -- look up ssl.local_cert / ssl.local_pk for mutual-TLS client - // auth; pre-zero the length slots so a miss selects plain attach. -- - let plain_attach = ctx.next_label("ssec_plain_attach_x"); - let after_attach = ctx.next_label("ssec_after_attach_x"); - emitter.instruction("mov QWORD PTR [rsp + 24], 0"); // ssl.local_cert length = 0 (no client cert by default) - emitter.instruction("mov QWORD PTR [rsp + 40], 0"); // ssl.local_pk length = 0 - abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // wrapper key "ssl" - emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "rdx", "_ssl_local_cert_key_str"); // option key "local_cert" - emitter.instruction("mov rcx, 10"); // strlen("local_cert") - emitter.instruction("lea r8, [rsp + 16]"); // local_cert out_ptr address - emitter.instruction("lea r9, [rsp + 24]"); // local_cert out_len address - emitter.instruction("call __rt_get_string_context_option"); // fill [rsp+16]/[rsp+24] on hit - abi::emit_symbol_address(emitter, "rdi", "_ssl_key_str"); // wrapper key "ssl" - emitter.instruction("mov rsi, 3"); // strlen("ssl") - abi::emit_symbol_address(emitter, "rdx", "_ssl_local_pk_key_str"); // option key "local_pk" - emitter.instruction("mov rcx, 8"); // strlen("local_pk") - emitter.instruction("lea r8, [rsp + 32]"); // local_pk out_ptr address - emitter.instruction("lea r9, [rsp + 40]"); // local_pk out_len address - emitter.instruction("call __rt_get_string_context_option"); // fill [rsp+32]/[rsp+40] on hit - // -- common attach args + variant selection -- - emitter.instruction("mov rdi, QWORD PTR [rsp + 64]"); // reload fd → 1st arg - emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // peer_name ptr → 2nd arg - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // peer_name len → 3rd arg - emitter.instruction("mov rax, QWORD PTR [rsp + 24]"); // local_cert length - emitter.instruction("test rax, rax"); // is a client certificate path present? - emitter.instruction(&format!("jz {}", plain_attach)); // no client cert → plain attach - emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // local_pk length - emitter.instruction("test rax, rax"); // is a client key path present? - emitter.instruction(&format!("jz {}", plain_attach)); // missing key → plain attach - // mutual-TLS attach: args 4-6 in rcx/r8/r9, arg 7 (key_len) on stack - emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // local_cert path ptr → 4th arg - emitter.instruction("mov r8, QWORD PTR [rsp + 24]"); // local_cert path len → 5th arg - emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // local_pk path len (for the stack arg) - emitter.instruction("mov r9, QWORD PTR [rsp + 32]"); // local_pk path ptr → 6th arg - emitter.instruction("sub rsp, 16"); // reserve the 7th stack arg + padding (stays 0-mod-16) - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // 7th arg = local_pk path len - abi::emit_load_symbol_to_reg(emitter, "r10", "_elephc_tls_attach_fd_client_cert_fn", 0); // mutual-TLS attach function pointer - emitter.instruction("call r10"); // rax = handle or -1 - emitter.instruction("add rsp, 16"); // pop the 7th stack arg - emitter.instruction(&format!("jmp {}", after_attach)); // skip the plain attach variant - emitter.label(&plain_attach); - emitter.instruction("mov rdi, QWORD PTR [rsp + 64]"); // reload fd → 1st arg - emitter.instruction("mov rsi, QWORD PTR [rsp + 0]"); // peer_name ptr → 2nd arg - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // peer_name len → 3rd arg - abi::emit_load_symbol_to_reg(emitter, "r9", "_elephc_tls_attach_fd_fn", 0); // server-auth-only attach function pointer - emitter.instruction("call r9"); // rax = handle or -1 - emitter.label(&after_attach); - emitter.instruction("mov r10, QWORD PTR [rsp + 64]"); // reload fd - abi::emit_release_temporary_stack(emitter, 64); // pop peer-name + cert/key spill - abi::emit_release_temporary_stack(emitter, 16); // pop saved fd - emitter.instruction("cmp rax, 0"); // did TLS attach return a failure handle? - emitter.instruction(&format!("jl {}", fail_label)); // report false when attach failed - abi::emit_symbol_address(emitter, "r11", "_tls_sessions"); // TLS session handle table - emitter.instruction("mov QWORD PTR [r11 + r10 * 8], rax"); // _tls_sessions[fd] = handle - emitter.instruction("mov eax, 1"); // report successful TLS enablement - emitter.instruction(&format!("jmp {}", done_label)); // skip the failure result - emitter.label(&fail_label); - emitter.instruction("xor eax, eax"); // report failed TLS enablement - } - } - - emitter.label(&done_label); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_socket_get_name.rs b/src/codegen/builtins/io/stream_socket_get_name.rs deleted file mode 100644 index 901c31a964..0000000000 --- a/src/codegen/builtins/io/stream_socket_get_name.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_get_name` calls. -//! Yields the local or peer address of a socket as an `A.B.C.D:port` string. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Marshals the descriptor and the remote flag into the two -//! `__rt_stream_socket_get_name` argument registers. -//! - The helper returns an owned heap string, or a null pointer boxed as -//! PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `stream_socket_get_name()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_get_name()"); - emit_stream_fd_arg("stream_socket_get_name", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // remote flag into argument 1 - abi::emit_pop_reg(emitter, "x0"); // descriptor into argument 0 - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // remote flag into argument 1 - abi::emit_pop_reg(emitter, "rdi"); // descriptor into argument 0 - } - } - abi::emit_call_label(emitter, "__rt_stream_socket_get_name"); - box_string_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("stream_socket_get_name_false"); - let done_label = ctx.next_label("stream_socket_get_name_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means the lookup failed - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means the lookup failed - emitter.instruction(&format!("jz {}", false_label)); // box false when the lookup failed - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/stream_socket_pair.rs b/src/codegen/builtins/io/stream_socket_pair.rs deleted file mode 100644 index c8cd86a71c..0000000000 --- a/src/codegen/builtins/io/stream_socket_pair.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_pair` calls. -//! Creates a connected pair of sockets and yields them as a two-element array. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Marshals the domain, type, and protocol into the three -//! `__rt_stream_socket_pair` argument registers; the helper returns the -//! pointer to a freshly built indexed array of socket resources. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_socket_pair()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_pair()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the domain - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the type - emit_expr(&args[2], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x2, x0"); // protocol into argument 2 - abi::emit_pop_reg(emitter, "x1"); // type into argument 1 - abi::emit_pop_reg(emitter, "x0"); // domain into argument 0 - } - Arch::X86_64 => { - emitter.instruction("mov rdx, rax"); // protocol into argument 2 - abi::emit_pop_reg(emitter, "rsi"); // type into argument 1 - abi::emit_pop_reg(emitter, "rdi"); // domain into argument 0 - } - } - abi::emit_call_label(emitter, "__rt_stream_socket_pair"); - box_pair_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Widens the descriptor array's typed int slots into boxed Mixed(resource) -/// cells, then boxes the resulting array pointer as a Mixed indexed-array -/// cell. A null pointer from the helper (socketpair failure) lowers to a -/// Mixed false cell instead. -fn box_pair_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("ssp_false"); - let done_label = ctx.next_label("ssp_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x0, {}", false_label)); // null pointer => box PHP false - emitter.instruction("mov x1, #9"); // resource tag: each fd becomes Mixed(resource) - abi::emit_call_label(emitter, "__rt_array_to_mixed"); // widen slots from raw ints to boxed Mixed pointers - emitter.instruction("mov x1, x0"); // success: converted array pointer becomes the Mixed payload low word - emitter.instruction("mov x2, #0"); // indexed array mixed payloads do not use a high word - emitter.instruction("mov x0, #4"); // runtime tag 4 = indexed array - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the success array as a Mixed cell - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // bool payload = 0 for false - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box PHP false for socketpair() failure - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // null pointer signals socketpair failure - emitter.instruction(&format!("jz {}", false_label)); // box PHP false when socketpair() failed - emitter.instruction("mov rdi, rax"); // array pointer for __rt_array_to_mixed - emitter.instruction("mov esi, 9"); // resource tag: each fd becomes Mixed(resource) - abi::emit_call_label(emitter, "__rt_array_to_mixed"); // widen slots from raw ints to boxed Mixed pointers - emitter.instruction("mov rdi, rax"); // converted array pointer becomes the Mixed payload low word - emitter.instruction("xor esi, esi"); // indexed array mixed payloads do not use a high word - emitter.instruction("mov eax, 4"); // runtime tag 4 = indexed array - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the success array as a Mixed cell - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // bool payload = 0 for false - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box PHP false for socketpair() failure - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/stream_socket_recvfrom.rs b/src/codegen/builtins/io/stream_socket_recvfrom.rs deleted file mode 100644 index b9ec639bb3..0000000000 --- a/src/codegen/builtins/io/stream_socket_recvfrom.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_recvfrom` calls. -//! Receives a message from a socket and yields it as a `string|false` value. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Marshals the descriptor, length, and optional flags into the three -//! `__rt_stream_socket_recvfrom` argument registers; omitted flags are 0. -//! - The helper returns an owned heap string, or a null pointer boxed as -//! PHP false. - -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `stream_socket_recvfrom()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_recvfrom()"); - emit_stream_fd_arg("stream_socket_recvfrom", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the length - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - } else { - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #0"), // omitted flags default to 0 - Arch::X86_64 => emitter.instruction("xor eax, eax"), // omitted flags default to 0 - } - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x2, x0"); // receive flags into argument 2 - abi::emit_pop_reg(emitter, "x1"); // length into argument 1 - abi::emit_pop_reg(emitter, "x0"); // descriptor into argument 0 - } - Arch::X86_64 => { - emitter.instruction("mov rdx, rax"); // receive flags into argument 2 - abi::emit_pop_reg(emitter, "rsi"); // length into argument 1 - abi::emit_pop_reg(emitter, "rdi"); // descriptor into argument 0 - } - } - abi::emit_call_label(emitter, "__rt_stream_socket_recvfrom"); - box_string_or_false(emitter, ctx); - if let Some(addr_arg) = args.get(3) { - emit_store_recv_address(addr_arg, emitter, ctx); - } - Some(PhpType::Mixed) -} - -/// Writes the sender address (stashed by `__rt_stream_socket_recvfrom` in the -/// `_recvfrom_addr_*` globals) into the by-reference `$address` variable. The -/// boxed receive result is preserved across the store. -fn emit_store_recv_address(arg: &Expr, emitter: &mut Emitter, ctx: &mut Context) { - let ExprKind::Variable(name) = &arg.kind else { - return; - }; - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, "x0"); // preserve the boxed receive result - abi::emit_symbol_address(emitter, "x9", "_recvfrom_addr_ptr"); - emitter.instruction("ldr x10, [x9]"); // load the stashed sender address pointer - abi::emit_symbol_address(emitter, "x9", "_recvfrom_addr_len"); - emitter.instruction("ldr x11, [x9]"); // load the stashed sender address length - emit_store_recv_address_slot(name, emitter, ctx); - abi::emit_pop_reg(emitter, "x0"); // restore the boxed receive result - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the boxed receive result - abi::emit_symbol_address(emitter, "r9", "_recvfrom_addr_ptr"); // address of the stashed-pointer global - emitter.instruction("mov r10, QWORD PTR [r9]"); // load the stashed sender address pointer - abi::emit_symbol_address(emitter, "r9", "_recvfrom_addr_len"); // address of the stashed-length global - emitter.instruction("mov r11, QWORD PTR [r9]"); // load the stashed sender address length - emit_store_recv_address_slot(name, emitter, ctx); - abi::emit_pop_reg(emitter, "rax"); // restore the boxed receive result - } - } - ctx.update_var_type_and_ownership(name, PhpType::Str, HeapOwnership::Owned); -} - -/// Stores the address string (pointer in x10/r10, length in x11/r11) into the -/// `$address` variable's 16-byte string slot, dispatching on storage class. -fn emit_store_recv_address_slot(name: &str, emitter: &mut Emitter, ctx: &Context) { - let is_global = - ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)); - if is_global { - let label = format!("_gvar_{}", name); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", &label); // load page of the global address variable - emitter.instruction("str x10, [x9]"); // store the address string pointer - emitter.instruction("str x11, [x9, #8]"); // store the address string length - } - Arch::X86_64 => { - abi::emit_store_reg_to_symbol(emitter, "r10", &label, 0); // store the address string pointer - abi::emit_store_reg_to_symbol(emitter, "r11", &label, 8); // store the address string length - } - } - return; - } - if ctx.ref_params.contains(name) { - let offset = ctx - .variables - .get(name) - .expect("codegen bug: missing ref-param slot for recvfrom $address") - .stack_offset; - match emitter.target.arch { - Arch::AArch64 => { - abi::load_at_offset(emitter, "x9", offset); // load the referenced address storage pointer - emitter.instruction("str x10, [x9]"); // store the address string pointer - emitter.instruction("str x11, [x9, #8]"); // store the address string length - } - Arch::X86_64 => { - abi::load_at_offset(emitter, "r9", offset); // load the referenced address storage pointer - abi::emit_store_to_address(emitter, "r10", "r9", 0); // store the address string pointer - abi::emit_store_to_address(emitter, "r11", "r9", 8); // store the address string length - } - } - return; - } - if let Some(offset) = ctx.variables.get(name).map(|var| var.stack_offset) { - // A local string slot keeps the pointer at `offset` and the length at - // `offset - 8`, matching `abi::emit_store`/`emit_load` for `PhpType::Str`. - match emitter.target.arch { - Arch::AArch64 => { - abi::store_at_offset(emitter, "x10", offset); // store the address string pointer - abi::store_at_offset(emitter, "x11", offset - 8); // store the address string length - } - Arch::X86_64 => { - abi::store_at_offset(emitter, "r10", offset); // store the address string pointer - abi::store_at_offset(emitter, "r11", offset - 8); // store the address string length - } - } - } -} - -/// Boxes the helper result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("stream_socket_recvfrom_false"); - let done_label = ctx.next_label("stream_socket_recvfrom_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means the receive failed - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means the receive failed - emitter.instruction(&format!("jz {}", false_label)); // box false when the receive failed - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/stream_socket_sendto.rs b/src/codegen/builtins/io/stream_socket_sendto.rs deleted file mode 100644 index df7de1fd96..0000000000 --- a/src/codegen/builtins/io/stream_socket_sendto.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_sendto` calls. -//! Sends a message on a socket, optionally to an explicit datagram address. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Marshals the descriptor, data string, optional flags, and optional -//! address string into the six `__rt_stream_socket_sendto` argument -//! registers; omitted flags default to 0 and an omitted address to empty. -//! - The helper returns the byte count, or -1 boxed as PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `stream_socket_sendto()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_sendto()"); - emit_stream_fd_arg("stream_socket_sendto", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, "x1"); // preserve the data pointer - abi::emit_push_reg(emitter, "x2"); // preserve the data length - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the data pointer - abi::emit_push_reg(emitter, "rdx"); // preserve the data length - } - } - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - } else { - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #0"), // omitted flags default to 0 - Arch::X86_64 => emitter.instruction("xor eax, eax"), // omitted flags default to 0 - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the send flags - match emitter.target.arch { - Arch::AArch64 => { - if args.len() >= 4 { - emit_expr(&args[3], emitter, ctx, data); - emitter.instruction("mov x4, x1"); // address pointer into argument 4 - emitter.instruction("mov x5, x2"); // address length into argument 5 - } else { - emitter.instruction("mov x4, #0"); // omitted address: NULL pointer - emitter.instruction("mov x5, #0"); // omitted address: zero length - } - abi::emit_pop_reg(emitter, "x3"); // send flags into argument 3 - abi::emit_pop_reg(emitter, "x2"); // data length into argument 2 - abi::emit_pop_reg(emitter, "x1"); // data pointer into argument 1 - abi::emit_pop_reg(emitter, "x0"); // descriptor into argument 0 - } - Arch::X86_64 => { - if args.len() >= 4 { - emit_expr(&args[3], emitter, ctx, data); - emitter.instruction("mov r8, rax"); // address pointer into argument 5 - emitter.instruction("mov r9, rdx"); // address length into argument 6 - } else { - emitter.instruction("xor r8d, r8d"); // omitted address: NULL pointer - emitter.instruction("xor r9d, r9d"); // omitted address: zero length - } - abi::emit_pop_reg(emitter, "rcx"); // send flags into argument 4 - abi::emit_pop_reg(emitter, "rdx"); // data length into argument 3 - abi::emit_pop_reg(emitter, "rsi"); // data pointer into argument 2 - abi::emit_pop_reg(emitter, "rdi"); // descriptor into argument 1 - } - } - abi::emit_call_label(emitter, "__rt_stream_socket_sendto"); - box_count_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a -1 sentinel becomes PHP `false`, any other value -/// becomes a boxed integer byte count. -fn box_count_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("stream_socket_sendto_false"); - let done_label = ctx.next_label("stream_socket_sendto_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the helper report a failed send? - emitter.instruction(&format!("b.lt {}", false_label)); // box PHP false on the -1 sentinel - emitter.instruction("mov x1, x0"); // move the byte count into the mixed payload - emitter.instruction("mov x2, #0"); // integer mixed payloads have no high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = integer - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid send - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the helper report a failed send? - emitter.instruction(&format!("js {}", false_label)); // box PHP false on the -1 sentinel - emitter.instruction("mov rdi, rax"); // move the byte count into the mixed payload - emitter.instruction("xor esi, esi"); // integer mixed payloads have no high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = integer - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid send - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/stream_socket_server.rs b/src/codegen/builtins/io/stream_socket_server.rs deleted file mode 100644 index ea730fc589..0000000000 --- a/src/codegen/builtins/io/stream_socket_server.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_server` calls. -//! Opens a listening TCP socket and yields it as a PHP stream resource. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The `__rt_stream_socket_server` helper returns the listening descriptor or -//! -1; -1 is boxed as PHP false, a valid descriptor as a stream resource. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_socket_server()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_server()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // address pointer becomes the first helper argument - emitter.instruction("mov x1, x2"); // address length becomes the second helper argument - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // address pointer becomes the first SysV argument - emitter.instruction("mov rsi, rdx"); // address length becomes the second SysV argument - } - } - abi::emit_call_label(emitter, "__rt_stream_socket_server"); - box_socket_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a -1 descriptor becomes PHP `false`, any other -/// value becomes a stream resource. Shared with `stream_socket_client`. -pub(super) fn box_socket_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("sockserver_false"); - let done_label = ctx.next_label("sockserver_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the helper report a failed socket? - emitter.instruction(&format!("b.lt {}", false_label)); // box PHP false on a -1 sentinel - emitter.instruction("mov x1, x0"); // move the descriptor into the mixed payload - emitter.instruction("mov x2, #0"); // resource mixed payloads have no high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("b {}", done_label)); // skip the false path after success - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the helper report a failed socket? - emitter.instruction(&format!("js {}", false_label)); // box PHP false on a -1 sentinel - emitter.instruction("mov rdi, rax"); // move the descriptor into the mixed payload - emitter.instruction("xor esi, esi"); // resource mixed payloads have no high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after success - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/stream_socket_shutdown.rs b/src/codegen/builtins/io/stream_socket_shutdown.rs deleted file mode 100644 index f0dad314b6..0000000000 --- a/src/codegen/builtins/io/stream_socket_shutdown.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_socket_shutdown` calls. -//! Disables further reception and/or transmission on a socket resource. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Unboxes the socket resource to its descriptor and evaluates the `how` -//! mode, then delegates to `__rt_stream_socket_shutdown`, which returns bool. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits codegen for PHP `stream_socket_shutdown()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_socket_shutdown()"); - emit_stream_fd_arg("stream_socket_shutdown", &args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the descriptor - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // shutdown mode into the second helper argument - abi::emit_pop_reg(emitter, "x0"); // descriptor into the first helper argument - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // shutdown mode into the second SysV argument - abi::emit_pop_reg(emitter, "rdi"); // descriptor into the first SysV argument - } - } - abi::emit_call_label(emitter, "__rt_stream_socket_shutdown"); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_wrapper_register.rs b/src/codegen/builtins/io/stream_wrapper_register.rs deleted file mode 100644 index 5607533c0a..0000000000 --- a/src/codegen/builtins/io/stream_wrapper_register.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_wrapper_register` calls. -//! Records a `(protocol, class-name)` pair in the runtime user-wrapper table -//! and returns the registration success boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - v1 stores up to 16 registrations in `_user_wrappers` and returns `true`; -//! the wrapper class is not yet invoked by `fopen` (that integration is the -//! next Phase-10 commit). -//! - The optional third `flags` argument is evaluated for its side effects -//! and otherwise ignored. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_wrapper_register()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_wrapper_register()"); - // PHP evaluates the protocol string first, then the class string, then - // the optional flags. The flags are accepted for compatibility and - // discarded; the two strings are handed to the runtime helper. - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the protocol string - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the class string - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - } - abi::emit_pop_reg_pair(emitter, "x2", "x3"); // restore class ptr/len - abi::emit_pop_reg_pair(emitter, "x0", "x1"); // restore protocol ptr/len - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the protocol string - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the class string - if args.len() >= 3 { - emit_expr(&args[2], emitter, ctx, data); - } - abi::emit_pop_reg_pair(emitter, "rdx", "rcx"); // restore class ptr/len - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore protocol ptr/len - } - } - abi::emit_call_label(emitter, "__rt_stream_wrapper_register"); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_wrapper_restore.rs b/src/codegen/builtins/io/stream_wrapper_restore.rs deleted file mode 100644 index 3486986b76..0000000000 --- a/src/codegen/builtins/io/stream_wrapper_restore.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_wrapper_restore` calls. -//! v1 always reports success — elephc's built-in wrappers cannot be -//! unregistered, so a restore is effectively a no-op. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The protocol argument is evaluated for its side effects and discarded; -//! the result is `true`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_wrapper_restore()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_wrapper_restore()"); - // Evaluate the protocol string for its side effects; the v1 stub always - // reports success because built-in wrappers cannot be unregistered. - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mov x0, #1"), // return true (built-in wrappers are always present) - Arch::X86_64 => emitter.instruction("mov eax, 1"), // return true (built-in wrappers are always present) - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/stream_wrapper_unregister.rs b/src/codegen/builtins/io/stream_wrapper_unregister.rs deleted file mode 100644 index 0923f5e994..0000000000 --- a/src/codegen/builtins/io/stream_wrapper_unregister.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `stream_wrapper_unregister` calls. -//! Removes a user-defined wrapper registration from the runtime -//! `_user_wrappers` table. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returns `true` when a matching slot was cleared and `false` when the -//! protocol was not registered. Built-in protocols (`file`, `php`, ...) are -//! not user-registered and cannot be unregistered in v1. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `stream_wrapper_unregister()` stream and I/O builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stream_wrapper_unregister()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // arg 0 = protocol pointer - emitter.instruction("mov x1, x2"); // arg 1 = protocol length - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // arg 0 = protocol pointer - emitter.instruction("mov rsi, rdx"); // arg 1 = protocol length - } - } - abi::emit_call_label(emitter, "__rt_stream_wrapper_unregister"); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/symlink.rs b/src/codegen/builtins/io/symlink.rs deleted file mode 100644 index ef4ecb65e8..0000000000 --- a/src/codegen/builtins/io/symlink.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Purpose: -//! Emits PHP `symlink` builtin calls. -//! Marshals target / link path arguments and invokes the libc wrapper runtime. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returns `true` on success, `false` on failure. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `symlink()` runtime helper. -/// -/// # Arguments -/// - `args[0]`: target path (the original file the link points to) -/// - `args[1]`: link path (the symbolic link to create) -/// -/// # Assembly sequence -/// 1. Evaluate `args[0]` (target) → string ptr/len in ABI register pair -/// 2. Preserve target registers across the second argument evaluation -/// 3. Evaluate `args[1]` (link) → string ptr/len in ABI register pair -/// 4. Restore target registers to primary string-argument position -/// 5. Call `__rt_symlink(target, link)` via libc wrapper -/// -/// # Return -/// Always returns `Some(PhpType::Bool)` — `true` on success, `false` on failure. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("symlink()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve target ptr/len while link is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move link pointer into the secondary string-argument pair - emitter.instruction("mov x4, x2"); // move link length into the secondary string-argument pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore target ptr/len - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve target ptr/len - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // link → secondary string pointer - emitter.instruction("mov rsi, rdx"); // link → secondary string length - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore target ptr/len - } - } - abi::emit_call_label(emitter, "__rt_symlink"); // libc symlink(target, link) wrapper - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/sys_get_temp_dir.rs b/src/codegen/builtins/io/sys_get_temp_dir.rs deleted file mode 100644 index 2d681a775d..0000000000 --- a/src/codegen/builtins/io/sys_get_temp_dir.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `sys_get_temp_dir` path-oriented builtin calls. -//! Marshals path strings into runtime helpers that normalize, split, or enumerate filesystem paths. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returned strings and arrays must use runtime allocation/layout compatible with PHP false-on-failure behavior. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the `sys_get_temp_dir` builtin, which returns "/tmp". -/// -/// On call, this function: -/// -/// 1. Adds the literal string "/tmp" to the data section and obtains its label and length. -/// 2. Materializes the string address into the ABI-defined string-pointer result register. -/// 3. Loads the string length into the ABI-defined string-length result register. -/// 4. Returns `PhpType::Str` to indicate the call produces a string result. -/// -/// Arguments (`_args`) are ignored — `sys_get_temp_dir` takes no parameters in PHP. -/// -/// # Returns -/// `Some(PhpType::Str)` on success, or `None` if the call should produce no value (not used here). -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sys_get_temp_dir()"); - let (lbl, len) = data.add_string(b"/tmp"); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_symbol_address(emitter, ptr_reg, &lbl); // materialize the hardcoded temp-directory string in the active string-pointer result register - abi::emit_load_int_immediate(emitter, len_reg, len as i64); // publish the hardcoded temp-directory string length in the paired string-length result register - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/tempnam.rs b/src/codegen/builtins/io/tempnam.rs deleted file mode 100644 index 1bb183476f..0000000000 --- a/src/codegen/builtins/io/tempnam.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Purpose: -//! Emits PHP `tempnam` path-oriented builtin calls. -//! Marshals path strings into runtime helpers that normalize, split, or enumerate filesystem paths. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Returned strings and arrays must use runtime allocation/layout compatible with PHP false-on-failure behavior. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `tempnam(dir, prefix)` builtin call. -/// -/// Evaluates `dir` (args[0]) first, then `prefix` (args[1]), marshaling both as -/// string pairs into the runtime helper `__rt_tempnam`. On ARM64 the directory -/// pair is saved/restored via the stack around the prefix evaluation; on x86_64 -/// it is preserved in `rax`/`rdx`. Returns `PhpType::Str` on success, -/// or the caller handles PHP false-on-failure semantics. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("tempnam()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push dir ptr and length onto the stack while the prefix expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the prefix pointer into the third ARM64 string-argument slot - emitter.instruction("mov x4, x2"); // move the prefix length into the fourth ARM64 string-argument slot - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the directory string pair after evaluating the prefix expression - abi::emit_call_label(emitter, "__rt_tempnam"); // call the target-aware runtime helper that builds the temp filename - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the directory string pair while the prefix expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the prefix pointer into the third x86_64 string-argument slot - emitter.instruction("mov rsi, rdx"); // move the prefix length into the fourth x86_64 string-argument slot - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the directory string pair after evaluating the prefix expression - abi::emit_call_label(emitter, "__rt_tempnam"); // call the target-aware runtime helper that builds the temp filename - } - } - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/io/tmpfile.rs b/src/codegen/builtins/io/tmpfile.rs deleted file mode 100644 index b17f073d6d..0000000000 --- a/src/codegen/builtins/io/tmpfile.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Purpose: -//! Emits PHP `tmpfile` builtin calls. -//! Creates an auto-deleting temp file through the runtime helper and boxes the -//! result as a stream resource (or false on failure) for PHP-compatible typing. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The runtime helper returns the raw fd in the result register (or -1 on -//! failure). The wrapper boxes it through `__rt_mixed_from_value` like -//! `fopen` so the result type is `resource|false`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `tmpfile` builtin calls. -/// -/// Calls `__rt_tmpfile` runtime helper, then boxes the raw fd result (or -1 -/// on failure) into a `PhpType::Mixed` value using `__rt_mixed_from_value`. -/// The boxed result is `resource|false` matching PHP's `tmpfile()` signature. -/// Returns `Some(PhpType::Mixed)`. -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("tmpfile()"); - abi::emit_call_label(emitter, "__rt_tmpfile"); // call the runtime helper that creates an auto-deleting /tmp/elephc-XXXXXX file - box_tmpfile_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the raw fd result from `__rt_tmpfile` into a PHP-compatible `Mixed`. -/// -/// On entry the result register holds the file descriptor (>= 0 on success, -/// -1 on failure). This function branches on the result, then calls -/// `__rt_mixed_from_value` to box either a resource (tag 9) or bool false -/// (tag 3) into the Mixed value returned to PHP. -fn box_tmpfile_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("tmpfile_false"); - let done_label = ctx.next_label("tmpfile_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did tmpfile() return a negative descriptor for failure? - emitter.instruction(&format!("b.lt {}", false_label)); // box PHP false when the temp file could not be created - emitter.instruction("mov x1, x0"); // move the native stream descriptor into the mixed payload low word - emitter.instruction("mov x2, #0"); // resource mixed payloads do not use a high word - emitter.instruction("mov x0, #9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the successful stream resource result - emitter.instruction(&format!("b {}", done_label)); // skip the false-boxing path after a successful tmpfile - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for tmpfile() failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible tmpfile() failure semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did tmpfile() return a negative descriptor for failure? - emitter.instruction(&format!("js {}", false_label)); // box PHP false when the temp file could not be created - emitter.instruction("mov rdi, rax"); // move the native stream descriptor into the mixed payload low word - emitter.instruction("xor esi, esi"); // resource mixed payloads do not use a high word - emitter.instruction("mov eax, 9"); // runtime tag 9 = resource - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the successful stream resource result - emitter.instruction(&format!("jmp {}", done_label)); // skip the false-boxing path after a successful tmpfile - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for tmpfile() failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible tmpfile() failure semantics - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/io/touch.rs b/src/codegen/builtins/io/touch.rs deleted file mode 100644 index b1dc193d8d..0000000000 --- a/src/codegen/builtins/io/touch.rs +++ /dev/null @@ -1,314 +0,0 @@ -//! Purpose: -//! Emits PHP `touch` filesystem mutation builtin calls. -//! Passes path and mode/owner arguments to runtime helpers that perform observable OS operations. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::functions::infer_contextual_type; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -const TOUCH_ATIME_NOW: u8 = 1; -const TOUCH_MTIME_NOW: u8 = 2; -const TOUCH_BOTH_NOW: u8 = TOUCH_ATIME_NOW | TOUCH_MTIME_NOW; - -/// `stream_metadata` vtable slot index in the per-class user-wrapper vtable. -const STREAM_METADATA_SLOT: usize = 14; -/// PHP `STREAM_META_TOUCH` option value (`touch`-style metadata change). -const STREAM_META_TOUCH: usize = 1; - -/// Emits code for the PHP `touch()` builtin. -/// -/// # Arguments -/// - `name`: Unused name matching the dispatcher signature. -/// - `args`: Expression tree for path, optional mtime, and optional atime. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and function metadata. -/// - `data`: Data section for string literals and relocations. -/// -/// # Returns -/// Always returns `Some(PhpType::Bool)` — `touch()` returns a boolean in PHP. -/// -/// # Behavior -/// Emits path pointer/length in x1/x2 (ARM64) or rdi/rsi (x86_64), then -/// timestamp fields in x3/x4/x5 (ARM64) or rdi/rsi/rcx (x86_64), and calls -/// `__rt_touch`. Timestamp fields encode whether each time is "now" via the -/// `TOUCH_*_NOW` flags in the control byte. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("touch()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emit_touch_args_aarch64(args, emitter, ctx, data); - emit_touch_tail_aarch64(emitter, ctx); - } - Arch::X86_64 => { - emit_touch_args_x86_64(args, emitter, ctx, data); - emit_touch_tail_x86_64(emitter, ctx); - } - } - Some(PhpType::Bool) -} - -/// Emits the wrapper-vs-libc dispatch tail for `touch()` on AArch64. -/// -/// On entry the path occupies `x1`/`x2`, the mtime `x3`, the atime `x4`, and the -/// current-time flags `x5` (as left by `emit_touch_args_aarch64`). A registered -/// `scheme://` path builds the `[mtime, atime]` value array via -/// `__rt_touch_meta_array` and dispatches to the wrapper's -/// `stream_metadata($path, STREAM_META_TOUCH, $value)` (vtable slot 14), -/// releasing the boxed value afterwards; any other path calls libc `__rt_touch`. -/// The bool result is left in `x0`. -fn emit_touch_tail_aarch64(emitter: &mut Emitter, ctx: &mut Context) { - let wrapper = ctx.next_label("touch_wrapper"); - let after = ctx.next_label("touch_after"); - emitter.instruction("sub sp, sp, #48"); // scratch: path ptr/len, mtime, atime, flags, result - emitter.instruction("str x1, [sp, #0]"); // save the path pointer - emitter.instruction("str x2, [sp, #8]"); // save the path length - emitter.instruction("str x3, [sp, #16]"); // save the mtime seconds - emitter.instruction("str x4, [sp, #24]"); // save the atime seconds - emitter.instruction("str x5, [sp, #32]"); // save the current-time flags - emitter.instruction("mov x0, x1"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov x1, x2"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // x0 = 1 when the scheme matches a registered wrapper - emitter.instruction(&format!("cbnz x0, {}", wrapper)); // registered wrapper scheme → stream_metadata - emitter.instruction("ldr x1, [sp, #0]"); // libc path ptr → x1 - emitter.instruction("ldr x2, [sp, #8]"); // libc path len → x2 - emitter.instruction("ldr x3, [sp, #16]"); // libc mtime → x3 - emitter.instruction("ldr x4, [sp, #24]"); // libc atime → x4 - emitter.instruction("ldr x5, [sp, #32]"); // libc current-time flags → x5 - emitter.instruction("add sp, sp, #48"); // release the scratch frame before the libc call - abi::emit_call_label(emitter, "__rt_touch"); // normal path: libc touch(path, mtime, atime, flags) - emitter.instruction(&format!("b {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("ldr x0, [sp, #16]"); // mtime → touch_meta_array arg0 - emitter.instruction("ldr x1, [sp, #24]"); // atime → touch_meta_array arg1 - emitter.instruction("ldr x2, [sp, #32]"); // flags → touch_meta_array arg2 - abi::emit_call_label(emitter, "__rt_touch_meta_array"); // x0 = boxed Mixed([mtime, atime]) - emitter.instruction("str x0, [sp, #16]"); // stash the boxed value pointer (mtime slot reused) - emitter.instruction("ldr x0, [sp, #0]"); // wrapper path ptr → x0 - emitter.instruction("ldr x1, [sp, #8]"); // wrapper path len → x1 - emitter.instruction(&format!("mov x2, #{}", STREAM_METADATA_SLOT)); // stream_metadata vtable slot - emitter.instruction(&format!("mov x3, #{}", STREAM_META_TOUCH)); // option = STREAM_META_TOUCH - emitter.instruction("ldr x4, [sp, #16]"); // value = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's stream_metadata - emitter.instruction("str x0, [sp, #0]"); // stash the bool result across the value release - emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed value pointer - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the boxed $value (caller owns; the method borrowed it) - emitter.instruction("ldr x0, [sp, #0]"); // restore the bool result - emitter.instruction("add sp, sp, #48"); // release the scratch frame - emitter.label(&after); -} - -/// Emits the wrapper-vs-libc dispatch tail for `touch()` on x86_64. -/// -/// On entry the path occupies `rax`/`rdx`, the mtime `rdi`, the atime `rsi`, and -/// the current-time flags `rcx` (as left by `emit_touch_args_x86_64`). Mirrors -/// `emit_touch_tail_aarch64`: a registered wrapper builds the value array and -/// dispatches to `stream_metadata`; any other path calls libc `__rt_touch`. The -/// bool result is left in `rax`. -fn emit_touch_tail_x86_64(emitter: &mut Emitter, ctx: &mut Context) { - let wrapper = ctx.next_label("touch_wrapper"); - let after = ctx.next_label("touch_after"); - emitter.instruction("sub rsp, 48"); // scratch: path ptr/len, mtime, atime, flags, result - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the path pointer - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the path length - emitter.instruction("mov QWORD PTR [rsp + 16], rdi"); // save the mtime seconds - emitter.instruction("mov QWORD PTR [rsp + 24], rsi"); // save the atime seconds - emitter.instruction("mov QWORD PTR [rsp + 32], rcx"); // save the current-time flags - emitter.instruction("mov rdi, rax"); // path_is_wrapper arg0 = path ptr - emitter.instruction("mov rsi, rdx"); // path_is_wrapper arg1 = path len - abi::emit_call_label(emitter, "__rt_path_is_wrapper"); // rax = 1 when the scheme matches a registered wrapper - emitter.instruction("test rax, rax"); // matched a registered wrapper scheme? - emitter.instruction(&format!("jnz {}", wrapper)); // registered wrapper scheme → stream_metadata - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // libc path ptr → rax - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // libc path len → rdx - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // libc mtime → rdi - emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // libc atime → rsi - emitter.instruction("mov rcx, QWORD PTR [rsp + 32]"); // libc current-time flags → rcx - emitter.instruction("add rsp, 48"); // release the scratch frame before the libc call - abi::emit_call_label(emitter, "__rt_touch"); // normal path: libc touch(path, mtime, atime, flags) - emitter.instruction(&format!("jmp {}", after)); // skip the wrapper path - emitter.label(&wrapper); - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // mtime → touch_meta_array arg0 - emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // atime → touch_meta_array arg1 - emitter.instruction("mov rdx, QWORD PTR [rsp + 32]"); // flags → touch_meta_array arg2 - abi::emit_call_label(emitter, "__rt_touch_meta_array"); // rax = boxed Mixed([mtime, atime]) - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // stash the boxed value pointer (mtime slot reused) - emitter.instruction("mov rdi, QWORD PTR [rsp + 0]"); // wrapper path ptr → rdi - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // wrapper path len → rsi - emitter.instruction(&format!("mov rdx, {}", STREAM_METADATA_SLOT)); // stream_metadata vtable slot - emitter.instruction(&format!("mov rcx, {}", STREAM_META_TOUCH)); // option = STREAM_META_TOUCH - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // value = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_user_wrapper_path_op"); // dispatch into the wrapper's stream_metadata - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // stash the bool result across the value release - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the boxed value pointer - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the boxed $value (caller owns; the method borrowed it) - emitter.instruction("mov rax, QWORD PTR [rsp + 0]"); // restore the bool result - emitter.instruction("add rsp, 48"); // release the scratch frame - emitter.label(&after); -} - -/// Materializes timestamp arguments for the `touch()` call on ARM64. -/// -/// # Arguments -/// - `args`: Expression tree for path, optional mtime, and optional atime. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context. -/// - `data`: Data section. -/// -/// # Behavior -/// The path pointer/len are already in x1/x2 when this is called. -/// The control byte (x5) flags whether "now" is used for each timestamp: -/// - `TOUCH_BOTH_NOW`: both atime and mtime use current time; x3/x4 are ignored. -/// - Otherwise: x3 = mtime seconds, x4 = atime seconds (defaults to mtime when atime is NULL). -/// -/// # Implementation notes -/// - `BothNow`: loads immediate zeros and `TOUCH_BOTH_NOW` into x3/x4/x5. -/// - `MtimeAlsoAtime`: evaluates `args[1]` into x0, copies to x3 and x4, sets control to 0. -/// - `ExplicitBoth`: nested evaluation of args[1] and args[2] with stack preservation -/// of path registers across the nested `emit_expr` calls. -fn emit_touch_args_aarch64( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - match touch_time_shape(args, ctx) { - TouchTimeShape::BothNow => { - emitter.instruction("mov x3, #0"); // ignored mtime seconds when runtime uses current time - emitter.instruction("mov x4, #0"); // ignored atime seconds when runtime uses current time - emitter.instruction(&format!("mov x5, #{}", TOUCH_BOTH_NOW)); // mark mtime and atime as current-time fields - } - TouchTimeShape::MtimeAlsoAtime => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve path while mtime is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x0"); // mtime seconds - emitter.instruction("mov x4, x0"); // atime defaults to mtime seconds - emitter.instruction("mov x5, #0"); // both timestamp fields are explicit - emitter.instruction("ldp x1, x2, [sp], #16"); // restore path ptr/len - } - TouchTimeShape::ExplicitBoth => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve path while timestamps are evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("str x0, [sp, #-16]!"); // save mtime seconds - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction("mov x4, x0"); // atime seconds - emitter.instruction("ldr x3, [sp], #16"); // restore mtime seconds - emitter.instruction("mov x5, #0"); // both timestamp fields are explicit - emitter.instruction("ldp x1, x2, [sp], #16"); // restore path ptr/len - } - } -} - -/// Materializes timestamp arguments for the `touch()` call on x86_64. -/// -/// # Arguments -/// - `args`: Expression tree for path, optional mtime, and optional atime. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context. -/// - `data`: Data section. -/// -/// # Behavior -/// The path pointer/len are already in rdi/rsi when this is called. -/// The control byte (rcx) flags whether "now" is used for each timestamp: -/// - `TOUCH_BOTH_NOW`: both atime and mtime use current time; rdi/rsi are ignored. -/// - Otherwise: rdi = mtime seconds, rsi = atime seconds (defaults to mtime when atime is NULL). -/// -/// # Implementation notes -/// - `BothNow`: loads immediate zeros and `TOUCH_BOTH_NOW` into rdi/rsi/rcx. -/// - `MtimeAlsoAtime`: uses `emit_push_reg_pair` to preserve rax/rdx across mtime evaluation, -/// then copies mtime into both rdi and rsi. -/// - `ExplicitBoth`: uses stack temporary to hold mtime across atime evaluation, with -/// aligned `sub rsp, 16` / `add rsp, 16` and `emit_push/pop_reg_pair` for path preservation. -fn emit_touch_args_x86_64( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - match touch_time_shape(args, ctx) { - TouchTimeShape::BothNow => { - emitter.instruction("mov rdi, 0"); // ignored mtime seconds when runtime uses current time - emitter.instruction("mov rsi, 0"); // ignored atime seconds when runtime uses current time - emitter.instruction(&format!("mov rcx, {}", TOUCH_BOTH_NOW)); // mark mtime and atime as current-time fields - } - TouchTimeShape::MtimeAlsoAtime => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve path while mtime is evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // mtime seconds - emitter.instruction("mov rsi, rax"); // atime defaults to mtime seconds - emitter.instruction("mov rcx, 0"); // both timestamp fields are explicit - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore path ptr/len - } - TouchTimeShape::ExplicitBoth => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve path while timestamps are evaluated - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("sub rsp, 16"); // reserve aligned temporary storage for mtime - emitter.instruction("mov QWORD PTR [rsp], rax"); // save mtime seconds - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction("mov rsi, rax"); // atime seconds - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // restore mtime seconds - emitter.instruction("add rsp, 16"); // release mtime temporary storage - emitter.instruction("mov rcx, 0"); // both timestamp fields are explicit - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore path ptr/len - } - } -} - -enum TouchTimeShape { - BothNow, - MtimeAlsoAtime, - ExplicitBoth, -} - -/// Categorizes the number of explicit timestamp arguments to `touch()`. -/// -/// # Arguments -/// - `args`: All arguments to the `touch()` call (path, optional mtime, optional atime). -/// - `ctx`: Codegen context for type inference. -/// -/// # Returns -/// - `BothNow`: Zero explicit timestamps, or both explicitly NULL. -/// - `MtimeAlsoAtime`: One timestamp argument (mtime), atime defaults to mtime. -/// - `ExplicitBoth`: Both mtime and atime are provided and non-NULL. -fn touch_time_shape(args: &[Expr], ctx: &Context) -> TouchTimeShape { - match args.len() { - 1 => TouchTimeShape::BothNow, - 2 if is_static_null(&args[1], ctx) => TouchTimeShape::BothNow, - 2 => TouchTimeShape::MtimeAlsoAtime, - _ if is_static_null(&args[1], ctx) && is_static_null(&args[2], ctx) => { - TouchTimeShape::BothNow - } - _ if is_static_null(&args[2], ctx) => TouchTimeShape::MtimeAlsoAtime, - _ => TouchTimeShape::ExplicitBoth, - } -} - -/// Checks whether an expression is statically known to be NULL or void. -/// -/// # Arguments -/// - `expr`: The expression to check. -/// - `ctx`: Codegen context for contextual type inference. -/// -/// # Returns -/// `true` if `expr` is a `Null` literal or inferred as `PhpType::Void`. -/// Used by `touch_time_shape` to treat NULL timestamps as "use current time". -fn is_static_null(expr: &Expr, ctx: &Context) -> bool { - matches!(expr.kind, ExprKind::Null) || infer_contextual_type(expr, ctx) == PhpType::Void -} diff --git a/src/codegen/builtins/io/umask.rs b/src/codegen/builtins/io/umask.rs deleted file mode 100644 index f4c7d50d0e..0000000000 --- a/src/codegen/builtins/io/umask.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Purpose: -//! Emits PHP `umask` filesystem mutation builtin calls. -//! Passes path and mode/owner arguments to runtime helpers that perform observable OS operations. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `umask()` PHP builtin. -/// -/// When called with no arguments, reads the current umask without modifying it. -/// Implemented as `umask(0)` then `umask(result)` to probe the previous value -/// portably; the saved previous mask is restored and returned as an integer. -/// -/// When called with one argument (mode), sets the umask to that value and -/// returns the previous umask as an integer. -/// -/// # Arguments -/// * `_name` — ignored; the builtin name is resolved via the catalog dispatch -/// * `args` — 0 or 1 expressions: the mode to set (int or expression coercing to int) -/// * `emitter` — target-aware instruction emitter -/// * `ctx` — codegen context (variable layout, ownership, class metadata) -/// * `data` — data section for relocations and literal storage -/// -/// # Returns -/// Always `Some(PhpType::Int)` — the previous umask value in both branches. -/// -/// # ABI -/// * ARM64: mask in `x0`, return in `x0` -/// * x86_64: mask in `rax`, return in `rax` -/// -/// # Effects -/// Calls `__rt_umask` runtime routine (effectful OS syscall). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("umask()"); - if args.is_empty() { - // PHP allows umask() with no args to read the current umask without - // changing it. The portable libc trick is to set umask(0) then - // immediately set it back. Here we approximate by setting umask(0) - // and then setting the returned value back, leaving the umask - // unchanged on the way out. - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // probe with mask = 0 - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // probe with mask = 0 - } - } - abi::emit_call_label(emitter, "__rt_umask"); // first call → returns previous mask - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("stp x0, xzr, [sp, #-16]!"); // save the probed previous mask - // Restore the original umask immediately. - // x0 now holds the previous mask; pass it back to umask(). - // The second call also returns the previous mask (which is the - // probed-zero value), so we ignore that return and restore x0. - emitter.instruction("ldr x0, [sp]"); // reload previous mask - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("push rax"); // save the probed previous mask - emitter.instruction("mov rax, QWORD PTR [rsp]"); // reload previous mask - } - } - abi::emit_call_label(emitter, "__rt_umask"); // restore the original umask - // Discard whatever the second call returned and restore the saved value. - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldp x0, xzr, [sp], #16"); // pop the saved previous mask back into x0 - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("pop rax"); // pop the saved previous mask back into rax - } - } - return Some(PhpType::Int); - } - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_umask"); // umask(mask) — returns previous mask - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/io/unlink.rs b/src/codegen/builtins/io/unlink.rs deleted file mode 100644 index 73427fc7bf..0000000000 --- a/src/codegen/builtins/io/unlink.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Emits PHP `unlink` filesystem mutation builtin calls. -//! Routes `scheme://` paths matching a registered userspace wrapper to the -//! wrapper's `unlink()` method; all other paths use the libc `__rt_unlink`. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - These calls are effectful and must preserve PHP-visible ordering and boolean failure results. -//! - The wrapper split mirrors `readfile()`: a `__rt_path_is_wrapper` probe picks -//! the wrapper branch (`__rt_user_wrapper_path_op` with the `unlink` vtable -//! slot 15) over the filesystem branch. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::path_op_wrapper::emit_single_path_wrapper_dispatch; - -/// `unlink` vtable slot index in the per-class user-wrapper vtable. -const UNLINK_SLOT: usize = 15; - -/// Emits code for the PHP `unlink(path)` builtin. -/// Consumes the path argument, dispatches a registered `scheme://` path to the -/// wrapper's `unlink()` (vtable slot 15) and any other path to the libc helper -/// `__rt_unlink`, then returns a bool (true on success, false on failure). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("unlink()"); - emit_expr(&args[0], emitter, ctx, data); - emit_single_path_wrapper_dispatch(emitter, ctx, "__rt_unlink", UNLINK_SLOT); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/io/var_dump.rs b/src/codegen/builtins/io/var_dump.rs deleted file mode 100644 index 525356a77d..0000000000 --- a/src/codegen/builtins/io/var_dump.rs +++ /dev/null @@ -1,678 +0,0 @@ -//! Purpose: -//! Emits PHP `var_dump` diagnostic output for scalar, array, and mixed values. -//! Owns recursive/runtime-aware formatting needed for PHP-visible stdout text. -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - Output is a side effect, and refcounted values must be inspected without consuming ownership. - -use crate::codegen::context::Context; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `write(fd=1, buf=literal, len=sizeof(literal))` syscall to stdout. -/// -/// Writes a compile-time-known byte string directly to stdout without any -/// runtime buffering or length computation. The string is stored in the data -/// section and referenced by address. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `data` - Data section where the literal string is placed -/// * `bytes` - The literal byte content to write -fn emit_write_literal(emitter: &mut Emitter, data: &mut DataSection, bytes: &[u8]) { - let (lbl, len) = data.add_string(bytes); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", &lbl); // resolve the literal string address - emitter.instruction(&format!("mov x2, #{}", len)); // pass the literal string length to write() - emitter.instruction("mov x0, #1"); // fd = stdout - emitter.syscall(4); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", &lbl); // point the Linux write() buffer register at the literal string bytes - emitter.instruction(&format!("mov edx, {}", len)); // pass the literal string length to write() - emitter.instruction("mov edi, 1"); // fd = stdout - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // write the literal bytes directly to stdout - } - } -} - -/// Emits a branch instruction when the integer payload is non-zero. -/// -/// Used to test whether a value is truthy or non-null without consuming -/// ownership. Dispatches to `b.ne` on ARM64 or `jne` on x86_64. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `label` - The target label for the branch when the condition is true -fn emit_branch_if_nonzero(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("b.ne {}", label)); // branch when the compared integer payload is non-zero - } - Arch::X86_64 => { - emitter.instruction(&format!("jne {}", label)); // branch when the compared integer payload is non-zero - } - } -} - -/// Emits a branch instruction when two compared values are equal. -/// -/// Dispatches to `b.eq` on ARM64 or `je` on x86_64. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `label` - The target label for the branch when the condition is true -fn emit_branch_if_eq(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("b.eq {}", label)); // branch when the compared values are equal - } - Arch::X86_64 => { - emitter.instruction(&format!("je {}", label)); // branch when the compared values are equal - } - } -} - -/// Emits a branch instruction when two compared values are different. -/// -/// Dispatches to `b.ne` on ARM64 or `jne` on x86_64. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `label` - The target label for the branch when the condition is true -fn emit_branch_if_ne(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("b.ne {}", label)); // branch when the compared values are different - } - Arch::X86_64 => { - emitter.instruction(&format!("jne {}", label)); // branch when the compared values are different - } - } -} - -/// Writes the current string result register to stdout. -/// -/// Uses the target ABI to emit the string pointer and length from the -/// string result registers through `__rt_write`. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -fn emit_write_current_string(emitter: &mut Emitter) { - abi::emit_write_stdout(emitter, &PhpType::Str); // write the current string result through the active target ABI -} - -/// Emits var_dump output for an integer payload. -/// -/// Checks the integer against the shared null sentinel (0x7fff_ffff_ffff_fffe). -/// If the payload is null, prints `NULL\n`. Otherwise prints `int(N)\n` -/// where N is the decimal conversion via `__rt_itoa`. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `ctx` - Codegen context (used for label allocation) -/// * `data` - Data section for literal strings -fn emit_var_dump_int(emitter: &mut Emitter, ctx: &mut Context, data: &mut DataSection) { - if crate::codegen::sentinels::null_repr_is_tagged() { - // Under the tagged representation a plain Int is never null; print the payload - // directly so the full i64 range (including PHP_INT_MAX - 1) round-trips. - emit_var_dump_int_payload(emitter, data); - return; - } - let not_null = ctx.next_label("vd_not_null"); - let done = ctx.next_label("vd_done"); - let result_reg = abi::int_result_reg(emitter); - let scratch_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_int_immediate(emitter, scratch_reg, NULL_SENTINEL); // materialize the shared null sentinel used by int-valued locals - emitter.instruction(&format!("cmp {}, {}", result_reg, scratch_reg)); // compare the incoming integer payload against the null sentinel - emit_branch_if_ne(emitter, ¬_null); // branch to the ordinary int path when the payload is not null - emit_write_literal(emitter, data, b"NULL\n"); - abi::emit_jump(emitter, &done); // skip the int formatter after printing NULL - emitter.label(¬_null); - emit_var_dump_int_payload(emitter, data); - emitter.label(&done); -} - -/// Emits `int(N)\n` for the integer payload in the result register without any null check. -/// Used directly for values that are statically known to be real ints (tagged scalar -/// non-null branch), and by `emit_var_dump_int` after its sentinel test. -fn emit_var_dump_int_payload(emitter: &mut Emitter, data: &mut DataSection) { - let result_reg = abi::int_result_reg(emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the integer payload before prefix writes clobber the integer result register - emit_write_literal(emitter, data, b"int("); - abi::emit_pop_reg(emitter, result_reg); // restore the integer payload after the prefix write - abi::emit_call_label(emitter, "__rt_itoa"); // convert the integer payload to decimal text through the target-aware runtime helper - emit_write_current_string(emitter); // write the converted decimal text to stdout - emit_write_literal(emitter, data, b")\n"); -} - -/// Emits var_dump output for a tagged scalar: `NULL\n` when the runtime tag is null, -/// otherwise `int(N)\n` for the payload with no in-band sentinel check (the full i64 -/// range is printable). -fn emit_var_dump_tagged_scalar(emitter: &mut Emitter, ctx: &mut Context, data: &mut DataSection) { - let null_case = ctx.next_label("vd_tagged_null"); - let done = ctx.next_label("vd_tagged_done"); - crate::codegen::sentinels::emit_branch_if_tagged_scalar_null(emitter, &null_case); - emit_var_dump_int_payload(emitter, data); - abi::emit_jump(emitter, &done); // skip the NULL literal after printing the tagged scalar payload - emitter.label(&null_case); - emit_var_dump_null(emitter, data); - emitter.label(&done); -} - -/// Emits var_dump output for a float payload. -/// -/// Prints `float(N)\n` where N is the decimal conversion of the float -/// in the floating-point result register via `__rt_ftoa`. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `data` - Data section for literal strings -fn emit_var_dump_float(emitter: &mut Emitter, data: &mut DataSection) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_call_label(emitter, "__rt_ftoa"); // convert the float payload to decimal text through the target-aware runtime helper - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the converted float string across literal writes - emit_write_literal(emitter, data, b"float("); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); // restore the converted float string after the prefix write - emit_write_current_string(emitter); // write the converted float text to stdout - emit_write_literal(emitter, data, b")\n"); -} - -/// Emits var_dump output for a string payload. -/// -/// Prints `string(LEN) "VALUE"\n` where LEN is the decimal string length -/// via `__rt_itoa` and VALUE is the raw string content in quotes. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `data` - Data section for literal strings -fn emit_var_dump_string(emitter: &mut Emitter, data: &mut DataSection) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the original string payload while printing the type prefix and quoted payload - emit_write_literal(emitter, data, b"string("); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #8]"); // load the preserved string length without consuming the saved payload pair - } - Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // load the preserved string length without consuming the saved payload pair - } - } - abi::emit_call_label(emitter, "__rt_itoa"); // convert the string length to decimal text through the target-aware runtime helper - emit_write_current_string(emitter); // write the decimal string length to stdout - emit_write_literal(emitter, data, b") \""); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); // restore the original string payload after the prefix writes finish - emit_write_current_string(emitter); // write the original quoted string payload to stdout - emit_write_literal(emitter, data, b"\"\n"); -} - -/// Emits var_dump output for a boolean payload. -/// -/// Prints `bool(false)\n` or `bool(true)\n` based on the integer result register. -/// The payload is expected in the standard integer result register. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `ctx` - Codegen context (used for label allocation) -/// * `data` - Data section for literal strings -fn emit_var_dump_bool(emitter: &mut Emitter, ctx: &mut Context, data: &mut DataSection) { - let true_label = ctx.next_label("vd_true"); - let done = ctx.next_label("vd_done"); - let result_reg = abi::int_result_reg(emitter); - emitter.instruction(&format!("cmp {}, 0", result_reg)); // test whether the boolean payload is false or true - emit_branch_if_nonzero(emitter, &true_label); // branch when the boolean payload is true - emit_write_literal(emitter, data, b"bool(false)\n"); - abi::emit_jump(emitter, &done); // skip the true branch after printing false - emitter.label(&true_label); - emit_write_literal(emitter, data, b"bool(true)\n"); - emitter.label(&done); -} - -/// Emits var_dump output for a resource payload. -/// -/// Prints `resource(N) of type (stream)\n` where N is the 1-based display id -/// (native payload + 1) via `__rt_itoa`. The native payload is preserved and -/// incremented before conversion. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `data` - Data section for literal strings -fn emit_var_dump_resource(emitter: &mut Emitter, data: &mut DataSection) { - let result_reg = abi::int_result_reg(emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the native resource payload before prefix writes clobber the result register - emit_write_literal(emitter, data, b"resource("); - abi::emit_pop_reg(emitter, result_reg); // restore the native resource payload for display-id formatting - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("add x0, x0, #1"); // convert the native resource payload into the 1-based display id - } - Arch::X86_64 => { - emitter.instruction("add rax, 1"); // convert the native resource payload into the 1-based display id - } - } - abi::emit_call_label(emitter, "__rt_itoa"); // convert the resource display id to decimal text - emit_write_current_string(emitter); // write the converted resource id to stdout - emit_write_literal(emitter, data, b") of type (stream)\n"); -} - -/// Emits var_dump output for a null/void/never payload. -/// -/// Prints `NULL\n`. Used as a fallback for types with no specific formatter. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `data` - Data section for literal strings -fn emit_var_dump_null(emitter: &mut Emitter, data: &mut DataSection) { - emit_write_literal(emitter, data, b"NULL\n"); -} - -/// Emits var_dump output for an array payload. -/// -/// Prints `array(N) {\n}\n` where N is the element count loaded from -/// the array/hash header via `__rt_itoa`. Does not recursively dump elements. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `data` - Data section for literal strings -fn emit_var_dump_array(emitter: &mut Emitter, data: &mut DataSection) { - emit_var_dump_array_with_elem(emitter, data, &PhpType::Mixed); -} - -/// Emit the var_dump body for an array/hash. The element type drives which -/// runtime walker is invoked: int arrays get \`__rt_var_dump_array_int\`, -/// string arrays \`__rt_var_dump_array_str\`. For other element shapes -/// (Hash, Mixed values) v1 prints just the header — the contents fall back -/// to the empty-body output. v2 will add a Mixed-aware walker that -/// dispatches per element tag. -fn emit_var_dump_array_with_elem( - emitter: &mut Emitter, - data: &mut DataSection, - elem_ty: &PhpType, -) { - let result_reg = abi::int_result_reg(emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the array pointer across the header write - emit_write_literal(emitter, data, b"array("); - abi::emit_pop_reg(emitter, result_reg); // restore the array pointer after the prefix write - abi::emit_push_reg(emitter, result_reg); // preserve it again for the per-element walker below - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [x0]"); // load the container element count from the array header - } - Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rax]"); // load the container element count from the array header - } - } - abi::emit_call_label(emitter, "__rt_itoa"); // convert the count to decimal text - emit_write_current_string(emitter); // write the count - emit_write_literal(emitter, data, b") {\n"); - abi::emit_pop_reg(emitter, result_reg); // restore the array pointer for the per-element walker - // Dispatch to a specialised walker when the element type is known to - // be homogeneous and one of the v1-supported scalar shapes. - let walker = match elem_ty { - PhpType::Int => Some("__rt_var_dump_array_int"), - PhpType::Str => Some("__rt_var_dump_array_str"), - PhpType::Bool => Some("__rt_var_dump_array_bool"), - PhpType::Float => Some("__rt_var_dump_array_float"), - // Mixed-element arrays need a per-element tag dispatch at runtime, - // but the static type `Array(Mixed)` reaches here both for arrays - // that were actually boxed as Mixed cells AND for arrays whose - // concrete element type was simply erased at the call site. The - // distinction is only visible through the array's value_type - // stamp at runtime, and conflating the two paths in the static - // dispatcher would corrupt the latter. Falling back to the - // header-only fallback keeps both cases printable, at the cost - // of empty bodies for genuine mixed-cell literals. - _ => None, - }; - if let Some(label) = walker { - if matches!(emitter.target.arch, Arch::X86_64) { - emitter.instruction("mov rdi, rax"); // move the array pointer into the SysV first-arg register - } - abi::emit_call_label(emitter, label); // walk the elements and emit per-element var_dump output - } - emit_write_literal(emitter, data, b"}\n"); -} - -/// Emits var_dump output for a callable payload. -/// -/// Prints `callable\n`. Used when a value's type is exactly `Callable` -/// (not a closure or invokable object). -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `data` - Data section for literal strings -fn emit_var_dump_callable(emitter: &mut Emitter, data: &mut DataSection) { - emit_write_literal(emitter, data, b"callable\n"); -} - -/// Emits var_dump output for a statically-known object class name. -/// -/// Prints `object(ClassName)\n` where ClassName is the known class. -/// Used for types that carry a resolved class name at codegen time. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `data` - Data section for literal strings -/// * `class_name` - The resolved class name to display -fn emit_var_dump_object_name(emitter: &mut Emitter, data: &mut DataSection, class_name: &str) { - let obj_str = format!("object({})\n", class_name); - emit_write_literal(emitter, data, obj_str.as_bytes()); -} - -/// Emits var_dump output for an object with runtime-determined class. -/// -/// Probes the heap kind via `__rt_heap_kind`, then performs a switch on -/// the runtime class id (loaded from the object header) to dispatch to -/// the matching `object(ClassName)` formatter. Falls back to `object\n` -/// for unknown class ids, and to `NULL\n` for null object pointers. -/// -/// # Arguments -/// * `emitter` - Target-aware instruction emitter -/// * `ctx` - Codegen context (used for label allocation and class metadata) -/// * `data` - Data section for literal strings -fn emit_var_dump_dynamic_object( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let mut classes: Vec<_> = ctx - .classes - .iter() - .map(|(class_name, class_info)| (class_name.clone(), class_info.class_id)) - .collect(); - classes.sort_by_key(|(_, class_id)| *class_id); - let mut cases = Vec::with_capacity(classes.len()); - let null_label = ctx.next_label("vd_object_null"); - let fallback = ctx.next_label("vd_object_fallback"); - let done = ctx.next_label("vd_object_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x0, {}", null_label)); // null object pointers print as NULL - emitter.instruction("ldr x9, [x0]"); // load the runtime class id from the object header - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // null object pointers print as NULL - emitter.instruction(&format!("je {}", null_label)); // branch to the null formatter for null object pointers - emitter.instruction("mov r11, QWORD PTR [rax]"); // load the runtime class id from the object header - } - } - for (class_name, class_id) in classes { - let case = ctx.next_label("vd_object_case"); - cases.push((case.clone(), class_name.clone())); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp x9, #{}", class_id)); // compare the runtime class id against a known class id - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp r11, {}", class_id)); // compare the runtime class id against a known class id - } - } - emit_branch_if_eq(emitter, &case); // branch when the class id matches this known class - } - abi::emit_jump(emitter, &fallback); // unknown runtime class ids fall back to a generic object marker - for (case, class_name) in cases { - emitter.label(&case); - emit_var_dump_object_name(emitter, data, &class_name); - abi::emit_jump(emitter, &done); // finish after printing the matching object class - } - emitter.label(&null_label); - emit_var_dump_null(emitter, data); - abi::emit_jump(emitter, &done); // finish after printing NULL for a null object pointer - emitter.label(&fallback); - emit_write_literal(emitter, data, b"object\n"); - emitter.label(&done); -} - -/// Emits PHP `var_dump` output for the first argument expression. -/// -/// Dispatches to a type-specific formatter based on the resolved type of -/// `args[0]`. Handles all PHP types: int, float, string, bool, resource, -/// null, array, object, callable, pointer, buffer, packed, and mixed/union -/// (which unboxes via `__rt_mixed_unbox` and re-dispatches). -/// -/// Does not consume ownership of the argument; values are inspected in place. -/// Returns `PhpType::Void` to indicate the call has side effects and yields no -/// value. -/// -/// # Arguments -/// * `_name` - The builtin name (unused; dispatch is by resolved type) -/// * `args` - Call arguments; only `args[0]` is formatted -/// * `emitter` - Target-aware instruction emitter -/// * `ctx` - Codegen context (label allocation, class metadata) -/// * `data` - Data section for literal strings -/// -/// # Returns -/// `Some(PhpType::Void)` because var_dump always produces output and returns null -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("var_dump()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - match &ty { - PhpType::Int => emit_var_dump_int(emitter, ctx, data), - PhpType::TaggedScalar => emit_var_dump_tagged_scalar(emitter, ctx, data), - PhpType::Float => emit_var_dump_float(emitter, data), - PhpType::Str => emit_var_dump_string(emitter, data), - PhpType::Bool => emit_var_dump_bool(emitter, ctx, data), - PhpType::Resource(_) => emit_var_dump_resource(emitter, data), - PhpType::Void | PhpType::Never => emit_var_dump_null(emitter, data), - PhpType::Iterable => { - // Iterable values are raw heap pointers. Probe the heap kind and reuse - // the array/object var_dump helpers directly, instead of routing through - // __rt_mixed_unbox which expects a Mixed cell layout. - let array_case = ctx.next_label("vd_iter_array"); - let object_case = ctx.next_label("vd_iter_object"); - let null_case = ctx.next_label("vd_iter_null"); - let done = ctx.next_label("vd_iter_done"); - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterable pointer across heap-kind probe - abi::emit_call_label(emitter, "__rt_heap_kind"); // x0/rax = heap kind tag for the iterable payload - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #2"); // iterable backed by indexed array? - emit_branch_if_eq(emitter, &array_case); // dispatch the array var_dump path - emitter.instruction("cmp x0, #3"); // iterable backed by hash table? - emit_branch_if_eq(emitter, &array_case); // hash tables also use the array var_dump path - emitter.instruction("cmp x0, #4"); // iterable backed by an object? - emit_branch_if_eq(emitter, &object_case); // dispatch the object var_dump path - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 2"); // iterable backed by indexed array? - emit_branch_if_eq(emitter, &array_case); // dispatch the array var_dump path - emitter.instruction("cmp rax, 3"); // iterable backed by hash table? - emit_branch_if_eq(emitter, &array_case); // hash tables also use the array var_dump path - emitter.instruction("cmp rax, 4"); // iterable backed by an object? - emit_branch_if_eq(emitter, &object_case); // dispatch the object var_dump path - } - } - abi::emit_jump(emitter, &null_case); // null pointers and unknown kinds print as NULL - - emitter.label(&array_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the iterable container pointer for the array var_dump prologue - emit_var_dump_array(emitter, data); - abi::emit_jump(emitter, &done); // finish after printing the array shell - - emitter.label(&object_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the iterable object pointer for the object var_dump prologue - emit_var_dump_dynamic_object(emitter, ctx, data); - abi::emit_jump(emitter, &done); // finish after printing the object marker - - emitter.label(&null_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // discard the saved iterable pointer on the null/fallback path - emit_var_dump_null(emitter, data); // print NULL for null/unknown iterable payloads - - emitter.label(&done); - } - PhpType::Mixed | PhpType::Union(_) => { - let int_case = ctx.next_label("vd_mixed_int"); - let string_case = ctx.next_label("vd_mixed_string"); - let float_case = ctx.next_label("vd_mixed_float"); - let bool_case = ctx.next_label("vd_mixed_bool"); - let resource_case = ctx.next_label("vd_mixed_resource"); - let array_case = ctx.next_label("vd_mixed_array"); - let object_case = ctx.next_label("vd_mixed_object"); - let null_case = ctx.next_label("vd_mixed_null"); - let done = ctx.next_label("vd_mixed_done"); - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // unwrap the boxed mixed payload before formatting it - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // does the mixed payload hold an int? - emit_branch_if_eq(emitter, &int_case); // ints reuse the ordinary int var_dump formatter - emitter.instruction("cmp x0, #1"); // does the mixed payload hold a string? - emit_branch_if_eq(emitter, &string_case); // strings reuse the ordinary string var_dump formatter - emitter.instruction("cmp x0, #2"); // does the mixed payload hold a float? - emit_branch_if_eq(emitter, &float_case); // floats reuse the ordinary float var_dump formatter - emitter.instruction("cmp x0, #3"); // does the mixed payload hold a bool? - emit_branch_if_eq(emitter, &bool_case); // bools reuse the ordinary bool var_dump formatter - emitter.instruction("cmp x0, #9"); // does the mixed payload hold a resource? - emit_branch_if_eq(emitter, &resource_case); // resources reuse the ordinary resource var_dump formatter - emitter.instruction("cmp x0, #4"); // does the mixed payload hold an indexed array? - emit_branch_if_eq(emitter, &array_case); // arrays reuse the ordinary array var_dump formatter - emitter.instruction("cmp x0, #5"); // does the mixed payload hold an associative array? - emit_branch_if_eq(emitter, &array_case); // associative arrays reuse the ordinary array var_dump formatter - emitter.instruction("cmp x0, #6"); // does the mixed payload hold an object/callable heap value? - emit_branch_if_eq(emitter, &object_case); // objects use runtime class-id dispatch for their name - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // does the mixed payload hold an int? - emit_branch_if_eq(emitter, &int_case); // ints reuse the ordinary int var_dump formatter - emitter.instruction("cmp rax, 1"); // does the mixed payload hold a string? - emit_branch_if_eq(emitter, &string_case); // strings reuse the ordinary string var_dump formatter - emitter.instruction("cmp rax, 2"); // does the mixed payload hold a float? - emit_branch_if_eq(emitter, &float_case); // floats reuse the ordinary float var_dump formatter - emitter.instruction("cmp rax, 3"); // does the mixed payload hold a bool? - emit_branch_if_eq(emitter, &bool_case); // bools reuse the ordinary bool var_dump formatter - emitter.instruction("cmp rax, 9"); // does the mixed payload hold a resource? - emit_branch_if_eq(emitter, &resource_case); // resources reuse the ordinary resource var_dump formatter - emitter.instruction("cmp rax, 4"); // does the mixed payload hold an indexed array? - emit_branch_if_eq(emitter, &array_case); // arrays reuse the ordinary array var_dump formatter - emitter.instruction("cmp rax, 5"); // does the mixed payload hold an associative array? - emit_branch_if_eq(emitter, &array_case); // associative arrays reuse the ordinary array var_dump formatter - emitter.instruction("cmp rax, 6"); // does the mixed payload hold an object/callable heap value? - emit_branch_if_eq(emitter, &object_case); // objects use runtime class-id dispatch for their name - } - } - abi::emit_jump(emitter, &null_case); // null and unknown tags print as NULL - - emitter.label(&int_case); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // move the unboxed int payload into the standard integer result register - } - Arch::X86_64 => { - emitter.instruction("mov rax, rdi"); // move the unboxed int payload into the standard integer result register - } - } - emit_var_dump_int(emitter, ctx, data); - abi::emit_jump(emitter, &done); // finish after printing the mixed int payload - - emitter.label(&string_case); - match emitter.target.arch { - Arch::AArch64 => {} - Arch::X86_64 => { - emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the standard string result register - } - } - emit_var_dump_string(emitter, data); // reuse the ordinary string var_dump formatter for mixed strings - abi::emit_jump(emitter, &done); // finish after printing the mixed string payload - - emitter.label(&float_case); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmov d0, x1"); // move the unboxed float bits into the floating-point result register - } - Arch::X86_64 => { - emitter.instruction("movq xmm0, rdi"); // move the unboxed float bits into the floating-point result register - } - } - emit_var_dump_float(emitter, data); - abi::emit_jump(emitter, &done); // finish after printing the mixed float payload - - emitter.label(&bool_case); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // move the unboxed bool payload into the standard integer result register - } - Arch::X86_64 => { - emitter.instruction("mov rax, rdi"); // move the unboxed bool payload into the standard integer result register - } - } - emit_var_dump_bool(emitter, ctx, data); - abi::emit_jump(emitter, &done); // finish after printing the mixed bool payload - - emitter.label(&resource_case); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // move the unboxed resource payload into the standard integer result register - } - Arch::X86_64 => { - emitter.instruction("mov rax, rdi"); // move the unboxed resource payload into the standard integer result register - } - } - emit_var_dump_resource(emitter, data); - abi::emit_jump(emitter, &done); // finish after printing the mixed resource payload - - emitter.label(&array_case); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // move the unboxed container pointer into the standard integer result register - } - Arch::X86_64 => { - emitter.instruction("mov rax, rdi"); // move the unboxed container pointer into the standard integer result register - } - } - emit_var_dump_array(emitter, data); - abi::emit_jump(emitter, &done); // finish after printing the mixed array payload - - emitter.label(&object_case); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // move the unboxed object pointer into the standard integer result register - } - Arch::X86_64 => { - emitter.instruction("mov rax, rdi"); // move the unboxed object pointer into the standard integer result register - } - } - emit_var_dump_dynamic_object(emitter, ctx, data); - abi::emit_jump(emitter, &done); // finish after printing the mixed object payload - - emitter.label(&null_case); - emit_var_dump_null(emitter, data); // print NULL for null/unknown mixed payloads - emitter.label(&done); - } - PhpType::Array(elem_ty) => { - emit_var_dump_array_with_elem(emitter, data, elem_ty); - } - PhpType::AssocArray { .. } => { - // Assoc-array layout differs (hash table, not contiguous - // 8-byte slots) — the v1 indexed-element walkers do not - // apply. Print just the `array(N) {\n}\n` shell. - emit_var_dump_array(emitter, data); - } - PhpType::Callable => emit_var_dump_callable(emitter, data), - PhpType::Object(class_name) => emit_var_dump_object_name(emitter, data, class_name), - PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) => { - // -- print pointer as hex address followed by newline -- - abi::emit_call_label(emitter, "__rt_ptoa"); // convert the pointer payload into the active target string result registers - emit_write_current_string(emitter); // write the converted pointer text to stdout - emit_write_literal(emitter, data, b"\n"); // terminate the pointer dump with a trailing newline - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/io/vfprintf.rs b/src/codegen/builtins/io/vfprintf.rs deleted file mode 100644 index c4e0d8a042..0000000000 --- a/src/codegen/builtins/io/vfprintf.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Purpose: -//! Emits PHP `vfprintf($stream, $format, $values)` — `fprintf` with the -//! arguments supplied as an array. Formats through the `__rt_vsprintf` -//! array→variadic bridge and writes the result to the stream via `__rt_fwrite` -//! (so write filters and userspace wrappers apply, exactly like `fwrite`). -//! -//! Called from: -//! - `crate::codegen::builtins::io::emit()`. -//! -//! Key details: -//! - The descriptor and the format string are stashed in the local frame so -//! they survive the `__rt_vsprintf` call (which uses its own frame for the -//! per-element records). Returns `PhpType::Int` (bytes written). - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::stream_arg::emit_stream_fd_arg; - -/// Emits a `vfprintf($stream, $format, $values)` call: format via -/// `__rt_vsprintf`, then `__rt_fwrite` the result to the stream descriptor. -/// Returns `Some(PhpType::Int)` (bytes written). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("vfprintf()"); - // args[0] = stream, args[1] = format, args[2] = values array. - emit_stream_fd_arg("vfprintf", &args[0], emitter, ctx, data); // fd → int-result reg - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("sub sp, sp, #32"); // frame: [sp,#0]=fd, [sp,#8..24]=format ptr/len - emitter.instruction("str x0, [sp, #0]"); // stash the descriptor across the format + vsprintf calls - emit_expr(&args[1], emitter, ctx, data); // format → x1/x2 - emitter.instruction("stp x1, x2, [sp, #8]"); // stash the format ptr/len across the array evaluation - emit_expr(&args[2], emitter, ctx, data); // values array → x0 - emitter.instruction("ldp x1, x2, [sp, #8]"); // restore the format ptr/len - abi::emit_call_label(emitter, "__rt_vsprintf"); // x1 = formatted ptr, x2 = formatted len - emitter.instruction("ldr x0, [sp, #0]"); // reload the descriptor (x1/x2 hold the payload) - abi::emit_call_label(emitter, "__rt_fwrite"); // write the formatted bytes, applying any write filter - emitter.instruction("add sp, sp, #32"); // release the frame (x0 = bytes written) - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 32"); // frame: [rsp]=fd, [rsp+8..24]=format ptr/len - emitter.instruction("mov QWORD PTR [rsp], rax"); // stash the descriptor across the format + vsprintf calls - emit_expr(&args[1], emitter, ctx, data); // format → rax/rdx - emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // stash the format ptr across the array evaluation - emitter.instruction("mov QWORD PTR [rsp + 16], rdx"); // stash the format len across the array evaluation - emit_expr(&args[2], emitter, ctx, data); // values array → rax - emitter.instruction("mov rdi, rax"); // array pointer → __rt_vsprintf first argument - emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // restore the format ptr - emitter.instruction("mov rdx, QWORD PTR [rsp + 16]"); // restore the format len - abi::emit_call_label(emitter, "__rt_vsprintf"); // rax = formatted ptr, rdx = formatted len - emitter.instruction("mov rsi, rax"); // formatted pointer → __rt_fwrite buffer arg (rdx=len in place) - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the descriptor → __rt_fwrite fd arg - abi::emit_call_label(emitter, "__rt_fwrite"); // write the formatted bytes, applying any write filter - emitter.instruction("add rsp, 32"); // release the frame (rax = bytes written) - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/math/abs.rs b/src/codegen/builtins/math/abs.rs deleted file mode 100644 index 3ce8b71c60..0000000000 --- a/src/codegen/builtins/math/abs.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Purpose: -//! Emits PHP `abs` numeric builtin calls. -//! Handles scalar argument lowering and returns the PHP numeric type promised by signature checking. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - Integer-vs-float result selection must stay aligned with PHP semantics and local type inference. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits inline assembly for PHP `abs($value)`. -/// -/// # Arguments -/// - `_name`: Unused; the caller guarantees this is `"abs"`. -/// - `args`: Must contain exactly one expression — the numeric operand. -/// - `emitter`: Write instruction stream here. -/// - `ctx`: Variable/layout context; `emit_expr` may allocate temps or load values. -/// - `data`: Data section for any literal payloads. -/// -/// # Returns -/// `Some(PhpType::Int)` if the operand is or promotes to integer, `Some(PhpType::Float)` otherwise. -/// Returns `None` only if `emit_expr` returns `None` (e.g. unsupported expression type). -/// -/// # Codegen behavior -/// - Float: uses IEEE-754 sign-bit masking via `fabs` (AArch64) or integer register tricks (x86_64). -/// - Integer: uses branchless two's-complement conditional-negate sequence. -/// - The result type must stay consistent with the type inferrer's expectations in the caller. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("abs()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if matches!(ty, PhpType::TaggedScalar) { - // narrow a tagged scalar (null -> 0) before the integer absolute-value sequence - crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(emitter); - } - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - // The operand is a boxed Mixed cell pointer, not a raw scalar; the runtime helper - // unboxes it, applies the integer or float absolute value per the stored tag, and - // reboxes — preserving PHP's int→int / float→float result typing. - crate::codegen::abi::emit_call_label(emitter, "__rt_abs_mixed"); - return Some(PhpType::Mixed); - } - if ty == PhpType::Float { - // -- float absolute value -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fabs d0, d0"); // take absolute value of the floating-point result in place - } - Arch::X86_64 => { - emitter.instruction("movq r10, xmm0"); // move the floating-point payload into a scratch integer register for sign-bit masking - emitter.instruction("mov r11, 0x7fffffffffffffff"); // materialize a mask that clears the IEEE-754 sign bit - emitter.instruction("and r10, r11"); // clear the sign bit so the payload becomes its absolute value - emitter.instruction("movq xmm0, r10"); // move the masked floating-point payload back into the result register - } - } - Some(PhpType::Float) - } else { - // -- integer absolute value via conditional negate -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // compare the integer value against zero - emitter.instruction("cneg x0, x0, lt"); // negate the integer result only when it was negative - } - Arch::X86_64 => { - emitter.instruction("mov r10, rax"); // copy the integer value into a scratch register before branchless sign handling - emitter.instruction("sar r10, 63"); // expand the sign bit to an all-zero or all-one mask - emitter.instruction("xor rax, r10"); // flip the payload bits when the original integer was negative - emitter.instruction("sub rax, r10"); // subtract the sign mask to finish the two's-complement absolute value - } - } - Some(PhpType::Int) - } -} diff --git a/src/codegen/builtins/math/acos.rs b/src/codegen/builtins/math/acos.rs deleted file mode 100644 index a301555ead..0000000000 --- a/src/codegen/builtins/math/acos.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Purpose: -//! Emits PHP `acos` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the PHP `acos` builtin, backed by the host libc `acos` routine. -/// -/// # Arguments -/// - `_name`: Unused; the builtin name is resolved by the dispatcher. -/// - `args`: Exactly one expression producing a float or integer value. -/// -/// # Behavior -/// - Normalizes integer operands to the floating-point result register via -/// `emit_int_result_to_float_result` before the libc call. -/// - Calls `acos` through the target's native calling convention (AArch64 `bl_c` -/// or x86_64 `call acos`). -/// -/// # Returns -/// `Some(PhpType::Float)` — `acos` always returns a float in PHP. -/// -/// # Panics -/// Requires `args.len() == 1` and a supported target architecture (AArch64, X86_64). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("acos()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer acos() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("acos"), // call libc acos() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call acos"), // call libc acos() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/asin.rs b/src/codegen/builtins/math/asin.rs deleted file mode 100644 index b9d72ffe72..0000000000 --- a/src/codegen/builtins/math/asin.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Purpose: -//! Emits PHP `asin` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `asin` builtin call with a single scalar argument, delegating to the -/// platform's libc `asin` function. Integer operands are normalized into the floating-point -/// result register before the call; floats are passed directly. Returns `PhpType::Float`. -/// -/// # Arguments -/// * `_name` - unused; kept for interface parity with other builtin emitters -/// * `args` - must contain exactly one expression (the angle in radians) -/// * `emitter` - drives instruction emission and exposes `target` -/// * `ctx` - carries variable layout and ownership state -/// * `data` - target data section for constants/literals -/// -/// # Aborts -/// Panics if `args` is empty or if `emitter.target` is an unsupported architecture. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("asin()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer asin() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("asin"), // call libc asin() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call asin"), // call libc asin() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/atan.rs b/src/codegen/builtins/math/atan.rs deleted file mode 100644 index 6a5efe55ca..0000000000 --- a/src/codegen/builtins/math/atan.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits PHP `atan` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the libc `atan` function for the first argument expression. -/// -/// # Arguments -/// - `args[0]` is evaluated and its value is passed to `atan()`. -/// - Integer arguments are normalized to float before the call via `emit_int_result_to_float_result`. -/// - The return type is always `PhpType::Float`. -/// -/// # Behavior -/// Calls the target-native `atan` routine (AArch64: `bl_c("atan")`, X86_64: `call atan`) -/// with the scalar in the native floating-point argument register. NaN and infinity -/// propagate according to libm semantics, which matches PHP's `atan` behavior. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("atan()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer atan() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("atan"), // call libc atan() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call atan"), // call libc atan() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/atan2.rs b/src/codegen/builtins/math/atan2.rs deleted file mode 100644 index 588f3fa1cc..0000000000 --- a/src/codegen/builtins/math/atan2.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Purpose: -//! Emits PHP `atan2` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the PHP `atan2(y, x)` builtin call. -/// -/// Evaluates `y` (first arg) and preserves it while `x` (second arg) is evaluated, -/// then calls the target libc `atan2` function. Both operands are normalized to -/// floating-point before the call. The return type is always `PhpType::Float`. -/// Target ABI: AArch64 passes `y` in `d0` and `x` in `d1`; x86_64 SysV passes -/// `y` in `xmm0` and `x` in `xmm1`. -/// -/// # Arguments -/// * `_name` – unused (builtin dispatch is by signature) -/// * `args` – exactly two expressions: `y` then `x` -/// * `emitter` – target assembly emitter -/// * `ctx` – compilation context (variables, types) -/// * `data` – data section for constants/literals -/// -/// # Returns -/// `Some(PhpType::Float)` – always a float result per PHP spec -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("atan2()"); - // -- evaluate y (first arg) -- - let t0 = emit_expr(&args[0], emitter, ctx, data); - if t0 != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the atan2() y operand into the active floating-point result register before it is preserved - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve the floating atan2() y operand while the x operand expression is evaluated - // -- evaluate x (second arg) -- - let t1 = emit_expr(&args[1], emitter, ctx, data); - if t1 != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the atan2() x operand into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmov d1, d0"); // move the floating atan2() x operand into the second AArch64 floating-point argument register - abi::emit_pop_float_reg(emitter, "d0"); // restore the floating atan2() y operand into the first AArch64 floating-point argument register - emitter.bl_c("atan2"); // delegate atan2(y, x) to libc on AArch64 - } - Arch::X86_64 => { - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the floating atan2() y operand into a scratch floating-point register before ordering the SysV libc arguments - emitter.instruction("movapd xmm2, xmm0"); // preserve the floating atan2() x operand while the y operand is moved into the first SysV floating-point argument register - emitter.instruction("movapd xmm0, xmm1"); // move the floating atan2() y operand into the first SysV floating-point argument register - emitter.instruction("movapd xmm1, xmm2"); // move the floating atan2() x operand into the second SysV floating-point argument register - emitter.instruction("call atan2"); // delegate atan2(y, x) to libc on linux-x86_64 - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/ceil.rs b/src/codegen/builtins/math/ceil.rs deleted file mode 100644 index 1a2c3f34a3..0000000000 --- a/src/codegen/builtins/math/ceil.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Purpose: -//! Emits PHP `ceil` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ceil(number)` builtin call, rounding its operand toward positive infinity. -/// -/// # Arguments -/// - `_name`: Unused; the builtin name is hardcoded as `ceil`. -/// - `args`: Single expression giving the number to round. -/// - `emitter`: Target-specific instruction emitter. -/// - `ctx`: Codegen context carrying variable layout and arch info. -/// - `data`: Data section for relocations and constant storage. -/// -/// # Returns -/// `Some(PhpType::Float)` — `ceil` always returns a float in PHP. -/// -/// # Codegen behavior -/// - Converts integer operands to float before rounding (SCVTF on ARM64, CVTSI2SD on x86_64). -/// - Uses `frintp` (ARM64) or `roundsd` with mode 2 (x86_64) to round toward +infinity. -/// - NaN and infinity inputs follow IEEE-754 rounding semantics. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ceil()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - if ty != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the ceil() input to float when it is an integer - } - emitter.instruction("frintp d0, d0"); // round toward plus infinity on AArch64 - } - Arch::X86_64 => { - if ty != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the ceil() input to float when it is an integer - } - emitter.instruction("roundsd xmm0, xmm0, 2"); // round toward plus infinity on x86_64 using SSE4.1 roundsd - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/clamp.rs b/src/codegen/builtins/math/clamp.rs deleted file mode 100644 index 51167de0af..0000000000 --- a/src/codegen/builtins/math/clamp.rs +++ /dev/null @@ -1,462 +0,0 @@ -//! Purpose: -//! Emits PHP 8.6 `clamp` builtin calls for integer, floating-point, string, and boxed numeric paths. -//! Validates PHP's bound rules before selecting the upper, lower, or original value. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - Bounds are checked before clamping; `$max` is tested before `$min` when selecting. -//! - Floating bounds reject NaN, and Mixed/Union call surfaces return a boxed Mixed float. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::functions::infer_contextual_type; -use crate::codegen::{abi, emit_box_current_value_as_mixed, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -const CLAMP_MIN_NAN_MESSAGE: &str = "clamp(): Argument #2 ($min) must not be NAN"; -const CLAMP_MAX_NAN_MESSAGE: &str = "clamp(): Argument #3 ($max) must not be NAN"; -const CLAMP_BOUNDS_MESSAGE: &str = - "clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)"; - -const MAX_SLOT: usize = 0; -const MIN_SLOT: usize = 16; -const VALUE_SLOT: usize = 32; -const CLAMP_STACK_BYTES: usize = 48; - -/// Lowers a PHP `clamp()` call into target assembly. -/// -/// The direct integer and all-string paths preserve their scalar result shape. -/// Float-like and Mixed paths normalize operands to doubles so bound validation, -/// NaN checks, and upper-before-lower selection can share one target-aware flow. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - if args.len() != 3 { - return None; - } - - emitter.comment("clamp()"); - let arg_types = clamp_arg_types(args, ctx); - if all_args_are_strings(&arg_types) { - return Some(emit_string_clamp(args, emitter, ctx, data)); - } - if all_args_are_ints(&arg_types) { - return Some(emit_int_clamp(args, emitter, ctx, data)); - } - - Some(emit_float_or_mixed_clamp( - args, - &arg_types, - emitter, - ctx, - data, - )) -} - -/// Infers contextual argument types before lowering so the emitter can choose the result representation. -fn clamp_arg_types(args: &[Expr], ctx: &Context) -> Vec { - args.iter() - .map(|arg| infer_contextual_type(arg, ctx).codegen_repr()) - .collect() -} - -/// Returns true when every argument is statically represented as a string. -fn all_args_are_strings(arg_types: &[PhpType]) -> bool { - arg_types.iter().all(|ty| matches!(ty, PhpType::Str)) -} - -/// Returns true when every argument is statically represented as an integer. -fn all_args_are_ints(arg_types: &[PhpType]) -> bool { - arg_types.iter().all(|ty| matches!(ty, PhpType::Int)) -} - -/// Returns true when the normalized float path must produce a boxed Mixed result. -fn float_path_returns_mixed(arg_types: &[PhpType]) -> bool { - arg_types - .iter() - .any(|ty| !matches!(ty, PhpType::Int | PhpType::Float)) -} - -/// Emits integer `clamp()` selection, including bound validation before the upper/lower tests. -fn emit_int_clamp( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - for arg in args { - emit_expr(arg, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - - let throw_label = ctx.next_label("clamp_int_invalid_bounds"); - let use_max_label = ctx.next_label("clamp_int_use_max"); - let use_min_label = ctx.next_label("clamp_int_use_min"); - let selected_label = ctx.next_label("clamp_int_selected"); - let finish_label = ctx.next_label("clamp_int_finish"); - let (message_label, message_len) = data.add_string(CLAMP_BOUNDS_MESSAGE.as_bytes()); - - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x9", MIN_SLOT); - abi::emit_load_temporary_stack_slot(emitter, "x10", MAX_SLOT); - emitter.instruction("cmp x9, x10"); // validate that the integer lower bound does not exceed the upper bound - emitter.instruction(&format!("b.gt {}", throw_label)); // throw ValueError when min > max before clamping - - abi::emit_load_temporary_stack_slot(emitter, "x9", VALUE_SLOT); - abi::emit_load_temporary_stack_slot(emitter, "x10", MAX_SLOT); - emitter.instruction("cmp x9, x10"); // compare the candidate against the upper bound first - emitter.instruction(&format!("b.gt {}", use_max_label)); // choose max when value is greater than the upper bound - abi::emit_load_temporary_stack_slot(emitter, "x10", MIN_SLOT); - emitter.instruction("cmp x9, x10"); // compare the candidate against the lower bound second - emitter.instruction(&format!("b.lt {}", use_min_label)); // choose min when value is lower than the lower bound - emitter.instruction("mov x0, x9"); // keep the original integer value when it is inside the bounds - abi::emit_jump(emitter, &selected_label); - - emitter.label(&use_max_label); - abi::emit_load_temporary_stack_slot(emitter, "x0", MAX_SLOT); - abi::emit_jump(emitter, &selected_label); - - emitter.label(&use_min_label); - abi::emit_load_temporary_stack_slot(emitter, "x0", MIN_SLOT); - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r9", MIN_SLOT); - abi::emit_load_temporary_stack_slot(emitter, "r10", MAX_SLOT); - emitter.instruction("cmp r9, r10"); // validate that the integer lower bound does not exceed the upper bound - emitter.instruction(&format!("jg {}", throw_label)); // throw ValueError when min > max before clamping - - abi::emit_load_temporary_stack_slot(emitter, "r9", VALUE_SLOT); - abi::emit_load_temporary_stack_slot(emitter, "r10", MAX_SLOT); - emitter.instruction("cmp r9, r10"); // compare the candidate against the upper bound first - emitter.instruction(&format!("jg {}", use_max_label)); // choose max when value is greater than the upper bound - abi::emit_load_temporary_stack_slot(emitter, "r10", MIN_SLOT); - emitter.instruction("cmp r9, r10"); // compare the candidate against the lower bound second - emitter.instruction(&format!("jl {}", use_min_label)); // choose min when value is lower than the lower bound - emitter.instruction("mov rax, r9"); // keep the original integer value when it is inside the bounds - abi::emit_jump(emitter, &selected_label); - - emitter.label(&use_max_label); - abi::emit_load_temporary_stack_slot(emitter, "rax", MAX_SLOT); - abi::emit_jump(emitter, &selected_label); - - emitter.label(&use_min_label); - abi::emit_load_temporary_stack_slot(emitter, "rax", MIN_SLOT); - } - } - - emitter.label(&selected_label); - abi::emit_release_temporary_stack(emitter, CLAMP_STACK_BYTES); - abi::emit_jump(emitter, &finish_label); - - emitter.label(&throw_label); - abi::emit_release_temporary_stack(emitter, CLAMP_STACK_BYTES); - emit_throw_value_error(emitter, &message_label, message_len); - - emitter.label(&finish_label); - PhpType::Int -} - -/// Emits floating-point `clamp()` selection and boxes the result when the static call surface is Mixed. -fn emit_float_or_mixed_clamp( - args: &[Expr], - arg_types: &[PhpType], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - for arg in args { - emit_arg_as_float(arg, emitter, ctx, data); - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - } - - let return_mixed = float_path_returns_mixed(arg_types); - let throw_min_nan_label = ctx.next_label("clamp_float_min_nan"); - let throw_max_nan_label = ctx.next_label("clamp_float_max_nan"); - let throw_bounds_label = ctx.next_label("clamp_float_invalid_bounds"); - let use_max_label = ctx.next_label("clamp_float_use_max"); - let use_min_label = ctx.next_label("clamp_float_use_min"); - let in_range_label = ctx.next_label("clamp_float_in_range"); - let selected_label = ctx.next_label("clamp_float_selected"); - let finish_label = ctx.next_label("clamp_float_finish"); - let (min_nan_label, min_nan_len) = data.add_string(CLAMP_MIN_NAN_MESSAGE.as_bytes()); - let (max_nan_label, max_nan_len) = data.add_string(CLAMP_MAX_NAN_MESSAGE.as_bytes()); - let (bounds_label, bounds_len) = data.add_string(CLAMP_BOUNDS_MESSAGE.as_bytes()); - - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "d1", MIN_SLOT); - emitter.instruction("fcmp d1, d1"); // detect NaN in the lower bound before any range comparison - emitter.instruction(&format!("b.vs {}", throw_min_nan_label)); // throw ValueError for a NaN lower bound - abi::emit_load_temporary_stack_slot(emitter, "d2", MAX_SLOT); - emitter.instruction("fcmp d2, d2"); // detect NaN in the upper bound before any range comparison - emitter.instruction(&format!("b.vs {}", throw_max_nan_label)); // throw ValueError for a NaN upper bound - emitter.instruction("fcmp d1, d2"); // validate that the lower bound does not exceed the upper bound - emitter.instruction(&format!("b.gt {}", throw_bounds_label)); // throw ValueError when min > max before clamping - - abi::emit_load_temporary_stack_slot(emitter, "d0", VALUE_SLOT); - emitter.instruction("fcmp d0, d2"); // compare the candidate against the upper bound first - emitter.instruction(&format!("b.vs {}", in_range_label)); // leave a NaN value unclamped because only bounds reject NaN - emitter.instruction(&format!("b.gt {}", use_max_label)); // choose max when value is greater than the upper bound - emitter.instruction("fcmp d0, d1"); // compare the candidate against the lower bound second - emitter.instruction(&format!("b.vs {}", in_range_label)); // leave a NaN value unclamped after the lower-bound comparison too - emitter.instruction(&format!("b.lt {}", use_min_label)); // choose min when value is lower than the lower bound - abi::emit_jump(emitter, &in_range_label); - - emitter.label(&use_max_label); - emitter.instruction("fmov d0, d2"); // return the upper bound when the candidate is too large - abi::emit_jump(emitter, &selected_label); - - emitter.label(&use_min_label); - emitter.instruction("fmov d0, d1"); // return the lower bound when the candidate is too small - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "xmm1", MIN_SLOT); - emitter.instruction("ucomisd xmm1, xmm1"); // detect NaN in the lower bound before any range comparison - emitter.instruction(&format!("jp {}", throw_min_nan_label)); // throw ValueError for a NaN lower bound - abi::emit_load_temporary_stack_slot(emitter, "xmm2", MAX_SLOT); - emitter.instruction("ucomisd xmm2, xmm2"); // detect NaN in the upper bound before any range comparison - emitter.instruction(&format!("jp {}", throw_max_nan_label)); // throw ValueError for a NaN upper bound - emitter.instruction("ucomisd xmm1, xmm2"); // validate that the lower bound does not exceed the upper bound - emitter.instruction(&format!("ja {}", throw_bounds_label)); // throw ValueError when min > max before clamping - - abi::emit_load_temporary_stack_slot(emitter, "xmm0", VALUE_SLOT); - emitter.instruction("ucomisd xmm0, xmm2"); // compare the candidate against the upper bound first - emitter.instruction(&format!("jp {}", in_range_label)); // leave a NaN value unclamped because only bounds reject NaN - emitter.instruction(&format!("ja {}", use_max_label)); // choose max when value is greater than the upper bound - emitter.instruction("ucomisd xmm0, xmm1"); // compare the candidate against the lower bound second - emitter.instruction(&format!("jp {}", in_range_label)); // leave a NaN value unclamped after the lower-bound comparison too - emitter.instruction(&format!("jb {}", use_min_label)); // choose min when value is lower than the lower bound - abi::emit_jump(emitter, &in_range_label); - - emitter.label(&use_max_label); - emitter.instruction("movsd xmm0, xmm2"); // return the upper bound when the candidate is too large - abi::emit_jump(emitter, &selected_label); - - emitter.label(&use_min_label); - emitter.instruction("movsd xmm0, xmm1"); // return the lower bound when the candidate is too small - } - } - - emitter.label(&in_range_label); - abi::emit_jump(emitter, &selected_label); - - emitter.label(&selected_label); - abi::emit_release_temporary_stack(emitter, CLAMP_STACK_BYTES); - if return_mixed { - emit_box_current_value_as_mixed(emitter, &PhpType::Float); - } - abi::emit_jump(emitter, &finish_label); - - emitter.label(&throw_min_nan_label); - abi::emit_release_temporary_stack(emitter, CLAMP_STACK_BYTES); - emit_throw_value_error(emitter, &min_nan_label, min_nan_len); - - emitter.label(&throw_max_nan_label); - abi::emit_release_temporary_stack(emitter, CLAMP_STACK_BYTES); - emit_throw_value_error(emitter, &max_nan_label, max_nan_len); - - emitter.label(&throw_bounds_label); - abi::emit_release_temporary_stack(emitter, CLAMP_STACK_BYTES); - emit_throw_value_error(emitter, &bounds_label, bounds_len); - - emitter.label(&finish_label); - if return_mixed { - PhpType::Mixed - } else { - PhpType::Float - } -} - -/// Emits an argument expression and normalizes its result to the active floating-point result register. -fn emit_arg_as_float( - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let ty = emit_expr(arg, emitter, ctx, data).codegen_repr(); - match ty { - PhpType::Float => {} - PhpType::Mixed => { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); - } - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_str_to_number"); - } - PhpType::Void | PhpType::Never => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_int_result_to_float_result(emitter); - } - _ => { - abi::emit_int_result_to_float_result(emitter); - } - } -} - -/// Emits all-string `clamp()` selection using `strcmp` ordering and PHP's upper-before-lower rule. -fn emit_string_clamp( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - for arg in args { - emit_expr(arg, emitter, ctx, data); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); - } - - let throw_label = ctx.next_label("clamp_string_invalid_bounds"); - let use_max_label = ctx.next_label("clamp_string_use_max"); - let use_min_label = ctx.next_label("clamp_string_use_min"); - let selected_label = ctx.next_label("clamp_string_selected"); - let finish_label = ctx.next_label("clamp_string_finish"); - let (message_label, message_len) = data.add_string(CLAMP_BOUNDS_MESSAGE.as_bytes()); - - emit_compare_string_slots(emitter, MIN_SLOT, MAX_SLOT); - emit_branch_if_string_compare_gt(emitter, &throw_label); - emit_compare_string_slots(emitter, VALUE_SLOT, MAX_SLOT); - emit_branch_if_string_compare_gt(emitter, &use_max_label); - emit_compare_string_slots(emitter, VALUE_SLOT, MIN_SLOT); - emit_branch_if_string_compare_lt(emitter, &use_min_label); - emit_load_string_slot_to_result(emitter, VALUE_SLOT); - abi::emit_jump(emitter, &selected_label); - - emitter.label(&use_max_label); - emit_load_string_slot_to_result(emitter, MAX_SLOT); - abi::emit_jump(emitter, &selected_label); - - emitter.label(&use_min_label); - emit_load_string_slot_to_result(emitter, MIN_SLOT); - - emitter.label(&selected_label); - abi::emit_release_temporary_stack(emitter, CLAMP_STACK_BYTES); - abi::emit_jump(emitter, &finish_label); - - emitter.label(&throw_label); - abi::emit_release_temporary_stack(emitter, CLAMP_STACK_BYTES); - emit_throw_value_error(emitter, &message_label, message_len); - - emitter.label(&finish_label); - PhpType::Str -} - -/// Compares two saved string slots with `__rt_strcmp` and leaves the integer result active. -fn emit_compare_string_slots(emitter: &mut Emitter, left_offset: usize, right_offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x1", left_offset); - abi::emit_load_temporary_stack_slot(emitter, "x2", left_offset + 8); - abi::emit_load_temporary_stack_slot(emitter, "x3", right_offset); - abi::emit_load_temporary_stack_slot(emitter, "x4", right_offset + 8); - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rdi", left_offset); - abi::emit_load_temporary_stack_slot(emitter, "rsi", left_offset + 8); - abi::emit_load_temporary_stack_slot(emitter, "rdx", right_offset); - abi::emit_load_temporary_stack_slot(emitter, "rcx", right_offset + 8); - } - } - abi::emit_call_label(emitter, "__rt_strcmp"); -} - -/// Branches to `label` when the most recent string comparison result is greater than zero. -fn emit_branch_if_string_compare_gt(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // test whether the left string sorted after the right string - emitter.instruction(&format!("b.gt {}", label)); // branch when the string comparison result is positive - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // test whether the left string sorted after the right string - emitter.instruction(&format!("jg {}", label)); // branch when the string comparison result is positive - } - } -} - -/// Branches to `label` when the most recent string comparison result is less than zero. -fn emit_branch_if_string_compare_lt(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // test whether the left string sorted before the right string - emitter.instruction(&format!("b.lt {}", label)); // branch when the string comparison result is negative - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // test whether the left string sorted before the right string - emitter.instruction(&format!("jl {}", label)); // branch when the string comparison result is negative - } - } -} - -/// Loads a saved string slot into the target's string result registers. -fn emit_load_string_slot_to_result(emitter: &mut Emitter, offset: usize) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, offset); - abi::emit_load_temporary_stack_slot(emitter, len_reg, offset + 8); -} - -/// Emits a catchable `ValueError` using a static message string. -fn emit_throw_value_error(emitter: &mut Emitter, message_symbol: &str, message_len: usize) { - match emitter.target.arch { - Arch::AArch64 => emit_throw_value_error_aarch64(emitter, message_symbol, message_len), - Arch::X86_64 => emit_throw_value_error_x86_64(emitter, message_symbol, message_len), - } -} - -/// Emits the AArch64 allocation and unwinder handoff for a `ValueError`. -fn emit_throw_value_error_aarch64( - emitter: &mut Emitter, - message_symbol: &str, - message_len: usize, -) { - emitter.instruction("mov x0, #32"); // request Throwable payload storage - emitter.instruction("bl __rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction("mov x9, #6"); // heap kind 6 = object instance - emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object - abi::emit_symbol_address(emitter, "x9", "_spl_value_error_class_id"); - emitter.instruction("ldr x9, [x9]"); // load ValueError's runtime class id for this program - emitter.instruction("str x9, [x0]"); // store class id at the object header - abi::emit_symbol_address(emitter, "x9", message_symbol); - emitter.instruction("str x9, [x0, #8]"); // store static ValueError message pointer - emitter.instruction(&format!("mov x9, #{}", message_len)); // load static ValueError message length - emitter.instruction("str x9, [x0, #16]"); // store exception message length - emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - abi::emit_symbol_address(emitter, "x9", "_exc_value"); - emitter.instruction("str x0, [x9]"); // publish the active exception object - emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder -} - -/// Emits the Linux x86_64 allocation and unwinder handoff for a `ValueError`. -fn emit_throw_value_error_x86_64( - emitter: &mut Emitter, - message_symbol: &str, - message_len: usize, -) { - emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation - emitter.instruction("mov rbp, rsp"); // establish aligned helper frame - emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned - emitter.instruction("mov rax, 32"); // request Throwable payload storage - emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object - abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_value_error_class_id", 0); // load ValueError's runtime class id for this program - emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at the object header - abi::emit_symbol_address(emitter, "r10", message_symbol); // materialize static ValueError message pointer - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static ValueError message pointer - emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store static ValueError message length - emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // publish the active exception object - emitter.instruction("mov rsp, rbp"); // release helper frame before throwing - emitter.instruction("pop rbp"); // restore caller frame pointer before throwing - emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder -} diff --git a/src/codegen/builtins/math/cos.rs b/src/codegen/builtins/math/cos.rs deleted file mode 100644 index 3260de4c72..0000000000 --- a/src/codegen/builtins/math/cos.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Emits PHP `cos` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `cos` builtin call for a single numeric argument. -/// -/// Loads the argument into the native floating-point argument register, calls -/// the platform's libc `cos` function, and returns `PhpType::Float`. Integer -/// operands are normalized into the floating-point result register before the call. -/// On AArch64 the scalar argument is in `d0`; on x86_64 it is in the SysV FP register. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("cos()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer cos() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("cos"), // call libc cos() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call cos"), // call libc cos() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/cosh.rs b/src/codegen/builtins/math/cosh.rs deleted file mode 100644 index 9d167186b1..0000000000 --- a/src/codegen/builtins/math/cosh.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `cosh` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `cosh()` call to the target's libc. -/// -/// Arguments: -/// - `_name`: unused, matches the builtin dispatcher signature -/// - `args[0]`: the operand, evaluated and left in the FP argument register -/// -/// Behavior: -/// - Emits the argument expression and normalizes non-Float types to the FP result register. -/// - Calls the platform's `cosh` libc function (AArch64: `bl cosh`, x86_64: `call cosh`). -/// - Returns `PhpType::Float`. NaN/infinity behavior follows libc's `cosh`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("cosh()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer cosh() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("cosh"), // call libc cosh() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call cosh"), // call libc cosh() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/deg2rad.rs b/src/codegen/builtins/math/deg2rad.rs deleted file mode 100644 index 96884bacbf..0000000000 --- a/src/codegen/builtins/math/deg2rad.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Emits PHP `deg2rad` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `deg2rad` builtin call. -/// -/// Converts a degree value to radians by multiplying with `M_PI / 180.0`. -/// -/// # Arguments -/// - `_name`: Unused; the builtin name is fixed (`deg2rad`). -/// - `args`: Single expression providing the degree value. -/// - `emitter`: Target-specific instruction emission. -/// - `ctx`: Codegen context (target architecture, current function frame). -/// - `data`: Data section for embedding floating-point constants. -/// -/// # Behavior -/// - Integer operands are normalized into the floating-point result register before conversion. -/// - Returns `Some(PhpType::Float)` since the result is always floating-point. -/// - Uses architecture-specific multiplication (`fmul` on AArch64, `mulsd` on x86_64). -/// - The conversion constant (`M_PI / 180.0`) is embedded in the data section. -/// -/// # ABI constraints -/// - AArch64: degree in `d0`, constant in `d1`, result in `d0`. -/// - x86_64: degree in `xmm0`, constant in `xmm1`, result in `xmm0`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("deg2rad()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the degree input into the active floating-point result register before applying the conversion factor - } - // -- multiply by M_PI / 180.0 to convert degrees to radians -- - let label = data.add_float(std::f64::consts::PI / 180.0); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_symbol_to_reg_via_page(emitter, "d1", "x9", &label); // load the degree-to-radian conversion constant into the secondary AArch64 floating-point register - emitter.instruction("fmul d0, d0, d1"); // multiply the degree input by the conversion constant in the standard AArch64 floating-point result register - } - Arch::X86_64 => { - abi::emit_load_symbol_to_reg(emitter, "xmm1", &label, 0); // load the degree-to-radian conversion constant into the secondary x86_64 floating-point register - emitter.instruction("mulsd xmm0, xmm1"); // multiply the degree input by the conversion constant in the standard x86_64 floating-point result register - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/exp.rs b/src/codegen/builtins/math/exp.rs deleted file mode 100644 index 75a5d4d7e1..0000000000 --- a/src/codegen/builtins/math/exp.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Purpose: -//! Emits PHP `exp` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `exp(x)` call to target assembly via the libc `exp` function. -/// -/// Arguments: -/// - `_name`: unused (保留 for API compatibility with the builtin dispatcher) -/// - `args`: single expression producing the exponent; must be int or float per signature -/// - `emitter`: target assembly emitter -/// - `ctx`: codegen context (variable layout, class metadata) -/// - `data`: data section for embedded constants -/// -/// Returns `Some(PhpType::Float)` — `exp` always returns float in PHP. -/// -/// Side effects: -/// - Emits the argument expression; if its type is `PhpType::Int`, emits an int-to-float -/// normalization step before the call so the libc function receives a float register value. -/// - Calls the platform's libc `exp()` using the native floating-point argument registers -/// (`x0`-`x7` on AArch64, ` xmm0`-`xmm7` on x86_64 SysV). -/// -/// ABI constraints: -/// - AArch64: scalar float arg in `d0`, result returned in `d0`. -/// - x86_64: scalar float arg in `xmm0`, result returned in `xmm0`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("exp()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer exp() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.bl_c("exp"); // call libc exp() with the scalar argument in the native AArch64 floating-point argument register - } - Arch::X86_64 => { - emitter.instruction("call exp"); // call libc exp() with the scalar argument in the native SysV floating-point argument register - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/fdiv.rs b/src/codegen/builtins/math/fdiv.rs deleted file mode 100644 index 1552f6437c..0000000000 --- a/src/codegen/builtins/math/fdiv.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Purpose: -//! Emits PHP `fdiv` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `fdiv(dividend, divisor)` builtin call. -/// -/// Converts the dividend to a double-precision float if it is an integer, then -/// preserves it in a float register while evaluating the divisor expression. The -/// divisor is similarly converted to float if needed before the division is -/// performed. On AArch64 the quotient is written directly to `d0`; on x86_64 the -/// result is moved from the left-hand scratch register to the standard result -/// register (`xmm0`). Returns `PhpType::Float`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fdiv()"); - let t0 = emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - if t0 != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the dividend to float when the first fdiv() argument is an integer - } - } - Arch::X86_64 => { - if t0 != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the dividend to float when the first fdiv() argument is an integer - } - } - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve the dividend while the divisor expression is evaluated - let t1 = emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - if t1 != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the divisor to float when the second fdiv() argument is an integer - } - abi::emit_pop_float_reg(emitter, "d1"); // restore the dividend into the left-hand floating-point scratch register - emitter.instruction("fdiv d0, d1, d0"); // compute dividend / divisor in the standard floating-point result register - } - Arch::X86_64 => { - if t1 != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the divisor to float when the second fdiv() argument is an integer - } - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the dividend into the left-hand floating-point scratch register - emitter.instruction("divsd xmm1, xmm0"); // compute dividend / divisor in the left-hand floating-point scratch register - emitter.instruction("movsd xmm0, xmm1"); // move the quotient back into the standard floating-point result register - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/floor.rs b/src/codegen/builtins/math/floor.rs deleted file mode 100644 index f115ab4787..0000000000 --- a/src/codegen/builtins/math/floor.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Purpose: -//! Emits PHP `floor` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `floor()` call, which rounds a value down to the nearest integer (toward minus infinity). -/// -/// # Arguments -/// - `_name`: Ignored; present for dispatcher consistency. -/// - `args`: Single operand to floor. May be a float or integer type. -/// - `emitter`: Target-aware instruction emitter. -/// - `ctx`: Codegen context carrying type and variable information. -/// - `data`: Data section for constants/literals. -/// -/// # Returns -/// Always returns `Some(PhpType::Float)` since floor always produces a floating-point result. -/// -/// # ABI & Instruction Details -/// - **AArch64**: Converts integer to double via `scvtf` if needed, then `frintm` (round toward -∞). -/// - **x86_64**: Converts integer to SSE2 double via `cvtsi2sd` if needed, then `roundsd` with mode 1 (round toward -∞). -/// -/// # Notes -/// PHP's `floor()` always returns a float, even for integer inputs. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("floor()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - if ty != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the floor() input to float when it is an integer - } - emitter.instruction("frintm d0, d0"); // round toward minus infinity on AArch64 - } - Arch::X86_64 => { - if ty != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the floor() input to float when it is an integer - } - emitter.instruction("roundsd xmm0, xmm0, 1"); // round toward minus infinity on x86_64 using SSE4.1 roundsd - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/fmod.rs b/src/codegen/builtins/math/fmod.rs deleted file mode 100644 index 3ba117e2dc..0000000000 --- a/src/codegen/builtins/math/fmod.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Purpose: -//! Emits PHP `fmod` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `fmod(x, y)` builtin call, returning the floating-point remainder of x / y. -/// -/// # Arguments -/// - `_name`: unused, matches the dispatcher contract. -/// - `args`: two expressions — the dividend `x` and the divisor `y`. -/// - `emitter`: target instruction emission. -/// - `ctx`: variable layout, ownership state, class/FFI metadata. -/// - `data`: read-only data section for relocations. -/// -/// # Returns -/// `Some(PhpType::Float)` — `fmod` always returns a float. -/// -/// # Behavior -/// Both operands are evaluated in source order. The dividend is preserved on the stack while -/// the divisor is evaluated so the ABI argument order can be satisfied. -/// - ARM64: converts integers to double (`scvtf`), then computes `dividend - trunc(dividend/divisor) * divisor` -/// using `frintz` + `fmsub` to match PHP/C `fmod` truncation-toward-zero semantics. -/// - x86_64: converts integers to double (`cvtsi2sd`), then delegates to `libc::fmod` via `call fmod`. -/// Division-by-zero and NaN inputs produce PHP-expected results via the underlying libm routines. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("fmod()"); - let t0 = emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - if t0 != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the dividend to float when the first fmod() argument is an integer - } - } - Arch::X86_64 => { - if t0 != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the dividend to float when the first fmod() argument is an integer - } - } - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve the dividend while the divisor expression is evaluated - let t1 = emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - if t1 != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the divisor to float when the second fmod() argument is an integer - } - abi::emit_pop_float_reg(emitter, "d1"); // restore the dividend into the left-hand floating-point scratch register - emitter.instruction("fdiv d2, d1, d0"); // compute dividend / divisor in a temporary floating-point register - emitter.instruction("frintz d2, d2"); // truncate the quotient toward zero to match PHP/C fmod semantics - emitter.instruction("fmsub d0, d2, d0, d1"); // compute dividend - trunc(dividend/divisor) * divisor as the floating remainder - } - Arch::X86_64 => { - if t1 != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the divisor to float when the second fmod() argument is an integer - } - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the dividend into the left-hand floating-point argument register - emitter.instruction("movapd xmm2, xmm0"); // preserve the divisor while rearranging the floating-point libc fmod() arguments - emitter.instruction("movapd xmm0, xmm1"); // move the dividend into the first libc fmod() floating-point argument register - emitter.instruction("movapd xmm1, xmm2"); // move the divisor into the second libc fmod() floating-point argument register - emitter.instruction("call fmod"); // delegate the floating remainder semantics to libc fmod() on linux-x86_64 - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/hypot.rs b/src/codegen/builtins/math/hypot.rs deleted file mode 100644 index ce33949cb2..0000000000 --- a/src/codegen/builtins/math/hypot.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Purpose: -//! Emits PHP `hypot` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `hypot($x, $y)` builtin call, delegating to the target libc. -/// -/// # Arguments -/// - `args`: Two expressions for the x and y operands. -/// - `emitter`: Target instruction emitter. -/// - `ctx`: Codegen context carrying variable layout and metadata. -/// - `data`: Read-only data section for constants. -/// -/// # Returns -/// Always returns `Some(PhpType::Float)`. The `Option` satisfies the -/// builtin-emitter interface even though `hypot` always produces a float. -/// -/// # Implementation notes -/// - Each argument is evaluated and normalized to a floating-point value. -/// - The x operand is saved to the stack while y is evaluated to avoid -/// clobbering the first argument register. -/// - AArch64: passes x in `d0`, y in `d1`, calls `hypot` via `bl`. -/// - X86_64 (SysV): passes x in `xmm0`, y in `xmm1`, calls `hypot` via `call`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hypot()"); - // -- evaluate x (first arg) -- - let t0 = emit_expr(&args[0], emitter, ctx, data); - if t0 != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the hypot() x operand into the active floating-point result register before it is preserved - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve the floating hypot() x operand while the y operand expression is evaluated - // -- evaluate y (second arg) -- - let t1 = emit_expr(&args[1], emitter, ctx, data); - if t1 != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the hypot() y operand into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmov d1, d0"); // move the floating hypot() y operand into the second AArch64 floating-point argument register - abi::emit_pop_float_reg(emitter, "d0"); // restore the floating hypot() x operand into the first AArch64 floating-point argument register - emitter.bl_c("hypot"); // delegate hypot(x, y) to libc on AArch64 - } - Arch::X86_64 => { - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the floating hypot() x operand into a scratch floating-point register before ordering the SysV libc arguments - emitter.instruction("movapd xmm2, xmm0"); // preserve the floating hypot() y operand while the x operand is moved into the first SysV floating-point argument register - emitter.instruction("movapd xmm0, xmm1"); // move the floating hypot() x operand into the first SysV floating-point argument register - emitter.instruction("movapd xmm1, xmm2"); // move the floating hypot() y operand into the second SysV floating-point argument register - emitter.instruction("call hypot"); // delegate hypot(x, y) to libc on linux-x86_64 - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/intdiv.rs b/src/codegen/builtins/math/intdiv.rs deleted file mode 100644 index ab883eacb5..0000000000 --- a/src/codegen/builtins/math/intdiv.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Purpose: -//! Emits PHP `intdiv` numeric builtin calls. -//! Handles scalar argument lowering and returns the PHP numeric type promised by signature checking. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - Integer-vs-float result selection must stay aligned with PHP semantics and local type inference. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `intdiv()` builtin call. -/// -/// Lowers two argument expressions to integer values, performs signed integer -/// division, and returns the result in `x0` (AArch64) or `rax` (x86_64). -/// -/// # Arguments -/// - `_name`: Unused; the caller dispatches by name. -/// - `args`: Two expressions evaluated left-to-right; dividend first, divisor second. -/// -/// # Behavior -/// - Coerces each operand to a raw integer via `coerce_to_int` (unboxing `Mixed`/`Union` values -/// through `__rt_mixed_cast_int`) so a boxed operand is never divided as if it were a raw int. -/// - Evaluates `args[0]` (dividend) and preserves it across evaluation of `args[1]` (divisor). -/// - On x86_64, the preserved dividend is held in `r11` during divisor evaluation, then moved -/// into `rax` before `idiv` (which uses the fixed `rax:rdx` pair for the dividend). -/// - Checks for division by zero; if the divisor is zero, emits a fatal error message to stderr -/// and terminates the process with exit code 1. -/// - On success, returns `PhpType::Int`. -/// -/// # ABI constraints -/// - AArch64: dividend in `x0`, divisor consumed into `x0`, result returned in `x0`. -/// - x86_64: dividend moved through `r11` → `rax`, divisor held in `r10`, quotient left in `rax`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("intdiv()"); - let zero_label = ctx.next_label("intdiv_zero"); - let done_label = ctx.next_label("intdiv_done"); - match emitter.target.arch { - Arch::AArch64 => { - // -- integer division: dividend / divisor -- - let dividend_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, ÷nd_ty); // unbox a Mixed/Union dividend into a raw integer before saving it - abi::emit_push_reg(emitter, "x0"); // preserve the dividend while evaluating the divisor expression - let divisor_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &divisor_ty); // unbox a Mixed/Union divisor into a raw integer before the division - abi::emit_pop_reg(emitter, "x1"); // restore the dividend into the left-hand division register - - // -- division by zero guard -- - emitter.instruction(&format!("cbz x0, {zero_label}")); // if the divisor is 0, branch to the fatal error path - emitter.instruction("sdiv x0, x1, x0"); // divide the saved dividend by the current divisor - emitter.instruction(&format!("b {done_label}")); // skip the fatal error path after a successful division - } - Arch::X86_64 => { - // -- integer division: dividend / divisor -- - let dividend_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, ÷nd_ty); // unbox a Mixed/Union dividend into a raw integer before saving it - abi::emit_push_reg(emitter, "rax"); // preserve the dividend while evaluating the divisor expression - let divisor_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &divisor_ty); // unbox a Mixed/Union divisor into a raw integer before the division - abi::emit_pop_reg(emitter, "r11"); // restore the dividend into a scratch register before idiv clobbers rax/rdx - - // -- division by zero guard -- - emitter.instruction("test rax, rax"); // check whether the divisor expression evaluated to zero - emitter.instruction(&format!("je {}", zero_label)); // branch to the fatal error path when the divisor is zero - emitter.instruction("mov r10, rax"); // preserve the divisor because idiv requires the dividend in rax - emitter.instruction("mov rax, r11"); // move the saved dividend into the mandatory idiv accumulator register - emitter.instruction("cqo"); // sign-extend the dividend into rdx:rax for signed division - emitter.instruction("idiv r10"); // divide the dividend by the preserved divisor and leave the quotient in rax - emitter.instruction(&format!("jmp {}", done_label)); // skip the fatal error path after a successful division - } - } - - // -- fatal error: division by zero -- - emitter.label(&zero_label); - let (err_label, err_len) = data.add_string(b"Fatal error: division by zero\n"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // fd = stderr - abi::emit_symbol_address(emitter, "x1", &err_label); // resolve the fatal error string address - emitter.instruction(&format!("mov x2, #{}", err_len)); // pass the fatal error string length to write() - emitter.syscall(4); - emitter.instruction("mov x0, #1"); // exit code 1 - emitter.syscall(1); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", &err_label); // point the Linux write() buffer register at the fatal error string - emitter.instruction(&format!("mov edx, {}", err_len)); // pass the fatal error string length to write() - emitter.instruction("mov edi, 2"); // fd = stderr - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal division-by-zero message before terminating - emitter.instruction("mov edi, 1"); // exit code 1 - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate the process after reporting division by zero - } - } - - emitter.label(&done_label); - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/math/log.rs b/src/codegen/builtins/math/log.rs deleted file mode 100644 index 35ca0158f9..0000000000 --- a/src/codegen/builtins/math/log.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Purpose: -//! Emits PHP `log` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `log($num)` or `log($num, $base)` as a call to libc `log()`. -/// -/// For a single argument, computes the natural logarithm directly. For two -/// arguments, computes `log($num) / log($base)` (change of base formula). -/// -/// Integer operands are normalized to floating-point before the libc call via -/// `emit_int_result_to_float_result`. The result is always `PhpType::Float`. -/// Both the numerator and denominator are preserved across the second `log()` -/// call using a stack push/pop on both architectures. -/// -/// ABI: SysV on x86_64 (scalar float args in `xmm0`-`xmm7`), AAPCS on AArch64 -/// (scalar float args in `d0`-`d7`). Return value in `xmm0` / `d0`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("log()"); - if args.len() == 1 { - // -- log($num) — natural logarithm -- - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer log() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.bl_c("log"); // call libc log() with the scalar argument in the native AArch64 floating-point argument register - } - Arch::X86_64 => { - emitter.instruction("call log"); // call libc log() with the scalar argument in the native SysV floating-point argument register - } - } - } else { - // -- log($num, $base) — change of base: log($num) / log($base) -- - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the logarithm value operand into the active floating-point result register before the first libc call - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.bl_c("log"); // compute log($num) through libc with the AArch64 floating-point calling convention - } - Arch::X86_64 => { - emitter.instruction("call log"); // compute log($num) through libc with the SysV x86_64 floating-point calling convention - } - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve log($num) while the logarithm base expression is evaluated and converted - let ty2 = emit_expr(&args[1], emitter, ctx, data); - if ty2 != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the logarithm base operand into the active floating-point result register before the second libc call - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.bl_c("log"); // compute log($base) through libc with the AArch64 floating-point calling convention - emitter.instruction("fmov d1, d0"); // preserve log($base) in the secondary AArch64 floating-point scratch register - abi::emit_pop_float_reg(emitter, "d0"); // restore log($num) into the primary AArch64 floating-point result register - emitter.instruction("fdiv d0, d0, d1"); // compute the change-of-base quotient in the standard AArch64 floating-point result register - } - Arch::X86_64 => { - emitter.instruction("call log"); // compute log($base) through libc with the SysV x86_64 floating-point calling convention - abi::emit_pop_float_reg(emitter, "xmm1"); // restore log($num) into a scratch floating-point register before forming the change-of-base quotient - emitter.instruction("divsd xmm1, xmm0"); // divide log($num) by log($base) in the scratch floating-point register - emitter.instruction("movsd xmm0, xmm1"); // move the change-of-base quotient back into the standard x86_64 floating-point result register - } - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/log10.rs b/src/codegen/builtins/math/log10.rs deleted file mode 100644 index 95cca6c6de..0000000000 --- a/src/codegen/builtins/math/log10.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Purpose: -//! Emits PHP `log10` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `log10($arg)` builtin call, computing the base-10 logarithm of the argument. -/// -/// Inputs: -/// - `args[0]` is the operand, which may be an integer or float. Integer operands are -/// normalized to floating-point before the libc call. -/// - `emitter` is used to emit the conversion, call, and any target-specific instructions. -/// - `ctx` carries variable layout and metadata through the call. -/// - `data` provides access to the data section for any constant materialization. -/// -/// Outputs: -/// - Always returns `Some(PhpType::Float)` since `log10` produces a float result. -/// -/// Side effects: -/// - Emits an `emit_int_result_to_float_result` call when the operand is an integer, -/// converting the integer in the integer result register to the float argument register. -/// - Calls the platform's libc `log10` function via `bl_c` (AArch64) or `call` (x86_64). -/// - On AArch64 the scalar argument is in `d0`; on x86_64 it follows the SysV float ABI. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("log10()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer log10() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.bl_c("log10"); // call libc log10() with the scalar argument in the native AArch64 floating-point argument register - } - Arch::X86_64 => { - emitter.instruction("call log10"); // call libc log10() with the scalar argument in the native SysV floating-point argument register - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/log2.rs b/src/codegen/builtins/math/log2.rs deleted file mode 100644 index 3c03ce400b..0000000000 --- a/src/codegen/builtins/math/log2.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `log2` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `log2` builtin call. -/// -/// Evaluates `args[0]` and converts integer operands to float before the libc call. -/// Dispatches to the target C library's `log2` function and returns `PhpType::Float`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("log2()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer log2() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.bl_c("log2"); // call libc log2() with the scalar argument in the native AArch64 floating-point argument register - } - Arch::X86_64 => { - emitter.instruction("call log2"); // call libc log2() with the scalar argument in the native SysV floating-point argument register - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/max.rs b/src/codegen/builtins/math/max.rs deleted file mode 100644 index e9d85d9991..0000000000 --- a/src/codegen/builtins/math/max.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Purpose: -//! Emits PHP `max` numeric builtin calls. -//! Handles scalar argument lowering and returns the PHP numeric type promised by signature checking. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - Integer-vs-float result selection must stay aligned with PHP semantics and local type inference. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `max()` builtin with scalar numeric arguments. -/// -/// Iterates over all arguments, maintaining the current maximum in the ABI result -/// register. Each subsequent argument is evaluated and compared, with the larger -/// value written back into the result register. Float arguments trigger promotion -/// of all prior integer candidates before comparison. -/// -/// # Arguments -/// * `_name` — unused; present for dispatcher uniformity with other builtins -/// * `args` — evaluated left-to-right; must contain at least one expression -/// * `emitter` — receives the comparison/selection instructions; carries target arch -/// * `ctx` — variable and type context for expression emission -/// * `data` — data section for any literals or runtime constants -/// -/// # Returns -/// `Some(PhpType::Float)` if any argument was a float (all prior ints promoted); -/// `Some(PhpType::Int)` if all arguments were integers. -/// -/// # ABI behavior -/// * AArch64: integer results in `x0`; floats in `d0`; scratch register `x1`/`d1` -/// * X86_64: integer results in `rax`; floats in `xmm0`; scratch register `r9`/`xmm1` -/// -/// # Side effects -/// * Stack: one 16-byte slot is pushed per iteration to preserve the running maximum -/// while the next candidate is evaluated; pops are architecture-aware (int vs float) -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("max()"); - - // -- evaluate first arg -- - let t0 = emit_expr(&args[0], emitter, ctx, data); - let mut any_float = t0 == PhpType::Float; - - for (i, arg) in args.iter().enumerate().skip(1) { - // -- push current maximum onto stack -- - if any_float { - if i == 1 && t0 != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the first max() operand into the active floating-point result register before it becomes the running floating maximum - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve the current floating maximum while the next candidate expression is evaluated - } else { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the current integer maximum while the next candidate expression is evaluated - } - - let ti = emit_expr(arg, emitter, ctx, data); - - if any_float || ti == PhpType::Float { - // -- float comparison path -- - if ti != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the new max() candidate into the active floating-point result register before the floating comparison - } - if !any_float { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x9"); // restore the previous integer maximum before promoting it into the floating comparison path - emitter.instruction("scvtf d1, x9"); // convert the previous integer maximum into the secondary AArch64 floating-point scratch register - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "r9"); // restore the previous integer maximum before promoting it into the floating comparison path - emitter.instruction("cvtsi2sd xmm1, r9"); // convert the previous integer maximum into the secondary x86_64 floating-point scratch register - } - } - } else { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_float_reg(emitter, "d1"); // restore the previous floating maximum into the secondary AArch64 floating-point scratch register - } - Arch::X86_64 => { - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the previous floating maximum into the secondary x86_64 floating-point scratch register - } - } - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmax d0, d1, d0"); // compute the larger of the previous and new floating candidates in the standard AArch64 floating-point result register - } - Arch::X86_64 => { - emitter.instruction("maxsd xmm1, xmm0"); // compute the larger of the previous and new floating candidates in the secondary x86_64 floating-point scratch register - emitter.instruction("movsd xmm0, xmm1"); // move the updated floating maximum back into the standard x86_64 floating-point result register - } - } - any_float = true; - } else { - // -- integer comparison path -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x1, [sp], #16"); // restore the previous integer maximum into the AArch64 scratch register before the scalar comparison - emitter.instruction("cmp x1, x0"); // compare the previous integer maximum against the new integer candidate - emitter.instruction("csel x0, x1, x0, gt"); // select the larger integer value into the standard AArch64 integer result register - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "r9"); // restore the previous integer maximum into a scratch register before the scalar comparison - emitter.instruction("cmp r9, rax"); // compare the previous integer maximum against the new integer candidate - emitter.instruction("cmovg rax, r9"); // keep the larger integer value in the standard x86_64 integer result register - } - } - } - } - - if any_float { - Some(PhpType::Float) - } else { - Some(PhpType::Int) - } -} diff --git a/src/codegen/builtins/math/min.rs b/src/codegen/builtins/math/min.rs deleted file mode 100644 index d3d1357064..0000000000 --- a/src/codegen/builtins/math/min.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! Purpose: -//! Emits PHP `min` numeric builtin calls. -//! Handles scalar argument lowering and returns the PHP numeric type promised by signature checking. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - Integer-vs-float result selection must stay aligned with PHP semantics and local type inference. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Lowers a PHP `min()` call into target assembly. -/// -/// Iterates arguments pairwise, pushing the current running minimum before evaluating -/// the next candidate. After each candidate is evaluated, a target-specific comparison -/// selects the smaller value back into the standard result register. -/// -/// # Arguments -/// * `_name` - Unused; present to match the builtin emitter signature. -/// * `args` - Non-empty slice of AST expressions passed to `min()`. -/// * `emitter` - Code emitter for instruction emission and target information. -/// * `ctx` - Codegen context (variable layout, class metadata, etc.). -/// * `data` - Data section for embedded literal tables. -/// -/// # Returns -/// `Some(PhpType::Float)` if any argument is a float; `Some(PhpType::Int)` otherwise. -/// Returns `None` only if `args` is empty (caller is responsible for validation). -/// -/// # ABI constraints -/// - ARM64: integer results in `x0`; float results in `d0`. -/// - x86_64: integer results in `rax`; float results in `xmm0`. -/// - Each comparison step preserves the running minimum on the stack or in a scratch -/// register so the next candidate can be evaluated without clobbering it. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("min()"); - - // -- evaluate first arg -- - let t0 = emit_expr(&args[0], emitter, ctx, data); - let mut any_float = t0 == PhpType::Float; - - // -- check all arg types for float promotion -- - // We need to know upfront if any arg is float so we use a consistent register - // For simplicity, we'll track float dynamically per pair - - for (i, arg) in args.iter().enumerate().skip(1) { - // -- push current minimum onto stack -- - if any_float { - if i == 1 && t0 != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the first min() operand into the active floating-point result register before it becomes the running floating minimum - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve the current floating minimum while the next candidate expression is evaluated - } else { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the current integer minimum while the next candidate expression is evaluated - } - - let ti = emit_expr(arg, emitter, ctx, data); - - if any_float || ti == PhpType::Float { - // -- float comparison path -- - if ti != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the new min() candidate into the active floating-point result register before the floating comparison - } - if !any_float { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x9"); // restore the previous integer minimum before promoting it into the floating comparison path - emitter.instruction("scvtf d1, x9"); // convert the previous integer minimum into the secondary AArch64 floating-point scratch register - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "r9"); // restore the previous integer minimum before promoting it into the floating comparison path - emitter.instruction("cvtsi2sd xmm1, r9"); // convert the previous integer minimum into the secondary x86_64 floating-point scratch register - } - } - } else { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_float_reg(emitter, "d1"); // restore the previous floating minimum into the secondary AArch64 floating-point scratch register - } - Arch::X86_64 => { - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the previous floating minimum into the secondary x86_64 floating-point scratch register - } - } - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmin d0, d1, d0"); // compute the smaller of the previous and new floating candidates in the standard AArch64 floating-point result register - } - Arch::X86_64 => { - emitter.instruction("minsd xmm1, xmm0"); // compute the smaller of the previous and new floating candidates in the secondary x86_64 floating-point scratch register - emitter.instruction("movsd xmm0, xmm1"); // move the updated floating minimum back into the standard x86_64 floating-point result register - } - } - any_float = true; - } else { - // -- integer comparison path -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x1, [sp], #16"); // restore the previous integer minimum into the AArch64 scratch register before the scalar comparison - emitter.instruction("cmp x1, x0"); // compare the previous integer minimum against the new integer candidate - emitter.instruction("csel x0, x1, x0, lt"); // select the smaller integer value into the standard AArch64 integer result register - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "r9"); // restore the previous integer minimum into a scratch register before the scalar comparison - emitter.instruction("cmp r9, rax"); // compare the previous integer minimum against the new integer candidate - emitter.instruction("cmovl rax, r9"); // keep the smaller integer value in the standard x86_64 integer result register - } - } - } - } - - if any_float { - Some(PhpType::Float) - } else { - Some(PhpType::Int) - } -} diff --git a/src/codegen/builtins/math/mod.rs b/src/codegen/builtins/math/mod.rs deleted file mode 100644 index 0208cc68ea..0000000000 --- a/src/codegen/builtins/math/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Purpose: -//! Dispatches numeric PHP builtins and libm-backed operations to their focused codegen emitters. -//! Keeps the public builtin category surface small while leaf files own lowering details. -//! -//! Called from: -//! - `crate::codegen::builtins::emit_builtin_call()`. -//! -//! Key details: -//! - Dispatcher names must stay aligned with the builtin catalog and signature normalization layer. - -mod abs; -mod acos; -mod asin; -mod atan; -mod atan2; -mod ceil; -mod clamp; -mod cos; -mod cosh; -mod deg2rad; -mod exp; -mod fdiv; -mod floor; -mod fmod; -mod hypot; -mod intdiv; -mod log; -mod log10; -mod log2; -mod max; -mod min; -mod pi; -mod pow; -mod rad2deg; -mod rand; -mod random_int; -mod round; -mod sin; -mod sinh; -mod sqrt; -mod tan; -mod tanh; - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Dispatches to the appropriate math builtin emitter based on `name`. -/// -/// Returns `Some(PhpType)` if `name` matches a known math builtin, or `None` if -/// the builtin is not recognized. Callers must have already validated argument -/// count and types via the type checker. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "abs" => abs::emit(name, args, emitter, ctx, data), - "floor" => floor::emit(name, args, emitter, ctx, data), - "ceil" => ceil::emit(name, args, emitter, ctx, data), - "clamp" => clamp::emit(name, args, emitter, ctx, data), - "round" => round::emit(name, args, emitter, ctx, data), - "sqrt" => sqrt::emit(name, args, emitter, ctx, data), - "pow" => pow::emit(name, args, emitter, ctx, data), - "min" => min::emit(name, args, emitter, ctx, data), - "max" => max::emit(name, args, emitter, ctx, data), - "intdiv" => intdiv::emit(name, args, emitter, ctx, data), - "fmod" => fmod::emit(name, args, emitter, ctx, data), - "fdiv" => fdiv::emit(name, args, emitter, ctx, data), - "rand" | "mt_rand" => rand::emit(name, args, emitter, ctx, data), - "random_int" => random_int::emit(name, args, emitter, ctx, data), - "sin" => sin::emit(name, args, emitter, ctx, data), - "cos" => cos::emit(name, args, emitter, ctx, data), - "tan" => tan::emit(name, args, emitter, ctx, data), - "asin" => asin::emit(name, args, emitter, ctx, data), - "acos" => acos::emit(name, args, emitter, ctx, data), - "atan" => atan::emit(name, args, emitter, ctx, data), - "atan2" => atan2::emit(name, args, emitter, ctx, data), - "sinh" => sinh::emit(name, args, emitter, ctx, data), - "cosh" => cosh::emit(name, args, emitter, ctx, data), - "tanh" => tanh::emit(name, args, emitter, ctx, data), - "log" => log::emit(name, args, emitter, ctx, data), - "log2" => log2::emit(name, args, emitter, ctx, data), - "log10" => log10::emit(name, args, emitter, ctx, data), - "exp" => exp::emit(name, args, emitter, ctx, data), - "hypot" => hypot::emit(name, args, emitter, ctx, data), - "pi" => pi::emit(name, args, emitter, ctx, data), - "deg2rad" => deg2rad::emit(name, args, emitter, ctx, data), - "rad2deg" => rad2deg::emit(name, args, emitter, ctx, data), - _ => None, - } -} diff --git a/src/codegen/builtins/math/pi.rs b/src/codegen/builtins/math/pi.rs deleted file mode 100644 index a3ea04347a..0000000000 --- a/src/codegen/builtins/math/pi.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `pi` numeric builtin calls. -//! Handles scalar argument lowering and returns the PHP numeric type promised by signature checking. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - Integer-vs-float result selection must stay aligned with PHP semantics and local type inference. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; -use crate::codegen::abi; - -/// Emits the `pi()` builtin as a compile-time float constant loaded into the ABI return register. -/// -/// `_name` is unused—signature checking has already validated the call. -/// `_args` is empty and not accessed—signature checking enforces arity. -/// Returns `Some(PhpType::Float)` since `pi()` always yields a float. -/// Loads the `std::f64::consts::PI` constant into `d0` (ARM64) or `xmm0` (x86_64) via -/// `DataSection` to avoid hardcoding relocatable assembly constants in the emitter. -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("pi()"); - let label = data.add_float(std::f64::consts::PI); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_symbol_to_reg_via_page(emitter, "d0", "x9", &label); // load the M_PI floating constant into the standard AArch64 floating-point result register - } - Arch::X86_64 => { - abi::emit_load_symbol_to_reg(emitter, "xmm0", &label, 0); // load the M_PI floating constant into the standard x86_64 floating-point result register - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/pow.rs b/src/codegen/builtins/math/pow.rs deleted file mode 100644 index 89e2d32764..0000000000 --- a/src/codegen/builtins/math/pow.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Purpose: -//! Emits PHP `pow` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `pow(base, exponent)` as a call to the platform's libc `pow()`. -/// -/// Both operands are evaluated, converted to floating-point if needed, and -/// passed to `pow()` following the target ABI (AArch64: d0/d1, X86_64: xmm0/xmm1). -/// The base is saved to a scratch float register before the exponent is evaluated, -/// then restored to the first argument register after exponent evaluation. The -/// result is always `PhpType::Float`. The `_name` parameter is unused for this -/// builtin and is accepted only to match the emitter dispatch signature. -/// -/// # Arguments -/// * `_name` - Unused; present only to match the builtin emitter dispatch. -/// * `args` - Must contain exactly 2 expressions: base and exponent. -/// * `emitter` - Target-specific instruction emission. -/// * `ctx` - Codegen context carrying variable layout and class metadata. -/// * `data` - Data section for relocations and constant materialization. -/// -/// # Returns -/// `Some(PhpType::Float)` on success; `None` is not produced by this emitter -/// but is returned to satisfy the emitter trait signature. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("pow()"); - // -- evaluate base, save it, evaluate exponent, call C pow() -- - let t0 = emit_expr(&args[0], emitter, ctx, data); - let t0_mixed = matches!(t0, PhpType::Mixed | PhpType::Union(_)); - match emitter.target.arch { - Arch::AArch64 => { - if t0_mixed { - // The base is a boxed Mixed cell pointer, not a scalar; cast it to a double - // through the runtime so `scvtf` does not convert the pointer itself. - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); - } else if t0 != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the pow() base to float when the first argument is an integer - } - } - Arch::X86_64 => { - if t0_mixed { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); - } else if t0 != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the pow() base to float when the first argument is an integer - } - } - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve the floating pow() base while the exponent expression is evaluated - let t1 = emit_expr(&args[1], emitter, ctx, data); - let t1_mixed = matches!(t1, PhpType::Mixed | PhpType::Union(_)); - match emitter.target.arch { - Arch::AArch64 => { - if t1_mixed { - // The exponent is a boxed Mixed cell pointer; cast it to a double through - // the runtime so `scvtf` does not convert the pointer itself. - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); - } else if t1 != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the pow() exponent to float when the second argument is an integer - } - emitter.instruction("fmov d1, d0"); // move the floating exponent into the second libc pow() argument register - abi::emit_pop_float_reg(emitter, "d0"); // restore the floating base into the first libc pow() argument register - emitter.bl_c("pow"); // delegate the exponentiation to libc pow() on AArch64 - } - Arch::X86_64 => { - if t1_mixed { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); - } else if t1 != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the pow() exponent to float when the second argument is an integer - } - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the floating base into a scratch floating-point register before ordering the SysV libc pow() arguments - emitter.instruction("movapd xmm2, xmm0"); // preserve the floating exponent while the floating base is moved into the first libc pow() argument register - emitter.instruction("movapd xmm0, xmm1"); // move the floating base into the first libc pow() argument register - emitter.instruction("movapd xmm1, xmm2"); // move the floating exponent into the second libc pow() argument register - emitter.instruction("call pow"); // delegate the exponentiation to libc pow() on linux-x86_64 - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/rad2deg.rs b/src/codegen/builtins/math/rad2deg.rs deleted file mode 100644 index 38d6cf0787..0000000000 --- a/src/codegen/builtins/math/rad2deg.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Purpose: -//! Emits PHP `rad2deg` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Converts a radian value to degrees by multiplying by `180.0 / PI`. -/// -/// Loads the radian input from `args[0]` into the floating-point result register, -/// normalizing integer operands to float first. Multiplies by the `180.0 / PI` -/// constant and returns `PhpType::Float`. -/// -/// # Arguments -/// * `_name` — unused; the builtin name is inferred from the call site -/// * `args` — single argument: the radian value (int or float) -/// * `emitter` — target-aware instruction emitter -/// * `ctx` — codegen context carrying variable layout and class metadata -/// * `data` — mutable data section for embedding the conversion constant -/// -/// # Returns -/// Always returns `Some(PhpType::Float)` as the result is always a float. -/// -/// # ABI notes -/// - AArch64: input in `d0`, constant loaded via `adrp`/`ldr_lo12` into `d1`, result in `d0` -/// - x86_64: input in `xmm0`, constant loaded via `movsd` into `xmm1`, result in `xmm0` -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rad2deg()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize the radian input into the active floating-point result register before applying the conversion factor - } - // -- multiply by 180.0 / M_PI to convert radians to degrees -- - let label = data.add_float(180.0 / std::f64::consts::PI); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_symbol_to_reg_via_page(emitter, "d1", "x9", &label); // load the radian-to-degree conversion constant into the secondary AArch64 floating-point register - emitter.instruction("fmul d0, d0, d1"); // multiply the radian input by the conversion constant in the standard AArch64 floating-point result register - } - Arch::X86_64 => { - abi::emit_load_symbol_to_reg(emitter, "xmm1", &label, 0); // load the radian-to-degree conversion constant into the secondary x86_64 floating-point register - emitter.instruction("mulsd xmm0, xmm1"); // multiply the radian input by the conversion constant in the standard x86_64 floating-point result register - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/rand.rs b/src/codegen/builtins/math/rand.rs deleted file mode 100644 index f6a9232654..0000000000 --- a/src/codegen/builtins/math/rand.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Purpose: -//! Emits PHP `rand` random-number builtin calls. -//! Delegates entropy and range handling to runtime helpers while producing PHP integer results. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - Random helpers are effectful and must not be treated as pure by callers or optimizer assumptions. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `rand(min, max)` or `rand()` builtin. -/// -/// With two arguments `min` and `max`, generates a uniformly distributed random -/// integer in the inclusive range `[min, max]`. With no arguments, returns a -/// non-negative random integer in `[0, 2^32-1]` via `__rt_random_u32`. -/// -/// # Arguments -/// - `name`: The builtin function name (unused beyond comments). -/// - `args`: Either two expressions evaluating to integers (min, max), or empty. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and metadata. -/// - `data`: Data section for relocatable constants. -/// -/// # Returns -/// Always `Some(PhpType::Int)`. The result is in the standard integer result -/// register (`x0` on ARM64, `rax` on x86_64). -/// -/// # Side effects -/// Calls `__rt_random_uniform` or `__rt_random_u32` at runtime; both are -/// effectful and must not be reordered or eliminated by the optimizer. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}()", name)); - if args.len() == 2 { - // -- rand(min, max): generate random int in [min, max] -- - emit_expr(&args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the inclusive minimum while evaluating the inclusive maximum expression - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "r9"); // restore the inclusive minimum into a scratch register before forming the random range on SysV x86_64 - emitter.instruction("sub rax, r9"); // compute the inclusive range width as max - min in the active integer result register - emitter.instruction("add rax, 1"); // widen the exclusive upper bound to max - min + 1 before sampling a uniform offset - emitter.instruction("mov rdi, rax"); // move the exclusive upper bound into the first SysV integer argument register for __rt_random_uniform - abi::emit_call_label(emitter, "__rt_random_uniform"); // draw a uniform random offset in the half-open range [0, max - min + 1) - emitter.instruction("add rax, r9"); // shift the sampled offset back into the caller-visible inclusive [min, max] interval - } - _ => { - abi::emit_pop_reg(emitter, "x9"); // restore the inclusive minimum into a scratch register before forming the random range on AArch64 - emitter.instruction("sub x0, x0, x9"); // compute the inclusive range width as max - min in the active integer result register - emitter.instruction("add x0, x0, #1"); // widen the exclusive upper bound to max - min + 1 before sampling a uniform offset - abi::emit_push_reg(emitter, "x9"); // preserve the inclusive minimum across the random helper call that reuses the primary integer result register - abi::emit_call_label(emitter, "__rt_random_uniform"); // draw a uniform random offset in the half-open range [0, max - min + 1) - abi::emit_pop_reg(emitter, "x9"); // restore the saved inclusive minimum after the random helper returns the sampled offset - emitter.instruction("add x0, x0, x9"); // shift the sampled offset back into the caller-visible inclusive [min, max] interval - } - } - } else { - // -- rand() with no args: return non-negative random int -- - abi::emit_call_label(emitter, "__rt_random_u32"); // generate a random uint32 through the target-aware runtime helper - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/math/random_int.rs b/src/codegen/builtins/math/random_int.rs deleted file mode 100644 index 692fb045d6..0000000000 --- a/src/codegen/builtins/math/random_int.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Purpose: -//! Emits PHP `random_int` random-number builtin calls. -//! Delegates entropy and range handling to runtime helpers while producing PHP integer results. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - Random helpers are effectful and must not be treated as pure by callers or optimizer assumptions. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `random_int($min, $max)` builtin. -/// -/// Produces a cryptographically secure random integer in the inclusive range -/// [$min, $max]. The implementation evaluates `$min` first, then `$max`, pushes -/// the minimum to preserve it while evaluating the maximum, then calls the runtime -/// helper `__rt_random_uniform` to obtain a uniform offset in the half-open range -/// [0, max-min+1). The offset is shifted by `$min` to restore the inclusive interval. -/// -/// # Arguments -/// - `_name`: the builtin name (unused, already resolved to `random_int`). -/// - `args`: exactly two expressions: the minimum and maximum bounds. -/// - `emitter`: target-specific instruction emitter. -/// - `ctx`: codegen context with variable layout and target info. -/// - `data`: mutable data section for constants/labels. -/// -/// # Returns -/// `Some(PhpType::Int)` indicating the result is a PHP integer. -/// The value is placed in the primary integer register (`x0` on ARM64, `rax` on x86_64) -/// per the target ABI. -/// -/// # ABI constraints -/// - `emit_expr` for each argument may clobber the primary integer register. -/// - A scratch register (`x9`/`r9`) holds the preserved minimum across calls. -/// - `__rt_random_uniform` is called with the exclusive upper bound in `rdi`/`x0` -/// and returns a uniform value in the primary integer register. -/// -/// # Panics -/// Panics if `args.len() != 2`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("random_int()"); - // -- random_int(min, max): cryptographically secure random in [min, max] -- - emit_expr(&args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the inclusive minimum while evaluating the inclusive maximum expression - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "r9"); // restore the inclusive minimum into a scratch register before forming the random range on SysV x86_64 - emitter.instruction("sub rax, r9"); // compute the inclusive range width as max - min in the active integer result register - emitter.instruction("add rax, 1"); // widen the exclusive upper bound to max - min + 1 before sampling a uniform offset - emitter.instruction("mov rdi, rax"); // move the exclusive upper bound into the first SysV integer argument register for __rt_random_uniform - abi::emit_call_label(emitter, "__rt_random_uniform"); // draw a uniform random offset in the half-open range [0, max - min + 1) - emitter.instruction("add rax, r9"); // shift the sampled offset back into the caller-visible inclusive [min, max] interval - } - _ => { - abi::emit_pop_reg(emitter, "x9"); // restore the inclusive minimum into a scratch register before forming the random range on AArch64 - emitter.instruction("sub x0, x0, x9"); // compute the inclusive range width as max - min in the active integer result register - emitter.instruction("add x0, x0, #1"); // widen the exclusive upper bound to max - min + 1 before sampling a uniform offset - abi::emit_push_reg(emitter, "x9"); // preserve the inclusive minimum across the random helper call that reuses the primary integer result register - abi::emit_call_label(emitter, "__rt_random_uniform"); // draw a uniform random offset in the half-open range [0, max - min + 1) - abi::emit_pop_reg(emitter, "x9"); // restore the saved inclusive minimum after the random helper returns the sampled offset - emitter.instruction("add x0, x0, x9"); // shift the sampled offset back into the caller-visible inclusive [min, max] interval - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/math/round.rs b/src/codegen/builtins/math/round.rs deleted file mode 100644 index 6e96302ec9..0000000000 --- a/src/codegen/builtins/math/round.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Purpose: -//! Emits PHP `round` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `round(value [, precision])` builtin. -/// -/// ## Single-argument form (`args.len() == 1`) -/// Emits `frinta` (AArch64) or `call round` (x86_64) directly on the value, -/// after promoting integers to float and normalising `Mixed`/`Union` payloads. -/// -/// ## Two-argument form (`args.len() == 2`) -/// Scales the value by `10^precision`, rounds to the nearest integer with ties -/// away from zero, then divides back by the multiplier. This produces PHP's -/// documented rounding behaviour for non-zero precision. -/// -/// # Arguments -/// * `_name` – builtin name (unused; the caller dispatches by name). -/// * `args` – expression tree for `value` and optionally `precision`. -/// * `emitter`– target assembly emitter. -/// * `ctx` – codegen context (types, frame layout, etc.). -/// * `data` – data section for literal pools. -/// -/// # Returns -/// `Some(PhpType::Float)` because `round()` always returns a float in PHP. -/// -/// # ABI notes -/// AArch64: input in `x0`/`d0`, result in `d0`. x86_64: input in `rax`/`xmm0`, -/// result in `xmm0`. `Mixed` payloads are normalised via `__rt_mixed_cast_float` -/// before any operation. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("round()"); - - if args.len() == 1 { - let ty = emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // normalize boxed numeric/null payloads to a floating-point round() input - } else if ty != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the round() input to float when it is an integer - } - emitter.instruction("frinta d0, d0"); // round to nearest with ties away from zero on AArch64 - } - Arch::X86_64 => { - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // normalize boxed numeric/null payloads to a floating-point round() input - } else if ty != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the round() input to float when it is an integer - } - emitter.instruction("call round"); // delegate PHP-compatible nearest-integer rounding to libc round() on linux-x86_64 - } - } - } else { - let ty = emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // normalize boxed numeric/null payloads to a floating-point round() input - } else if ty != PhpType::Float { - emitter.instruction("scvtf d0, x0"); // convert the round() value to float when it is an integer - } - emitter.instruction("str d0, [sp, #-16]!"); // preserve the original value while computing the precision multiplier - - let t1 = emit_expr(&args[1], emitter, ctx, data); - if t1 == PhpType::Float { - emitter.instruction("fcvtzs x0, d0"); // convert the precision argument from float to integer before pow() - } - - emitter.instruction("scvtf d1, x0"); // convert the integer precision into the pow() exponent floating-point register - emitter.instruction("str d1, [sp, #-16]!"); // preserve the floating precision exponent while materializing the pow() base - emitter.instruction("fmov d0, #10.0"); // materialize 10.0 as the pow() base for precision scaling - emitter.instruction("ldr d1, [sp], #16"); // restore the floating precision exponent into the second pow() argument register - emitter.bl_c("pow"); // compute 10^precision through the C library pow() helper - - emitter.instruction("ldr d1, [sp], #16"); // restore the original value after pow() returns the precision multiplier - emitter.instruction("fmul d1, d1, d0"); // scale the original value by the precision multiplier before rounding - emitter.instruction("str d0, [sp, #-16]!"); // preserve the precision multiplier for the final division step - emitter.instruction("frinta d0, d1"); // round the scaled value to the nearest integer with ties away from zero - emitter.instruction("ldr d1, [sp], #16"); // restore the precision multiplier for the final division step - emitter.instruction("fdiv d0, d0, d1"); // divide the rounded scaled value back down by the precision multiplier - } - Arch::X86_64 => { - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // normalize boxed numeric/null payloads to a floating-point round() input - } else if ty != PhpType::Float { - emitter.instruction("cvtsi2sd xmm0, rax"); // convert the round() value to float when it is an integer - } - abi::emit_push_float_reg(emitter, "xmm0"); // preserve the original value while computing the precision multiplier - - let t1 = emit_expr(&args[1], emitter, ctx, data); - if t1 == PhpType::Float { - emitter.instruction("cvttsd2si rax, xmm0"); // convert the precision argument from float to integer before pow() - } - - emitter.instruction("cvtsi2sd xmm1, rax"); // convert the integer precision into the second pow() floating-point argument register - emitter.instruction("mov rax, 0x4024000000000000"); // materialize the IEEE-754 bit pattern for 10.0 before calling pow() - emitter.instruction("movq xmm0, rax"); // move 10.0 into the first pow() floating-point argument register - emitter.instruction("call pow"); // compute 10^precision through the libc pow() helper on linux-x86_64 - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the original value into the left-hand floating-point scratch register - emitter.instruction("mulsd xmm1, xmm0"); // scale the original value by the precision multiplier before rounding - abi::emit_push_float_reg(emitter, "xmm0"); // preserve the precision multiplier for the final division step - emitter.instruction("movsd xmm0, xmm1"); // move the scaled value into the first libc round() floating-point argument register - emitter.instruction("call round"); // round the scaled value through libc round() to preserve PHP rounding semantics - abi::emit_pop_float_reg(emitter, "xmm1"); // restore the precision multiplier into the left-hand floating-point scratch register - emitter.instruction("divsd xmm0, xmm1"); // divide the rounded scaled value back down by the precision multiplier - } - } - } - - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/sin.rs b/src/codegen/builtins/math/sin.rs deleted file mode 100644 index 6b6f2aafa0..0000000000 --- a/src/codegen/builtins/math/sin.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Purpose: -//! Emits PHP `sin` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `sin` builtin call. - /// - /// ## Arguments - /// - `args[0]`: the operand (integer or float) — emits the expression and normalizes - /// integer results to float before the libc call. - /// - /// ## Returns - /// - Always `PhpType::Float`. NaN/infinity follow from libm; type-checker signatures - /// guarantee PHP-compatible behavior. - /// - /// ## Side effects - /// - Calls `sin` from the C library on the active floating-point register. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sin()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer sin() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("sin"), // call libc sin() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call sin"), // call libc sin() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/sinh.rs b/src/codegen/builtins/math/sinh.rs deleted file mode 100644 index e251316a15..0000000000 --- a/src/codegen/builtins/math/sinh.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Purpose: -//! Emits PHP `sinh` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `sinh()` builtin call to compute the hyperbolic sine of a float or integer. -/// -/// # Inputs -/// - `_name`: Unused name parameter (matches dispatcher signature). -/// - `args`: Single argument expression to be evaluated and passed to `sinh()`. -/// - `emitter`: Target assembly emitter. -/// - `ctx`: Codegen context carrying operand type info. -/// - `data`: Data section for any embedded literals. -/// -/// # Behavior -/// Evaluates the argument expression to determine its type. For integer operands, -/// inserts an integer-to-float conversion before the libc call to normalize the -/// input into the active floating-point result register. Then issues a platform- -/// specific `bl sinh` (AArch64) or `call sinh` (x86_64) instruction to invoke the -/// C library routine. Returns `PhpType::Float` regardless of input type. -/// -/// # ABI notes -/// - AArch64: scalar argument is placed in `d0` per the AAPCS calling convention. -/// - x86_64: scalar argument is placed in `xmm0` per the SysV AMD64 ABI. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sinh()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer sinh() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("sinh"), // call libc sinh() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call sinh"), // call libc sinh() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/sqrt.rs b/src/codegen/builtins/math/sqrt.rs deleted file mode 100644 index b9e0e2c165..0000000000 --- a/src/codegen/builtins/math/sqrt.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Purpose: -//! Emits PHP `sqrt` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `sqrt($arg)` builtin call as target-native square-root instructions. -/// -/// Consumes the first argument expression, promoting integer operands to float before -/// the square-root operation. Emits `fsqrt d0, d0` on AArch64 or `sqrtsd xmm0, xmm0` on -/// x86_64. The floating-point result is left in the ABI return register (`d0`/`xmm0`). -/// -/// Returns `Some(PhpType::Float)` since `sqrt` always produces a float in PHP. -/// -/// # Arguments -/// * `_name` — unused; present for dispatcher uniformity -/// * `args` — must contain exactly one argument (checked by the type checker) -/// * `emitter` — target assembly emitter -/// * `ctx` — codegen context (variable layout, ownership state) -/// * `data` — data section for any emitted constants -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sqrt()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - // -- convert int to float if needed, then compute square root -- - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer sqrt() inputs into the active floating-point result register before the square-root operation - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fsqrt d0, d0"); // compute the scalar square root in the native AArch64 floating-point result register - } - Arch::X86_64 => { - emitter.instruction("sqrtsd xmm0, xmm0"); // compute the scalar square root in the native x86_64 floating-point result register - } - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/tan.rs b/src/codegen/builtins/math/tan.rs deleted file mode 100644 index 67c0a23548..0000000000 --- a/src/codegen/builtins/math/tan.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Purpose: -//! Emits PHP `tan` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `tan` numeric builtin call backed by the platform libc `tan` routine. -/// -/// # Arguments -/// * `_name` — Unused name parameter (保留 for dispatcher signature compatibility). -/// * `args` — Single argument: the operand to compute tangent on. -/// * `emitter` — Target assembly emitter. -/// * `ctx` — Codegen context carrying variable layout and class metadata. -/// * `data` — Mutable data section for constant pools. -/// -/// # Behavior -/// - Emits the operand expression and captures its type. -/// - If the operand is not `PhpType::Float`, emits an integer-to-float normalization -/// via `emit_int_result_to_float_result` so the floating-point register holds the value -/// before the libc call. -/// - Emits a `bl tan` (AArch64) or `call tan` (x86_64) instruction to invoke the -/// platform's libm tangent function. -/// -/// # Returns -/// Always returns `Some(PhpType::Float)` — `tan` produces a floating-point result. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("tan()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer tan() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("tan"), // call libc tan() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call tan"), // call libc tan() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/math/tanh.rs b/src/codegen/builtins/math/tanh.rs deleted file mode 100644 index 52f08e73dc..0000000000 --- a/src/codegen/builtins/math/tanh.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Purpose: -//! Emits PHP `tanh` numeric builtin calls backed by floating-point/libm-style helpers. -//! Marshals integer or float operands into the target ABI and records the numeric return type. -//! -//! Called from: -//! - `crate::codegen::builtins::math::emit()`. -//! -//! Key details: -//! - NaN, infinity, rounding, and division edge cases must remain PHP-compatible with type-checker signatures. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `tanh($arg)` call. -/// -/// Normalizes integer operands to float (via `emit_int_result_to_float_result`) before -/// the libc call, ensuring the floating-point argument register holds the correct value. -/// Dispatches to the target-specific libc `tanh` symbol (AArch64 `bl_c`, x86_64 `call`). -/// -/// # Arguments -/// * `name` — builtin name (unused, only for signature compatibility) -/// * `args` — single argument expression -/// * `emitter` — target assembly emitter -/// * `ctx` — codegen context (variable layout, ownership) -/// * `data` — data section for literals/constants -/// -/// # Returns -/// Always returns `Some(PhpType::Float)`. NaN and infinity are produced by the underlying -/// libc implementation and are PHP-compatible by construction. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("tanh()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer tanh() inputs into the active floating-point result register before the libc call - } - match emitter.target.arch { - Arch::AArch64 => emitter.bl_c("tanh"), // call libc tanh() with the scalar argument in the native AArch64 floating-point argument register - Arch::X86_64 => emitter.instruction("call tanh"), // call libc tanh() with the scalar argument in the native SysV floating-point argument register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/mod.rs b/src/codegen/builtins/mod.rs deleted file mode 100644 index 5fc8f6cee1..0000000000 --- a/src/codegen/builtins/mod.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Purpose: -//! Routes normalized PHP builtin calls from expression codegen into category dispatchers. -//! Owns shared named/spread argument lowering before builtin-specific emitters see arguments. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()`. -//! -//! Key details: -//! - Builtin names arrive after type/catalog resolution, including PHP case-insensitive and namespace fallback behavior. - -pub(crate) mod arrays; -/// Resolves string-literal function names used by callable/introspection builtins. -/// Shares PHP case-insensitive lookup between string-callback and introspection builtins. -/// Include variants, externs, builtins, and user functions stay distinguishable so callers -/// can choose the right lowering path. -pub(crate) mod callable_lookup; -mod io; -mod math; -mod pointers; -mod spl; -mod strings; -mod system; -mod types; - -pub(crate) use io::publish_tls_function_pointers; -pub(crate) use io::phar_stream; -pub(crate) use io::stream_filter_bzip2; -pub(crate) use io::stream_filter_iconv; -pub(crate) use io::stream_filter_iconv_write; -pub(crate) use io::stream_filter_inflate; -pub(crate) use io::stream_filter_zlib; -pub(crate) use strings::hash_crypto; - -use super::context::Context; -use super::data_section::DataSection; -use super::emit::Emitter; -use crate::parser::ast::Expr; -use crate::span::Span; -use crate::types::PhpType; - -/// Routes a normalized PHP builtin call to its category dispatcher and emits the call. - /// - /// Handles named-argument reordering, spread unpacking, and call argument preevaluation - /// before delegating to the first category `emit()` that recognizes the builtin name. - /// - /// Returns `Some(return_type)` if a dispatcher handled the call, or `None` if no - /// category recognized the name (caller should treat it as a user function call). -pub fn emit_builtin_call( - name: &str, - args: &[Expr], - call_span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let normalized_args; - let args = if let Some(sig) = crate::types::builtin_call_sig(name) { - let regular_param_count = - crate::codegen::expr::calls::args::regular_param_count(Some(&sig), args.len()); - let normalized = if crate::codegen::expr::calls::args::has_named_args(args) { - crate::codegen::expr::calls::args::preevaluate_named_call_args_to_temps( - &sig, - args, - call_span, - regular_param_count, - true, - emitter, - ctx, - data, - ) - } else { - crate::codegen::expr::calls::args::normalize_builtin_call_args_with_checks(&sig, args) - }; - crate::codegen::expr::calls::args::emit_spread_length_checks( - &normalized.spread_length_checks, - emitter, - ctx, - data, - ); - normalized_args = normalized.args; - normalized_args.as_slice() - } else { - args - }; - - system::emit(name, args, emitter, ctx, data) - .or_else(|| strings::emit(name, args, emitter, ctx, data)) - .or_else(|| arrays::emit(name, args, emitter, ctx, data)) - .or_else(|| math::emit(name, args, emitter, ctx, data)) - .or_else(|| types::emit(name, args, emitter, ctx, data)) - .or_else(|| io::emit(name, args, emitter, ctx, data)) - .or_else(|| pointers::emit(name, args, emitter, ctx, data)) - .or_else(|| spl::emit(name, args, emitter, ctx, data)) -} diff --git a/src/codegen/builtins/pointers/mod.rs b/src/codegen/builtins/pointers/mod.rs deleted file mode 100644 index 2e38f97ff0..0000000000 --- a/src/codegen/builtins/pointers/mod.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Purpose: -//! Dispatches compiler-extension pointer builtins to their focused codegen emitters. -//! Keeps the public builtin category surface small while leaf files own lowering details. -//! -//! Called from: -//! - `crate::codegen::builtins::emit_builtin_call()`. -//! -//! Key details: -//! - Dispatcher names must stay aligned with the builtin catalog and signature normalization layer. - -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; - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{can_coerce_result_to_type, coerce_result_to_type}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Dispatches pointer builtin calls by name to their leaf emitters. -/// Returns `Some(PhpType)` on recognized builtin, `None` for unknown names. -/// -/// # Arguments -/// * `name` — the builtin function name (e.g. `"ptr"`, `"ptr_null"`) -/// * `args` — the parsed call arguments -/// * `emitter` — target assembly emitter -/// * `ctx` — codegen context (variables, classes, globals) -/// * `data` — mutable data section for relocations and constants -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "ptr" => ptr::emit(name, args, emitter, ctx, data), - "ptr_null" => ptr_null::emit(name, args, emitter, ctx, data), - "ptr_is_null" => ptr_is_null::emit(name, args, emitter, ctx, data), - "ptr_offset" => ptr_offset::emit(name, args, emitter, ctx, data), - "ptr_get" => ptr_get::emit(name, args, emitter, ctx, data), - "ptr_read8" => ptr_read8::emit(name, args, emitter, ctx, data), - "ptr_read16" => ptr_read16::emit(name, args, emitter, ctx, data), - "ptr_read32" => ptr_read32::emit(name, args, emitter, ctx, data), - "ptr_read_string" => ptr_read_string::emit(name, args, emitter, ctx, data), - "ptr_set" => ptr_set::emit(name, args, emitter, ctx, data), - "ptr_write8" => ptr_write8::emit(name, args, emitter, ctx, data), - "ptr_write16" => ptr_write16::emit(name, args, emitter, ctx, data), - "ptr_write32" => ptr_write32::emit(name, args, emitter, ctx, data), - "ptr_write_string" => ptr_write_string::emit(name, args, emitter, ctx, data), - "ptr_sizeof" => ptr_sizeof::emit(name, args, emitter, ctx, data), - _ => None, - } -} - -/// Coerces the current integer result to a specific PHP type, handling Mixed ownership. -/// If `arg` owns a boxed Mixed value that must be released after coercion, preserves it -/// on the stack, performs the coercion, then releases the preserved value. -/// -/// # Arguments -/// * `arg` — the expression that produced the current result (used for ownership tracking) -/// * `source_ty` — the current result PHP type before coercion -/// * `emitter` — target assembly emitter -/// * `ctx` — codegen context -/// * `data` — mutable data section -/// # Returns -/// The `PhpType` after coercion (always `PhpType::Int` on success) -pub(super) fn coerce_current_result_to_int_arg( - arg: &Expr, - source_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if !can_coerce_result_to_type(source_ty, &PhpType::Int) { - return source_ty.clone(); - } - if crate::codegen::stmt::helpers::should_release_owned_mixed_after_coerce( - arg, - source_ty, - &PhpType::Int, - ) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed Mixed value so it can be released after integer coercion - coerce_result_to_type(emitter, ctx, data, source_ty, &PhpType::Int); - crate::codegen::stmt::helpers::release_preserved_mixed_after_coercion( - emitter, - &PhpType::Int, - ); - } else { - coerce_result_to_type(emitter, ctx, data, source_ty, &PhpType::Int); - } - PhpType::Int -} diff --git a/src/codegen/builtins/pointers/ptr.rs b/src/codegen/builtins/pointers/ptr.rs deleted file mode 100644 index fa33eaf4a0..0000000000 --- a/src/codegen/builtins/pointers/ptr.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr` pointer operations. -//! Lowers raw address arithmetic, loads, or stores using the target ABI without PHP runtime boxing. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Pointer builtins are elephc extensions and must keep raw memory effects explicit and target-aware. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits the `ptr` builtin: takes the address of a variable as a raw integer. -/// -/// - **Local variable**: computes the frame-slot address and stores it in the integer result register. -/// - **Global variable**: materializes the static storage label address into the integer result register. -/// - **Unknown / non-variable**: returns a null pointer (all bits zero). -/// -/// Returns `PhpType::Pointer(None)` — a raw address, not a PHP boxed value. -/// The result is placed in `abi::int_result_reg()` per the target ABI. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("ptr() — take address of variable"); - // The argument must be a variable — we emit its stack address, not its value - if let ExprKind::Variable(var_name) = &args[0].kind { - if let Some(info) = ctx.variables.get(var_name) { - let offset = info.stack_offset; - // -- compute address of variable's stack slot -- - abi::emit_frame_slot_address(emitter, abi::int_result_reg(emitter), offset); - } else if let Some(label) = ctx.global_vars.get(var_name) { - // Global variable — use its static storage address - let label = label.clone(); - abi::emit_symbol_address(emitter, abi::int_result_reg(emitter), &label); // materialize the global variable storage address for the active target ABI - } else { - // Variable not found — return null pointer - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // materialize a null pointer result when the requested variable storage does not exist on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // materialize a null pointer result when the requested variable storage does not exist on x86_64 - } - } - } - } else { - // Non-variable argument — return null pointer - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // materialize a null pointer when ptr() targets a non-addressable expression on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // materialize a null pointer when ptr() targets a non-addressable expression on x86_64 - } - } - } - Some(PhpType::Pointer(None)) -} diff --git a/src/codegen/builtins/pointers/ptr_get.rs b/src/codegen/builtins/pointers/ptr_get.rs deleted file mode 100644 index 7704b20e7c..0000000000 --- a/src/codegen/builtins/pointers/ptr_get.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_get` pointer operations. -//! Lowers raw address arithmetic, loads, or stores using the target ABI without PHP runtime boxing. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Pointer builtins are elephc extensions and must keep raw memory effects explicit and target-aware. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_get` builtin: loads a machine-word (8 bytes) through a pointer. -/// Checks the pointer is non-null before loading; returns `PhpType::Int`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_get() — dereference pointer"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with fatal error on null pointer dereference before loading from pointer memory - // -- load 8 bytes at the pointer address -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [x0]"); // load one machine-word integer payload through the validated pointer on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rax]"); // load one machine-word integer payload through the validated pointer on x86_64 - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/pointers/ptr_is_null.rs b/src/codegen/builtins/pointers/ptr_is_null.rs deleted file mode 100644 index 10a3ef6213..0000000000 --- a/src/codegen/builtins/pointers/ptr_is_null.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_is_null` null-pointer operations. -//! Materializes or tests raw pointer sentinel values in the target integer register convention. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Null pointers are raw addresses, not PHP null Mixed cells. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_is_null` builtin, which tests whether a raw pointer is null. -/// -/// # Arguments -/// - `args[0]`: the pointer expression to test (already emitted into the result register). -/// -/// # Result -/// Returns `PhpType::Bool`: 1 if the pointer is null (sentinel `0x0`), 0 otherwise. -/// The result is materialized in the integer register convention (`x0` on AArch64, `rax` on x86_64). -/// -/// # ABI -/// The pointer payload is assumed to already reside in the integer result register (`x0`/`rax`). -/// The null comparison and boolean materialization are emitted inline using `cmp`/`cset` or `test`/`sete`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_is_null()"); - emit_expr(&args[0], emitter, ctx, data); - // -- check if pointer is null (0x0) -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // compare the pointer payload against the null sentinel on AArch64 - emitter.instruction("cset x0, eq"); // materialize 1 when the pointer is null and 0 otherwise on AArch64 - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // compare the pointer payload against the null sentinel on x86_64 - emitter.instruction("sete al"); // materialize the boolean null result in the low byte register - emitter.instruction("movzx rax, al"); // widen the boolean null result back into the x86_64 integer result register - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/pointers/ptr_null.rs b/src/codegen/builtins/pointers/ptr_null.rs deleted file mode 100644 index 532933972c..0000000000 --- a/src/codegen/builtins/pointers/ptr_null.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_null` null-pointer operations. -//! Materializes or tests raw pointer sentinel values in the target integer register convention. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Null pointers are raw addresses, not PHP null Mixed cells. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_null` builtin: materializes a null pointer sentinel. -/// Returns `PhpType::Pointer(None)` — the raw address 0x0. -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("ptr_null()"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // materialize the null pointer sentinel in the AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // materialize the null pointer sentinel in the x86_64 integer result register - } - } - Some(PhpType::Pointer(None)) -} diff --git a/src/codegen/builtins/pointers/ptr_offset.rs b/src/codegen/builtins/pointers/ptr_offset.rs deleted file mode 100644 index 9d72584f8c..0000000000 --- a/src/codegen/builtins/pointers/ptr_offset.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_offset` pointer operations. -//! Lowers raw address arithmetic, loads, or stores using the target ABI without PHP runtime boxing. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Pointer builtins are elephc extensions and must keep raw memory effects explicit and target-aware. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_offset` builtin: adds a byte offset to a raw pointer. -/// Coerces the offset argument to integer; returns the derived `PhpType::Pointer`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_offset()"); - // -- evaluate pointer expression -- - let ptr_ty = emit_expr(&args[0], emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the base pointer while the byte-offset expression is evaluated - - // -- evaluate byte offset -- - let offset_ty = emit_expr(&args[1], emitter, ctx, data); - super::coerce_current_result_to_int_arg(&args[1], &offset_ty, emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // copy the byte offset into a scratch integer register on AArch64 - abi::emit_pop_reg(emitter, "x0"); // restore the base pointer after the byte-offset expression has been evaluated - emitter.instruction("add x0, x0, x1"); // compute the derived pointer address on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov rcx, rax"); // copy the byte offset into a scratch integer register on x86_64 - abi::emit_pop_reg(emitter, "rax"); // restore the base pointer after the byte-offset expression has been evaluated - emitter.instruction("add rax, rcx"); // compute the derived pointer address on x86_64 - } - } - Some(match ptr_ty { - PhpType::Pointer(tag) => PhpType::Pointer(tag), - _ => PhpType::Pointer(None), - }) -} diff --git a/src/codegen/builtins/pointers/ptr_read16.rs b/src/codegen/builtins/pointers/ptr_read16.rs deleted file mode 100644 index 7e349417ec..0000000000 --- a/src/codegen/builtins/pointers/ptr_read16.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_read16` pointer operations. -//! Lowers little-endian 16-bit raw memory reads with target-specific load instructions. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Reads must check null pointers first and zero-extend the 16-bit payload to PHP int. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_read16` builtin: reads one unsigned 16-bit word through a pointer. -/// Checks for null before reading; aborts with a fatal error if the pointer is null. -/// Zero-extends the 16-bit payload into a PHP integer (`PhpType::Int`). -/// -/// # Arguments -/// - `_name`: unused, matches the builtin dispatcher signature -/// - `args`: single expression producing the pointer value -/// - `emitter`: target assembly emitter -/// - `ctx`: codegen context (contains target, variable layout, etc.) -/// - `data`: mutable data section for literals/relocs -/// -/// # Returns -/// `Some(PhpType::Int)` — the result type is always a PHP integer. -/// -/// # Side effects -/// - Calls `__rt_ptr_check_nonnull` which aborts if the pointer is null -/// - Clobbers `x0`/`rax` (the loaded integer value) and `x1`/`rax` may be used as scratch -/// - Returns the result value in the integer register per target ABI -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_read16() — read two bytes at pointer address"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with a fatal error on null pointer dereference before reading from memory - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldrh w0, [x0]"); // load one unsigned 16-bit word and zero-extend it through the AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("movzx eax, WORD PTR [rax]"); // load one unsigned 16-bit word and zero-extend it through the x86_64 integer result register - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/pointers/ptr_read32.rs b/src/codegen/builtins/pointers/ptr_read32.rs deleted file mode 100644 index 5fc521cc28..0000000000 --- a/src/codegen/builtins/pointers/ptr_read32.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_read32` pointer operations. -//! Lowers raw address arithmetic, loads, or stores using the target ABI without PHP runtime boxing. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Pointer builtins are elephc extensions and must keep raw memory effects explicit and target-aware. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_read32` builtin: reads one 32-bit word through a pointer. -/// Checks non-null; zero-extends to `PhpType::Int`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_read32() — read one 32-bit word at pointer address"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with a fatal error on null pointer dereference before reading from memory - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr w0, [x0]"); // load one 32-bit word and zero-extend it through the AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("mov eax, DWORD PTR [rax]"); // load one 32-bit word and zero-extend it through the x86_64 integer result register - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/pointers/ptr_read8.rs b/src/codegen/builtins/pointers/ptr_read8.rs deleted file mode 100644 index 5fb7aad509..0000000000 --- a/src/codegen/builtins/pointers/ptr_read8.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_read8` pointer operations. -//! Lowers raw address arithmetic, loads, or stores using the target ABI without PHP runtime boxing. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Pointer builtins are elephc extensions and must keep raw memory effects explicit and target-aware. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_read8` builtin: reads one unsigned byte from a pointer address. -/// -/// # Arguments -/// - `args[0]`: the pointer expression to dereference. -/// -/// # Behavior -/// - Calls `__rt_ptr_check_nonnull` to abort with a fatal error if the pointer is null. -/// - Architecture-specific load: `ldrb w0, [x0]` on AArch64, `movzx eax, BYTE PTR [rax]` on X86_64. -/// - The loaded byte is zero-extended through the integer result register. -/// -/// # Return -/// Returns `Some(PhpType::Int)` representing a PHP integer value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_read8() — read one byte at pointer address"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with a fatal error on null pointer dereference before reading from memory - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldrb w0, [x0]"); // load one unsigned byte and zero-extend it through the AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("movzx eax, BYTE PTR [rax]"); // load one unsigned byte and zero-extend it through the x86_64 integer result register - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/pointers/ptr_read_string.rs b/src/codegen/builtins/pointers/ptr_read_string.rs deleted file mode 100644 index dbfe94e2b1..0000000000 --- a/src/codegen/builtins/pointers/ptr_read_string.rs +++ /dev/null @@ -1,58 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_read_string` raw memory reads. -//! Bridges a checked raw pointer plus byte length into an owned elephc PHP string result. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - The runtime helper copies exactly the requested byte count and does not scan for NUL. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_read_string` builtin: copies raw bytes into an owned PHP string. - /// - /// ## Arguments - /// - `args[0]`: source pointer expression - /// - `args[1]`: byte length expression - /// - /// ## Sequence - /// 1. Evaluates the pointer expression and validates it against null via `__rt_ptr_check_nonnull` (aborts if null). - /// 2. Pushes the validated pointer onto the stack to preserve it while the length expression is evaluated. - /// 3. Evaluates the length expression (result lands in `x0`/`rax` per ABI). - /// 4. Moves the length into the string-length parameter register (`x1` on ARM64, `rdx` on x86_64), then pops the preserved pointer into `x0`/`rax`. - /// 5. Calls `__rt_ptr_read_string` which allocates a PHP string and copies exactly `length` bytes from the source pointer (no NUL scan). - /// - /// ## Returns - /// `PhpType::Str` — the owned PHP string result. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_read_string() — copy raw bytes into an owned PHP string"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with a fatal error on null pointer dereference before reading from memory - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the source pointer while the length expression is evaluated - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the requested byte length into the runtime helper length register - abi::emit_pop_reg(emitter, "x0"); // restore the validated source pointer for the runtime helper - } - Arch::X86_64 => { - emitter.instruction("mov rdx, rax"); // move the requested byte length into the x86_64 string-length register - abi::emit_pop_reg(emitter, "rax"); // restore the validated source pointer for the runtime helper - } - } - abi::emit_call_label(emitter, "__rt_ptr_read_string"); // allocate and copy the exact raw byte slice into an owned PHP string - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/pointers/ptr_set.rs b/src/codegen/builtins/pointers/ptr_set.rs deleted file mode 100644 index 0c19171e75..0000000000 --- a/src/codegen/builtins/pointers/ptr_set.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_set` pointer operations. -//! Lowers raw address arithmetic, loads, or stores using the target ABI without PHP runtime boxing. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Pointer builtins are elephc extensions and must keep raw memory effects explicit and target-aware. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_set` builtin: writes a machine-word integer through a pointer. -/// Checks non-null, coerces the value to integer. Returns `PhpType::Void`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_set() — write value at pointer address"); - // -- evaluate pointer -- - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with fatal error on null pointer dereference before writing through the pointer - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the validated destination pointer while the stored value expression is evaluated - - // -- evaluate value to write -- - let value_ty = emit_expr(&args[1], emitter, ctx, data); - super::coerce_current_result_to_int_arg(&args[1], &value_ty, emitter, ctx, data); - - // -- store value at pointer address -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // copy the stored integer payload into a scratch register before restoring the destination pointer on AArch64 - abi::emit_pop_reg(emitter, "x0"); // restore the validated destination pointer after evaluating the stored value on AArch64 - emitter.instruction("str x1, [x0]"); // store one machine-word integer payload through the validated pointer on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov rcx, rax"); // copy the stored integer payload into a scratch register before restoring the destination pointer on x86_64 - abi::emit_pop_reg(emitter, "rax"); // restore the validated destination pointer after evaluating the stored value on x86_64 - emitter.instruction("mov QWORD PTR [rax], rcx"); // store one machine-word integer payload through the validated pointer on x86_64 - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/pointers/ptr_sizeof.rs b/src/codegen/builtins/pointers/ptr_sizeof.rs deleted file mode 100644 index 62e15ee7d5..0000000000 --- a/src/codegen/builtins/pointers/ptr_sizeof.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_sizeof` queries for low-level types. -//! Returns static byte sizes used by pointer arithmetic and raw memory access code. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Reported sizes must match the layouts used by buffer, packed class, and ABI lowering. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits the `ptr_sizeof` builtin call: returns the static byte size of a low-level type. -/// -/// The type name is resolved from the first argument's string literal (`"int"`, `"float"`, -/// `"bool"`, `"string"`, `"ptr"`, or a class name). For class names, the size is computed -/// as `[8] + [properties.len() * 16] + [optional 8 bytes for dynamic properties]`. -/// -/// On success, the computed size (as `usize`) is materialized in the integer result register -/// (`x0` on AArch64, `rax` on x86_64). On failure (non-literal argument or unknown type), -/// zero is placed in the result register. -/// -/// Returns `PhpType::Int` to indicate the result is an integer value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("ptr_sizeof()"); - // -- determine size from string literal argument -- - if let ExprKind::StringLiteral(type_name) = &args[0].kind { - let size: usize = match type_name.as_str() { - "int" | "integer" => 8, - "float" | "double" => 8, - "bool" | "boolean" => 8, - "string" => 16, - "ptr" | "pointer" => 8, - class_name => { - // Look up class and compute total property size - if let Some(class_info) = ctx.classes.get(class_name) { - // Object layout: [class_id:8] + [prop:16] * num_properties - // + optional [dyn_props_ptr:8] for #[\AllowDynamicProperties] - let dyn_slot = if class_info.allow_dynamic_properties { 8 } else { 0 }; - 8 + class_info.properties.len() * 16 + dyn_slot - } else if let Some(class_info) = ctx.extern_classes.get(class_name) { - class_info.total_size - } else { - 0 // unknown type - } - } - }; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", size)); // materialize the computed pointee size in the AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", size)); // materialize the computed pointee size in the x86_64 integer result register - } - } - } else { - // Non-literal argument — return 0 - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // return zero when ptr_sizeof() cannot resolve a literal type name on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // return zero when ptr_sizeof() cannot resolve a literal type name on x86_64 - } - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/pointers/ptr_write16.rs b/src/codegen/builtins/pointers/ptr_write16.rs deleted file mode 100644 index bb14f62df2..0000000000 --- a/src/codegen/builtins/pointers/ptr_write16.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_write16` pointer operations. -//! Lowers little-endian 16-bit raw memory stores with target-specific store instructions. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Stores must check null pointers first and truncate the PHP int to the low 16 bits. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_write16` builtin: writes one 16-bit word through a pointer. -/// Checks non-null, coerces value to integer, truncates to low 16 bits. Returns `PhpType::Void`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_write16() — write two bytes at pointer address"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with a fatal error on null pointer dereference before writing to memory - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the target pointer while the value expression is evaluated - let value_ty = emit_expr(&args[1], emitter, ctx, data); - super::coerce_current_result_to_int_arg(&args[1], &value_ty, emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w1, w0"); // keep only the low 16 bits of the integer value in a scratch AArch64 register - abi::emit_pop_reg(emitter, "x0"); // restore the target pointer after evaluating the written value - emitter.instruction("strh w1, [x0]"); // store one 16-bit word at the destination pointer on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov cx, ax"); // keep only the low 16 bits of the integer value in a scratch x86_64 register - abi::emit_pop_reg(emitter, "rax"); // restore the target pointer after evaluating the written value - emitter.instruction("mov WORD PTR [rax], cx"); // store one 16-bit word at the destination pointer on x86_64 - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/pointers/ptr_write32.rs b/src/codegen/builtins/pointers/ptr_write32.rs deleted file mode 100644 index ee2079b8a4..0000000000 --- a/src/codegen/builtins/pointers/ptr_write32.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_write32` pointer operations. -//! Lowers raw address arithmetic, loads, or stores using the target ABI without PHP runtime boxing. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Pointer builtins are elephc extensions and must keep raw memory effects explicit and target-aware. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_write32` builtin: writes one 32-bit word through a pointer. -/// Checks non-null, coerces value to integer, truncates to low 32 bits. Returns `PhpType::Void`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_write32() — write one 32-bit word at pointer address"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with a fatal error on null pointer dereference before writing to memory - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the target pointer while the value expression is evaluated - let value_ty = emit_expr(&args[1], emitter, ctx, data); - super::coerce_current_result_to_int_arg(&args[1], &value_ty, emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w1, w0"); // keep only the low 32 bits of the integer value in a scratch AArch64 register - abi::emit_pop_reg(emitter, "x0"); // restore the target pointer after evaluating the written value - emitter.instruction("str w1, [x0]"); // store one 32-bit word at the destination pointer on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov ecx, eax"); // keep only the low 32 bits of the integer value in a scratch x86_64 register - abi::emit_pop_reg(emitter, "rax"); // restore the target pointer after evaluating the written value - emitter.instruction("mov DWORD PTR [rax], ecx"); // store one 32-bit word at the destination pointer on x86_64 - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/pointers/ptr_write8.rs b/src/codegen/builtins/pointers/ptr_write8.rs deleted file mode 100644 index 004eaf1c7f..0000000000 --- a/src/codegen/builtins/pointers/ptr_write8.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_write8` pointer operations. -//! Lowers raw address arithmetic, loads, or stores using the target ABI without PHP runtime boxing. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - Pointer builtins are elephc extensions and must keep raw memory effects explicit and target-aware. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_write8` builtin: writes one byte through a pointer. -/// Checks non-null, coerces value to integer, truncates to low 8 bits. Returns `PhpType::Void`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_write8() — write one byte at pointer address"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with a fatal error on null pointer dereference before writing to memory - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the target pointer while the value expression is evaluated - let value_ty = emit_expr(&args[1], emitter, ctx, data); - super::coerce_current_result_to_int_arg(&args[1], &value_ty, emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov w1, w0"); // keep only the low 8 bits of the integer value in a scratch AArch64 register - abi::emit_pop_reg(emitter, "x0"); // restore the target pointer after evaluating the written value - emitter.instruction("strb w1, [x0]"); // store one byte at the destination pointer on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov cl, al"); // keep only the low 8 bits of the integer value in a scratch x86_64 register - abi::emit_pop_reg(emitter, "rax"); // restore the target pointer after evaluating the written value - emitter.instruction("mov BYTE PTR [rax], cl"); // store one byte at the destination pointer on x86_64 - } - } - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/pointers/ptr_write_string.rs b/src/codegen/builtins/pointers/ptr_write_string.rs deleted file mode 100644 index 5d8bedabf5..0000000000 --- a/src/codegen/builtins/pointers/ptr_write_string.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits compiler-extension `ptr_write_string` raw memory writes. -//! Copies borrowed PHP string bytes into caller-owned raw memory without adding a terminator. -//! -//! Called from: -//! - `crate::codegen::builtins::pointers::emit()`. -//! -//! Key details: -//! - The source string remains borrowed; the helper returns the number of payload bytes copied. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ptr_write_string` builtin: copies PHP string bytes into raw memory. -/// Does not append a NUL terminator. Source string remains borrowed. Returns the -/// number of bytes copied as `PhpType::Int`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ptr_write_string() — copy PHP string bytes into raw memory"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with a fatal error on null pointer dereference before writing to memory - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the destination pointer while the source string is evaluated - emit_expr(&args[1], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x0"); // restore the destination pointer while leaving the string pair in x1/x2 - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "rdi"); // restore the destination pointer into the x86_64 helper destination register - } - } - abi::emit_call_label(emitter, "__rt_ptr_write_string"); // copy the borrowed string payload and return its byte length - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/spl/iterator_apply.rs b/src/codegen/builtins/spl/iterator_apply.rs deleted file mode 100644 index 6acdb08c54..0000000000 --- a/src/codegen/builtins/spl/iterator_apply.rs +++ /dev/null @@ -1,748 +0,0 @@ -//! Purpose: -//! Emits PHP `iterator_apply()` calls for Iterator/IteratorAggregate objects. -//! Reuses the statement foreach iterator driver while invoking a callback for each valid position. -//! -//! Called from: -//! - `crate::codegen::builtins::spl::emit()` -//! -//! Key details: -//! - The callback is evaluated once before rewind(), and callback falsehood stops iteration before next(). -//! - The returned count includes the callback invocation that requested the stop. -//! - Runtime callable-array variables are resolved once before the loop and then invoked through -//! the selected descriptor for each valid iterator position. - -use crate::codegen::abi; -use crate::codegen::builtins::arrays::callback_env; -use crate::codegen::builtins::arrays::call_user_func_array::{ - self, LoadedArraySource, -}; -use crate::codegen::builtins::arrays::receiver_call_args; -use crate::codegen::builtins::arrays::runtime_callable_array_callback; -use crate::codegen::callable_dispatch::RuntimeCallableCase; -use crate::codegen::callable_descriptor; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::calls::args as call_args; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::codegen::stmt::{emit_iterable_object_loop, emit_iterator_loop}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; - -use super::iterator_common; - -/// Emits the iterator apply entry point for this module. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("iterator_apply()"); - let source_ty = emit_expr(&args[0], emitter, ctx, data); - let source_kind = match source_ty.codegen_repr() { - PhpType::Iterable => ApplySourceKind::RuntimeIterable, - PhpType::Object(class_name) if class_name == "Traversable" => { - ApplySourceKind::TraversableObject - } - PhpType::Object(class_name) => ApplySourceKind::StaticObject(class_name), - _ => { - return Some(PhpType::Int); - } - }; - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterator receiver while resolving the callback - - let call_reg = abi::nested_call_reg(emitter); - let runtime_string_callback = - call_user_func_array::callback_is_runtime_string(&args[1], ctx); - if !runtime_string_callback - && runtime_callable_array_callback::emit_without_saved_array( - &args[1], - emitter, - ctx, - data, - |case, receiver_ty, emitter, ctx, data| { - emit_runtime_callable_array_iterator_apply_case( - case, - receiver_ty, - &source_kind, - args, - emitter, - ctx, - data, - ); - }, - ) - { - return Some(PhpType::Int); - } - let (captures, sig, callback_slot_kind, descriptor_arg_prefix) = if runtime_string_callback { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s callback-invocation counter before preserving the string callback - let callback_ty = emit_expr(&args[1], emitter, ctx, data); - debug_assert!(matches!(callback_ty.codegen_repr(), PhpType::Str)); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // save the runtime string callback name beneath the loop receiver - (Vec::new(), None, CallbackSlotKind::EntryAddress, None) - } else { - let is_callable_expr = matches!( - &args[1].kind, - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) - ); - let direct_fcc_function = - crate::codegen::callables::direct_first_class_function_sig(&args[1], ctx); - let precomputed_sig = direct_fcc_function - .as_ref() - .map(|(_, sig)| sig.clone()) - .or_else(|| crate::codegen::callables::callable_sig(&args[1], ctx)); - if let Some(array_callback) = - callback_env::resolve_callable_array_descriptor_callback(&args[1], ctx, data) - { - let descriptor_arg_prefix = array_callback - .receiver_prefix - .as_ref() - .map(|(receiver, _)| receiver.clone()); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s callback-invocation counter - abi::emit_symbol_address(emitter, call_reg, &array_callback.descriptor_label); - abi::emit_push_reg(emitter, call_reg); // save the callable-array descriptor beneath the loop receiver - ( - Vec::new(), - Some(array_callback.sig), - CallbackSlotKind::Descriptor, - descriptor_arg_prefix, - ) - } else if callback_env::expr_call_needs_descriptor_callback_env(&args[1], ctx) - && callback_env::descriptor_callback_env_supported(&args[1]) - { - let callback_ty = emit_expr(&args[1], emitter, ctx, data); - debug_assert!(matches!(callback_ty.codegen_repr(), PhpType::Callable)); - callback_env::retain_borrowed_descriptor_callback_result(&args[1], emitter); - emitter.instruction(&format!("mov {}, {}", call_reg, abi::int_result_reg(emitter))); // keep the selected callable descriptor while initializing the counter - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s callback-invocation counter - abi::emit_push_reg(emitter, call_reg); // save the selected callable descriptor beneath the loop receiver - (Vec::new(), precomputed_sig, CallbackSlotKind::Descriptor, None) - } else { - let captures = if let Some((resolved_name, _)) = direct_fcc_function.as_ref() { - let label = crate::names::function_symbol(resolved_name); - abi::emit_symbol_address(emitter, call_reg, &label); - Vec::new() - } else { - callback_env::materialize_callback_address(&args[1], call_reg, emitter, ctx, data) - }; - let sig: Option = if direct_fcc_function.is_none() && is_callable_expr { - ctx.deferred_closures - .last() - .map(|deferred| deferred.sig.clone()) - } else { - precomputed_sig - }; - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s callback-invocation counter - abi::emit_push_reg(emitter, call_reg); // save the resolved callback address beneath the loop receiver - (captures, sig, CallbackSlotKind::EntryAddress, None) - } - }; - let ret_ty = sig - .as_ref() - .map(|sig| sig.return_type.clone()) - .unwrap_or_else(|| if runtime_string_callback { PhpType::Mixed } else { PhpType::Int }); - - let callback_arg_source = match callback_args_expr(args) { - CallbackArgsExpr::Literal(callback_args) - if runtime_string_callback || callback_slot_kind == CallbackSlotKind::Descriptor => - { - let arg_array = iterator_apply_descriptor_arg_array( - descriptor_arg_prefix.as_ref(), - callback_args, - args.get(2).map(|arg| arg.span).unwrap_or(args[1].span), - ); - let arg_array_ty = emit_expr(&arg_array, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s synthesized callback-argument array for every invocation - CallbackArgSource::Dynamic { - args_offset: 16, - callback_offset: 32, - count_offset: 48, - arg_array_ty, - literal_elems: Some(callback_args), - } - } - CallbackArgsExpr::Evaluated { - expr, - literal_elems: _, - } if callback_slot_kind == CallbackSlotKind::Descriptor - && descriptor_arg_prefix.is_some() => - { - let arg_array_ty = crate::codegen::functions::infer_contextual_type(expr, ctx); - let prefixed = receiver_call_args::emit_receiver_prefixed_dynamic_arg_mixed( - descriptor_arg_prefix - .as_ref() - .expect("descriptor prefix checked before dynamic arg rewriting"), - expr, - &arg_array_ty, - emitter, - ctx, - data, - ); - debug_assert!(prefixed); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s receiver-prefixed callback-argument array for every invocation - CallbackArgSource::Dynamic { - args_offset: 16, - callback_offset: 32, - count_offset: 48, - arg_array_ty: PhpType::Mixed, - literal_elems: None, - } - } - CallbackArgsExpr::Evaluated { - expr, - literal_elems, - } => { - let arg_array_ty = emit_expr(expr, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s evaluated callback-argument array for every invocation - CallbackArgSource::Dynamic { - args_offset: 16, - callback_offset: 32, - count_offset: 48, - arg_array_ty, - literal_elems, - } - } - CallbackArgsExpr::Literal(args) => CallbackArgSource::Literal { - args, - callback_offset: 16, - count_offset: 32, - }, - }; - let receiver_offset = match callback_arg_source { - CallbackArgSource::Dynamic { .. } => 48, - CallbackArgSource::Literal { .. } => 32, - }; - emit_iterator_apply_loops( - &source_kind, - &callback_arg_source, - &captures, - sig.as_ref(), - &ret_ty, - runtime_string_callback, - callback_slot_kind, - receiver_offset, - emitter, - ctx, - data, - ); - if matches!(callback_arg_source, CallbackArgSource::Dynamic { .. }) { - abi::emit_release_temporary_stack(emitter, 16); // discard the evaluated iterator_apply() args array - } - if callback_slot_kind == CallbackSlotKind::Descriptor { - release_saved_descriptor_callback_slot(emitter); - } else { - abi::emit_release_temporary_stack(emitter, 16); // discard the saved callback slot after iteration - } - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the final iterator_apply() invocation count - abi::emit_release_temporary_stack(emitter, 16); // discard the receiver preserved while resolving the callback - Some(PhpType::Int) -} - -/// Emits one iterator_apply() branch for a runtime-selected callable-array descriptor. -fn emit_runtime_callable_array_iterator_apply_case( - case: &RuntimeCallableCase, - receiver_ty: Option<&PhpType>, - source_kind: &ApplySourceKind, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let call_reg = abi::nested_call_reg(emitter); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s callback-invocation counter for the selected descriptor - abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); - abi::emit_push_reg(emitter, call_reg); // save the selected callable-array descriptor beneath callback args - - let arg_array_ty = - emit_runtime_callable_array_iterator_args(receiver_ty, args, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_apply()'s descriptor argument container for every invocation - - let selected_receiver_bytes = usize::from(receiver_ty.is_some()) * 16; - let callback_arg_source = CallbackArgSource::Dynamic { - args_offset: 16, - callback_offset: 32, - count_offset: 48, - arg_array_ty, - literal_elems: None, - }; - emit_iterator_apply_loops( - source_kind, - &callback_arg_source, - &[], - Some(&case.sig), - &PhpType::Mixed, - false, - CallbackSlotKind::Descriptor, - 48 + selected_receiver_bytes, - emitter, - ctx, - data, - ); - abi::emit_release_temporary_stack(emitter, 16); // discard the selected descriptor argument container - release_saved_descriptor_callback_slot(emitter); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the final iterator_apply() invocation count - if selected_receiver_bytes != 0 { - abi::emit_release_temporary_stack(emitter, selected_receiver_bytes); // discard the saved runtime callable-array receiver - } - abi::emit_release_temporary_stack(emitter, 16); // discard the receiver preserved while resolving the callback -} - -/// Evaluates iterator_apply() callback args for a runtime-selected callable-array descriptor. -fn emit_runtime_callable_array_iterator_args( - receiver_ty: Option<&PhpType>, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let args_span = args.get(2).map(|arg| arg.span).unwrap_or(args[1].span); - let callback_args = callback_args_expr(args); - match receiver_ty { - Some(_) => { - let (arg_expr, arg_ty) = match callback_args { - CallbackArgsExpr::Literal(callback_args) => ( - iterator_apply_descriptor_arg_array(None, callback_args, args_span), - PhpType::Array(Box::new(PhpType::Mixed)), - ), - CallbackArgsExpr::Evaluated { expr, .. } => { - (expr.clone(), crate::codegen::functions::infer_contextual_type(expr, ctx)) - } - }; - let prefixed = receiver_call_args::emit_saved_receiver_prefixed_dynamic_arg_mixed( - 32, - &arg_expr, - &arg_ty, - emitter, - ctx, - data, - ); - debug_assert!(prefixed); - PhpType::Mixed - } - None => match callback_args { - CallbackArgsExpr::Literal(callback_args) => { - let arg_array = - iterator_apply_descriptor_arg_array(None, callback_args, args_span); - emit_expr(&arg_array, emitter, ctx, data) - } - CallbackArgsExpr::Evaluated { expr, .. } => emit_expr(expr, emitter, ctx, data), - }, - } -} - -/// Emits the shared iterator loop body for iterator_apply() once callback state is staged. -#[allow(clippy::too_many_arguments)] -fn emit_iterator_apply_loops( - source_kind: &ApplySourceKind, - callback_arg_source: &CallbackArgSource<'_>, - captures: &[(String, PhpType, bool)], - sig: Option<&FunctionSig>, - ret_ty: &PhpType, - runtime_string_callback: bool, - callback_slot_kind: CallbackSlotKind, - receiver_offset: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - abi::emit_load_temporary_stack_slot( - emitter, - abi::int_result_reg(emitter), - receiver_offset, - ); - - let loop_start = ctx.next_label("iterator_apply_start"); - let loop_end = ctx.next_label("iterator_apply_end"); - let loop_cont = ctx.next_label("iterator_apply_cont"); - match source_kind { - ApplySourceKind::StaticObject(class_name) => { - emit_iterator_loop( - class_name, - &loop_start, - &loop_end, - &loop_cont, - emitter, - ctx, - data, - |_, _, _, _| (), - |_, emitter, ctx, data| { - emit_callback_invocation( - callback_arg_source, - captures, - sig, - ret_ty, - runtime_string_callback, - callback_slot_kind, - &loop_end, - emitter, - ctx, - data, - ); - }, - |_, _, _, _| {}, - ); - } - ApplySourceKind::TraversableObject => { - emit_iterable_object_loop( - "iterator_apply_traversable", - emitter, - ctx, - data, - |_, _, _, _| (), - |_, active_loop_end, emitter, ctx, data| { - emit_callback_invocation( - callback_arg_source, - captures, - sig, - ret_ty, - runtime_string_callback, - callback_slot_kind, - active_loop_end, - emitter, - ctx, - data, - ); - }, - |_, _, _, _| {}, - ); - } - ApplySourceKind::RuntimeIterable => { - emit_apply_loaded_iterable( - callback_arg_source, - captures, - sig, - ret_ty, - runtime_string_callback, - callback_slot_kind, - emitter, - ctx, - data, - ); - } - } -} - -enum ApplySourceKind { - StaticObject(String), - TraversableObject, - RuntimeIterable, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum CallbackSlotKind { - EntryAddress, - Descriptor, -} - -enum CallbackArgSource<'a> { - Literal { - args: &'a [Expr], - callback_offset: usize, - count_offset: usize, - }, - Dynamic { - args_offset: usize, - callback_offset: usize, - count_offset: usize, - arg_array_ty: PhpType, - literal_elems: Option<&'a [Expr]>, - }, -} - -enum CallbackArgsExpr<'a> { - Literal(&'a [Expr]), - Evaluated { - expr: &'a Expr, - literal_elems: Option<&'a [Expr]>, - }, -} - -/// Builds the AST expression for callback args. -fn callback_args_expr(args: &[Expr]) -> CallbackArgsExpr<'_> { - match args.get(2) { - Some(arg) => match &arg.kind { - ExprKind::Null => CallbackArgsExpr::Literal(&[]), - ExprKind::ArrayLiteral(elems) if elems.iter().all(is_static_callback_arg_literal) => { - CallbackArgsExpr::Literal(elems.as_slice()) - } - ExprKind::ArrayLiteral(elems) => CallbackArgsExpr::Evaluated { - expr: arg, - literal_elems: Some(elems.as_slice()), - }, - _ => CallbackArgsExpr::Evaluated { - expr: arg, - literal_elems: None, - }, - }, - None => CallbackArgsExpr::Literal(&[]), - } -} - -/// Builds the callback argument array, optionally prefixing a callable-array receiver. -fn iterator_apply_descriptor_arg_array( - descriptor_arg_prefix: Option<&Expr>, - callback_args: &[Expr], - span: crate::span::Span, -) -> Expr { - let mut elems = - Vec::with_capacity(callback_args.len() + usize::from(descriptor_arg_prefix.is_some())); - if let Some(prefix) = descriptor_arg_prefix { - elems.push(prefix.clone()); - } - elems.extend(callback_args.iter().cloned()); - Expr::new(ExprKind::ArrayLiteral(elems), span) -} - -/// Returns true when static callback arg literal. -fn is_static_callback_arg_literal(expr: &Expr) -> bool { - match &expr.kind { - ExprKind::StringLiteral(_) - | ExprKind::IntLiteral(_) - | ExprKind::FloatLiteral(_) - | ExprKind::BoolLiteral(_) - | ExprKind::Null => true, - ExprKind::Negate(inner) => matches!( - inner.kind, - ExprKind::IntLiteral(_) | ExprKind::FloatLiteral(_) - ), - _ => false, - } -} - -/// Emits assembly for callback invocation. -fn emit_callback_invocation( - callback_arg_source: &CallbackArgSource<'_>, - captures: &[(String, PhpType, bool)], - sig: Option<&FunctionSig>, - ret_ty: &PhpType, - runtime_string_callback: bool, - callback_slot_kind: CallbackSlotKind, - loop_end: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let call_reg = abi::nested_call_reg(emitter); - let callback_offset = match callback_arg_source { - CallbackArgSource::Literal { - callback_offset, .. - } - | CallbackArgSource::Dynamic { - callback_offset, .. - } => *callback_offset, - }; - if !runtime_string_callback { - abi::emit_load_temporary_stack_slot(emitter, call_reg, callback_offset); - } - - if let CallbackArgSource::Dynamic { - args_offset, - count_offset, - arg_array_ty, - literal_elems, - .. - } = callback_arg_source - { - let save_concat_before_args = emitter.target.arch == Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - let dynamic_ret_ty = if callback_slot_kind == CallbackSlotKind::Descriptor { - call_user_func_array::emit_call_descriptor_array_invoker( - LoadedArraySource::TemporaryStackSlot(*args_offset), - arg_array_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - PhpType::Mixed - } else if runtime_string_callback { - call_user_func_array::emit_loaded_array_string_callback_call( - LoadedArraySource::TemporaryStackSlot(*args_offset), - arg_array_ty, - callback_offset, - callback_offset + 8, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ) - } else if let Some(sig) = sig { - call_user_func_array::emit_loaded_array_callback_call( - LoadedArraySource::TemporaryStackSlot(*args_offset), - arg_array_ty, - *literal_elems, - call_reg, - captures, - sig, - save_concat_before_args, - emitter, - ctx, - data, - ) - } else { - call_user_func_array::emit_loaded_array_unknown_callback_call( - LoadedArraySource::TemporaryStackSlot(*args_offset), - arg_array_ty, - call_reg, - captures, - None, - save_concat_before_args, - emitter, - ctx, - data, - ) - }; - crate::codegen::expr::coerce_to_truthiness(emitter, ctx, &dynamic_ret_ty); - iterator_common::emit_increment_saved_count_at_offset(*count_offset, emitter); - emit_branch_if_callback_false(emitter, loop_end); - return; - } - - let (callback_args, count_offset) = match callback_arg_source { - CallbackArgSource::Literal { - args, - count_offset, - .. - } => (*args, *count_offset), - CallbackArgSource::Dynamic { .. } => unreachable!(), - }; - debug_assert!(callback_slot_kind == CallbackSlotKind::EntryAddress); - - let save_concat_before_args = emitter.target.arch == Arch::X86_64; - if save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - - let mut arg_types = Vec::new(); - for (i, arg) in callback_args.iter().enumerate() { - let target_ty = call_args::declared_target_ty(sig, i); - let pushed_ty = call_args::push_expr_arg(arg, target_ty, emitter, ctx, data); - arg_types.push(pushed_ty); - } - - if let Some(sig) = sig { - let visible_param_count = sig.params.len(); - let regular_param_count = if sig.variadic.is_some() { - visible_param_count.saturating_sub(1) - } else { - visible_param_count - }; - for i in arg_types.len()..regular_param_count { - if let Some(Some(default_expr)) = sig.defaults.get(i) { - let target_ty = sig.params.get(i).map(|(_, ty)| ty); - let pushed_ty = call_args::push_expr_arg(default_expr, target_ty, emitter, ctx, data); - arg_types.push(pushed_ty); - } - } - } - callback_env::push_captures_as_hidden_args(captures, emitter, ctx, &mut arg_types); - - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); - - if !save_concat_before_args { - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - } - abi::emit_call_reg(emitter, call_reg); - if save_concat_before_args { - abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, ret_ty); - } else { - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, ret_ty); - abi::emit_release_temporary_stack(emitter, overflow_bytes); - } - - crate::codegen::expr::coerce_to_truthiness(emitter, ctx, ret_ty); - iterator_common::emit_increment_saved_count_at_offset(count_offset, emitter); - emit_branch_if_callback_false(emitter, loop_end); -} - -/// Emits assembly for apply loaded iterable. -fn emit_apply_loaded_iterable( - callback_arg_source: &CallbackArgSource<'_>, - captures: &[(String, PhpType, bool)], - sig: Option<&FunctionSig>, - ret_ty: &PhpType, - runtime_string_callback: bool, - callback_slot_kind: CallbackSlotKind, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let object_case = ctx.next_label("iterator_apply_iterable_object"); - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterable pointer across heap-kind probing - abi::emit_call_label(emitter, "__rt_heap_kind"); // classify the iterator_apply() Traversable candidate - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #4"); // is the iterable payload an object? - emitter.instruction(&format!("b.eq {}", object_case)); // dispatch object payloads through Traversable runtime checks - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 4"); // is the iterable payload an object? - emitter.instruction(&format!("je {}", object_case)); // dispatch object payloads through Traversable runtime checks - } - } - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // discard non-object iterable payload before reporting unsupported input - abi::emit_call_label(emitter, "__rt_iterable_unsupported_kind"); // iterator_apply() cannot traverse array payloads - - emitter.label(&object_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore object payload before Traversable dispatch - emit_iterable_object_loop( - "iterator_apply_iterable", - emitter, - ctx, - data, - |_, _, _, _| (), - |_, active_loop_end, emitter, ctx, data| { - emit_callback_invocation( - callback_arg_source, - captures, - sig, - ret_ty, - runtime_string_callback, - callback_slot_kind, - active_loop_end, - emitter, - ctx, - data, - ); - }, - |_, _, _, _| {}, - ); -} - -/// Releases the retained descriptor stored in iterator_apply()'s saved callback slot. -fn release_saved_descriptor_callback_slot(emitter: &mut Emitter) { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 0); - callable_descriptor::emit_release_current_descriptor(emitter); - abi::emit_release_temporary_stack(emitter, 16); // discard the saved callable descriptor after releasing it -} - -/// Emits assembly for branch if callback false. -fn emit_branch_if_callback_false(emitter: &mut Emitter, loop_end: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did iterator_apply() callback request iteration stop? - emitter.instruction(&format!("b.eq {}", loop_end)); // stop before next() when callback returned false - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did iterator_apply() callback request iteration stop? - emitter.instruction(&format!("je {}", loop_end)); // stop before next() when callback returned false - } - } -} diff --git a/src/codegen/builtins/spl/iterator_common.rs b/src/codegen/builtins/spl/iterator_common.rs deleted file mode 100644 index 770746700b..0000000000 --- a/src/codegen/builtins/spl/iterator_common.rs +++ /dev/null @@ -1,413 +0,0 @@ -//! Purpose: -//! Shared lowering helpers for SPL iterator helper builtins. -//! Builds temporary result containers and bridges Iterator method results to array/hash storage. -//! -//! Called from: -//! - `crate::codegen::builtins::spl::iterator_count` -//! - `crate::codegen::builtins::spl::iterator_to_array` -//! -//! Key details: -//! - Iterator loop state keeps the receiver at the top of the temporary stack. -//! - Extra builtin state is stored underneath that receiver so foreach-style dispatch can reuse it. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::codegen::stmt::{reload_iterator_receiver, IteratorDispatchTarget}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Provides the Iterator object name helper used by the iterator common module. -pub(super) fn iterator_object_name(ty: &PhpType) -> Option<&str> { - match ty { - PhpType::Object(class_name) => Some(class_name.as_str()), - _ => None, - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum PreserveKeysArg { - Static(bool), - Dynamic, -} - -/// Builds the argument metadata for preserve keys. -pub(super) fn preserve_keys_arg(args: &[Expr]) -> PreserveKeysArg { - match args.get(1).map(|arg| &arg.kind) { - Some(kind) => static_truthiness(kind) - .map(PreserveKeysArg::Static) - .unwrap_or(PreserveKeysArg::Dynamic), - None => PreserveKeysArg::Static(true), - } -} - -/// Provides the Static truthiness helper used by the iterator common module. -fn static_truthiness(kind: &ExprKind) -> Option { - match 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, - } -} - -/// Emits assembly for count loaded array. -pub(super) fn emit_count_loaded_array(source_ty: &PhpType, emitter: &mut Emitter) -> bool { - match source_ty.codegen_repr() { - PhpType::Array(_) | PhpType::AssocArray { .. } => { - abi::emit_load_from_address( - emitter, - abi::int_result_reg(emitter), - abi::int_result_reg(emitter), - 0, - ); - true - } - _ => false, - } -} - -/// Emits assembly for clone loaded array. -pub(super) fn emit_clone_loaded_array(source_ty: &PhpType, emitter: &mut Emitter) -> Option { - match source_ty.codegen_repr() { - PhpType::Array(elem_ty) => { - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // pass the loaded indexed array to the shallow-clone helper - } - abi::emit_call_label(emitter, "__rt_array_clone_shallow"); - Some(PhpType::Array(elem_ty)) - } - PhpType::AssocArray { key, value } => { - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // pass the loaded hash to the shallow-clone helper - } - abi::emit_call_label(emitter, "__rt_hash_clone_shallow"); - Some(PhpType::AssocArray { key, value }) - } - _ => None, - } -} - -/// Emits assembly for clone loaded runtime indexed array as mixed. -pub(super) fn emit_clone_loaded_runtime_indexed_array_as_mixed(emitter: &mut Emitter) { - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // pass the runtime indexed array to the shallow-clone helper - } - abi::emit_call_label(emitter, "__rt_array_clone_shallow"); - emit_loaded_runtime_indexed_array_as_mixed(emitter); -} - -/// Emits assembly for loaded runtime indexed array as mixed. -pub(super) fn emit_loaded_runtime_indexed_array_as_mixed(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x1, [x0, #-8]"); // load packed indexed-array metadata before widening to Mixed slots - emitter.instruction("lsr x1, x1, #8"); // move the runtime value_type tag into the low bits - emitter.instruction("and x1, x1, #0x7f"); // isolate the indexed-array value_type tag for conversion - abi::emit_call_label(emitter, "__rt_array_to_mixed"); // convert cloned indexed-array slots to boxed Mixed cells - } - Arch::X86_64 => { - emitter.instruction("mov rsi, QWORD PTR [rax - 8]"); // load packed indexed-array metadata before widening to Mixed slots - emitter.instruction("shr rsi, 8"); // move the runtime value_type tag into the low bits - emitter.instruction("and rsi, 0x7f"); // isolate the indexed-array value_type tag for conversion - emitter.instruction("mov rdi, rax"); // pass the cloned indexed array to the Mixed conversion helper - abi::emit_call_label(emitter, "__rt_array_to_mixed"); // convert cloned indexed-array slots to boxed Mixed cells - } - } -} - -/// Emits assembly for clone loaded runtime hash as mixed. -pub(super) fn emit_clone_loaded_runtime_hash_as_mixed(emitter: &mut Emitter) { - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // pass the runtime hash to the shallow-clone helper - } - abi::emit_call_label(emitter, "__rt_hash_clone_shallow"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_call_label(emitter, "__rt_hash_to_mixed"); // convert cloned hash entries to boxed Mixed cells - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // pass the cloned hash to the Mixed conversion helper - abi::emit_call_label(emitter, "__rt_hash_to_mixed"); // convert cloned hash entries to boxed Mixed cells - } - } -} - -/// Emits assembly for new mixed indexed array. -pub(super) fn emit_new_mixed_indexed_array(emitter: &mut Emitter) { - abi::emit_load_int_immediate(emitter, abi::int_arg_reg_name(emitter.target, 0), 16); - abi::emit_load_int_immediate(emitter, abi::int_arg_reg_name(emitter.target, 1), 8); - abi::emit_call_label(emitter, "__rt_array_new"); - crate::codegen::expr::arrays::emit_array_value_type_stamp( - emitter, - abi::int_result_reg(emitter), - &PhpType::Mixed, - ); -} - -/// Emits assembly for new mixed hash. -pub(super) fn emit_new_mixed_hash(emitter: &mut Emitter) { - abi::emit_load_int_immediate(emitter, abi::int_arg_reg_name(emitter.target, 0), 16); - abi::emit_load_int_immediate( - emitter, - abi::int_arg_reg_name(emitter.target, 1), - crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64, - ); - abi::emit_call_label(emitter, "__rt_hash_new"); -} - -/// Emits assembly for save result under receiver. -pub(super) fn emit_save_result_under_receiver(emitter: &mut Emitter) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); -} - -/// Emits assembly for restore receiver from preserved reg. -pub(super) fn emit_restore_receiver_from_preserved_reg(emitter: &mut Emitter, receiver_reg: &str) { - emitter.instruction(&format!( // restore the iterator receiver as the next loop-driver input - "mov {}, {}", - abi::int_result_reg(emitter), - receiver_reg - )); -} - -/// Emits assembly for increment saved count. -pub(super) fn emit_increment_saved_count(emitter: &mut Emitter) { - emit_increment_saved_count_at_offset(16, emitter); -} - -/// Emits assembly for increment saved count at offset. -pub(super) fn emit_increment_saved_count_at_offset(offset: usize, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x9, [sp, #{}]", offset)); // load the saved iterator helper counter beneath the receiver slot - emitter.instruction("add x9, x9, #1"); // count this valid iterator position - emitter.instruction(&format!("str x9, [sp, #{}]", offset)); // persist the updated iterator helper counter - } - Arch::X86_64 => { - emitter.instruction(&format!("add QWORD PTR [rsp + {}], 1", offset)); // count this valid iterator position beneath the receiver slot - } - } -} - -/// Emits assembly for append current to saved array. -pub(super) fn emit_append_current_to_saved_array( - dispatch_target: &IteratorDispatchTarget, - emitter: &mut Emitter, - ctx: &mut Context, -) { - reload_iterator_receiver(emitter); - let current_ty = dispatch_target.dispatch("current", emitter, ctx); - crate::codegen::emit_box_current_value_as_mixed(emitter, ¤t_ty.codegen_repr()); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed current() value while loading the result array - emitter.instruction("ldr x0, [sp, #32]"); // load iterator_to_array()'s indexed result array beneath receiver and value - emitter.instruction("ldr x1, [sp], #16"); // restore boxed current() as the appended mixed payload - emitter.instruction("bl __rt_array_push_int"); // append the owned mixed value to the indexed result array - emitter.instruction("str x0, [sp, #16]"); // save the possibly-grown result array beneath the receiver slot - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve a temporary slot for the boxed current() value - emitter.instruction("mov QWORD PTR [rsp], rax"); // preserve the boxed current() value while loading the result array - emitter.instruction("mov rdi, QWORD PTR [rsp + 32]"); // load iterator_to_array()'s indexed result array beneath receiver and value - emitter.instruction("mov rsi, QWORD PTR [rsp]"); // pass boxed current() as the appended mixed payload - emitter.instruction("add rsp, 16"); // restore the stack so the receiver is again the top temporary slot - emitter.instruction("call __rt_array_push_int"); // append the owned mixed value to the indexed result array - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // save the possibly-grown result array beneath the receiver slot - } - } -} - -/// Emits assembly for insert current with iterator key. -pub(super) fn emit_insert_current_with_iterator_key( - dispatch_target: &IteratorDispatchTarget, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - reload_iterator_receiver(emitter); - let key_ty = dispatch_target.dispatch("key", emitter, ctx); - emit_normalized_key_from_result(&key_ty.codegen_repr(), emitter, ctx, data); - reload_iterator_receiver(emitter); - let (key_lo_reg, key_hi_reg) = normalized_key_regs(emitter); - abi::emit_push_reg_pair(emitter, key_lo_reg, key_hi_reg); // preserve the normalized iterator key while current() is dispatched - - let current_ty = dispatch_target.dispatch("current", emitter, ctx); - crate::codegen::emit_box_current_value_as_mixed(emitter, ¤t_ty.codegen_repr()); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, x0"); // pass the boxed current() value as hash value_lo - emitter.instruction("mov x4, xzr"); // boxed mixed hash values do not use value_hi - emitter.instruction("mov x5, #7"); // value tag 7 tells the hash it owns a boxed mixed cell - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the normalized iterator key into hash-set argument registers - emitter.instruction("ldr x0, [sp, #16]"); // load iterator_to_array()'s associative result hash beneath the receiver slot - emitter.instruction("bl __rt_hash_set"); // insert or update the preserved key with the owned mixed current() value - emitter.instruction("str x0, [sp, #16]"); // save the possibly-grown result hash beneath the receiver slot - } - Arch::X86_64 => { - emitter.instruction("mov rcx, rax"); // pass the boxed current() value as hash value_lo - emitter.instruction("xor r8, r8"); // boxed mixed hash values do not use value_hi - emitter.instruction("mov r9, 7"); // value tag 7 tells the hash it owns a boxed mixed cell - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore the normalized iterator key into hash-set argument registers - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // load iterator_to_array()'s associative result hash beneath the receiver slot - emitter.instruction("call __rt_hash_set"); // insert or update the preserved key with the owned mixed current() value - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // save the possibly-grown result hash beneath the receiver slot - } - } -} - -/// Provides the Normalized key regs helper used by the iterator common module. -fn normalized_key_regs(emitter: &Emitter) -> (&'static str, &'static str) { - match emitter.target.arch { - Arch::AArch64 => ("x1", "x2"), - Arch::X86_64 => ("rax", "rdx"), - } -} - -/// Emits assembly for normalized key from result. -fn emit_normalized_key_from_result( - key_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - match key_ty { - PhpType::Int | PhpType::Bool => emit_integer_key_from_result(emitter), - PhpType::Float => emit_float_key_from_result(emitter), - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_hash_normalize_key"); - } - PhpType::Mixed | PhpType::Union(_) => emit_mixed_key_from_result(emitter, ctx, data), - _ => emit_integer_key_from_result(emitter), - } -} - -/// Emits assembly for integer key from result. -fn emit_integer_key_from_result(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // use the scalar key payload as normalized key_lo - emitter.instruction("mov x2, #-1"); // key_hi sentinel marks the iterator key as integer - } - Arch::X86_64 => { - emitter.instruction("mov rdx, -1"); // key_hi sentinel marks the iterator key as integer while rax stays key_lo - } - } -} - -/// Emits assembly for float key from result. -fn emit_float_key_from_result(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fcvtzs x1, d0"); // PHP casts float iterator keys to integer array keys - emitter.instruction("mov x2, #-1"); // key_hi sentinel marks the iterator key as integer - } - Arch::X86_64 => { - emitter.instruction("cvttsd2si rax, xmm0"); // PHP casts float iterator keys to integer array keys - emitter.instruction("mov rdx, -1"); // key_hi sentinel marks the iterator key as integer - } - } -} - -/// Emits assembly for mixed key from result. -fn emit_mixed_key_from_result(emitter: &mut Emitter, ctx: &mut Context, data: &mut DataSection) { - let string_label = ctx.next_label("iterator_key_string"); - let int_label = ctx.next_label("iterator_key_int"); - let bool_label = ctx.next_label("iterator_key_bool"); - let float_label = ctx.next_label("iterator_key_float"); - let null_label = ctx.next_label("iterator_key_null"); - let done_label = ctx.next_label("iterator_key_done"); - let (empty_label, _) = data.add_string(b""); - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #1"); // is the mixed iterator key a string? - emitter.instruction(&format!("b.eq {}", string_label)); // normalize string iterator keys through the hash key helper - emitter.instruction("cmp x0, #0"); // is the mixed iterator key an integer? - emitter.instruction(&format!("b.eq {}", int_label)); // use integer payloads directly as array keys - emitter.instruction("cmp x0, #3"); // is the mixed iterator key a boolean? - emitter.instruction(&format!("b.eq {}", bool_label)); // use boolean payloads as integer array keys - emitter.instruction("cmp x0, #2"); // is the mixed iterator key a float? - emitter.instruction(&format!("b.eq {}", float_label)); // cast float iterator keys to integer array keys - emitter.instruction("cmp x0, #8"); // is the mixed iterator key null? - emitter.instruction(&format!("b.eq {}", null_label)); // PHP treats null array keys as the empty string - emitter.instruction(&format!("b {}", int_label)); // unsupported key payloads fall back to their low word - - emitter.label(&string_label); - emitter.instruction("bl __rt_hash_normalize_key"); // normalize numeric-string iterator keys before insertion - emitter.instruction(&format!("b {}", done_label)); // skip scalar-key normalization after string handling - - emitter.label(&int_label); - emitter.instruction("mov x2, #-1"); // mark the unboxed integer low word as an integer key - emitter.instruction(&format!("b {}", done_label)); // finish normalized mixed-key handling - - emitter.label(&bool_label); - emitter.instruction("mov x2, #-1"); // mark the unboxed boolean low word as an integer key - emitter.instruction(&format!("b {}", done_label)); // finish normalized mixed-key handling - - emitter.label(&float_label); - emitter.instruction("fmov d0, x1"); // reinterpret the unboxed float payload bits for integer-key casting - emitter.instruction("fcvtzs x1, d0"); // PHP casts float array keys to integer keys - emitter.instruction("mov x2, #-1"); // mark the converted float payload as an integer key - emitter.instruction(&format!("b {}", done_label)); // finish normalized mixed-key handling - - emitter.label(&null_label); - abi::emit_symbol_address(emitter, "x1", &empty_label); - emitter.instruction("mov x2, #0"); // null iterator keys become the empty-string key - emitter.instruction("bl __rt_hash_normalize_key"); // preserve empty-string key semantics for hash insertion - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 1"); // is the mixed iterator key a string? - emitter.instruction(&format!("je {}", string_label)); // normalize string iterator keys through the hash key helper - emitter.instruction("cmp rax, 0"); // is the mixed iterator key an integer? - emitter.instruction(&format!("je {}", int_label)); // use integer payloads directly as array keys - emitter.instruction("cmp rax, 3"); // is the mixed iterator key a boolean? - emitter.instruction(&format!("je {}", bool_label)); // use boolean payloads as integer array keys - emitter.instruction("cmp rax, 2"); // is the mixed iterator key a float? - emitter.instruction(&format!("je {}", float_label)); // cast float iterator keys to integer array keys - emitter.instruction("cmp rax, 8"); // is the mixed iterator key null? - emitter.instruction(&format!("je {}", null_label)); // PHP treats null array keys as the empty string - emitter.instruction(&format!("jmp {}", int_label)); // unsupported key payloads fall back to their low word - - emitter.label(&string_label); - emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into hash-normalize key_lo - emitter.instruction("call __rt_hash_normalize_key"); // normalize numeric-string iterator keys before insertion - emitter.instruction(&format!("jmp {}", done_label)); // skip scalar-key normalization after string handling - - emitter.label(&int_label); - emitter.instruction("mov rax, rdi"); // move the unboxed integer low word into normalized key_lo - emitter.instruction("mov rdx, -1"); // mark the key as integer - emitter.instruction(&format!("jmp {}", done_label)); // finish normalized mixed-key handling - - emitter.label(&bool_label); - emitter.instruction("mov rax, rdi"); // move the unboxed boolean low word into normalized key_lo - emitter.instruction("mov rdx, -1"); // mark the key as integer - emitter.instruction(&format!("jmp {}", done_label)); // finish normalized mixed-key handling - - emitter.label(&float_label); - emitter.instruction("movq xmm0, rdi"); // reinterpret the unboxed float payload bits for integer-key casting - emitter.instruction("cvttsd2si rax, xmm0"); // PHP casts float array keys to integer keys - emitter.instruction("mov rdx, -1"); // mark the converted float payload as an integer key - emitter.instruction(&format!("jmp {}", done_label)); // finish normalized mixed-key handling - - emitter.label(&null_label); - abi::emit_symbol_address(emitter, "rax", &empty_label); - emitter.instruction("xor rdx, rdx"); // null iterator keys become the empty-string key - emitter.instruction("call __rt_hash_normalize_key"); // preserve empty-string key semantics for hash insertion - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/spl/iterator_count.rs b/src/codegen/builtins/spl/iterator_count.rs deleted file mode 100644 index d0b5683b15..0000000000 --- a/src/codegen/builtins/spl/iterator_count.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Purpose: -//! Emits PHP `iterator_count()` calls for arrays and Iterator/IteratorAggregate objects. -//! Reuses the statement foreach iterator driver for object traversal. -//! -//! Called from: -//! - `crate::codegen::builtins::spl::emit()` -//! -//! Key details: -//! - Object iteration calls rewind(), valid(), and next() just like PHP and leaves the iterator exhausted. -//! - The saved count lives beneath the loop driver's receiver stack slot. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::codegen::stmt::{emit_iterable_object_loop, emit_iterator_loop}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::iterator_common; - -/// Emits the iterator count entry point for this module. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("iterator_count()"); - let source_ty = emit_expr(&args[0], emitter, ctx, data); - if iterator_common::emit_count_loaded_array(&source_ty, emitter) { - return Some(PhpType::Int); - } - - if matches!(source_ty.codegen_repr(), PhpType::Iterable) { - emit_count_loaded_iterable(emitter, ctx, data); - return Some(PhpType::Int); - } - - let Some(class_name) = iterator_common::iterator_object_name(&source_ty) else { - return Some(PhpType::Int); - }; - - if class_name == "Traversable" { - emit_count_loaded_traversable_object(emitter, ctx, data); - return Some(PhpType::Int); - } - - emit_count_loaded_iterator_object(class_name, emitter, ctx, data); - Some(PhpType::Int) -} - -/// Emits assembly for count loaded iterator object. -fn emit_count_loaded_iterator_object( - class_name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let receiver_reg = abi::nested_call_reg(emitter); - emitter.instruction(&format!( // preserve iterator receiver while initializing the count slot - "mov {}, {}", - receiver_reg, - abi::int_result_reg(emitter) - )); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_count()'s counter underneath the loop receiver - iterator_common::emit_restore_receiver_from_preserved_reg(emitter, receiver_reg); - - let loop_start = ctx.next_label("iterator_count_start"); - let loop_end = ctx.next_label("iterator_count_end"); - let loop_cont = ctx.next_label("iterator_count_cont"); - emit_iterator_loop( - class_name, - &loop_start, - &loop_end, - &loop_cont, - emitter, - ctx, - data, - |_, _, _, _| (), - |_, emitter, _, _| iterator_common::emit_increment_saved_count(emitter), - |_, _, _, _| {}, - ); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the final saved iterator_count() counter -} - -/// Emits assembly for count loaded traversable object. -fn emit_count_loaded_traversable_object( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let receiver_reg = abi::nested_call_reg(emitter); - emitter.instruction(&format!( // preserve Traversable receiver while initializing the count slot - "mov {}, {}", - receiver_reg, - abi::int_result_reg(emitter) - )); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save iterator_count()'s counter underneath the loop receiver - iterator_common::emit_restore_receiver_from_preserved_reg(emitter, receiver_reg); - - emit_iterable_object_loop( - "iterator_count_traversable", - emitter, - ctx, - data, - |_, _, _, _| (), - |_, _, emitter, _, _| iterator_common::emit_increment_saved_count(emitter), - |_, _, _, _| {}, - ); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the final saved iterator_count() counter -} - -/// Emits assembly for count loaded iterable. -fn emit_count_loaded_iterable( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let indexed_case = ctx.next_label("iterator_count_iterable_indexed"); - let hash_case = ctx.next_label("iterator_count_iterable_hash"); - let object_case = ctx.next_label("iterator_count_iterable_object"); - let done = ctx.next_label("iterator_count_iterable_done"); - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterable pointer across heap-kind probing - abi::emit_call_label(emitter, "__rt_heap_kind"); // classify the type-erased iterable payload - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #2"); // is the iterable an indexed array? - emitter.instruction(&format!("b.eq {}", indexed_case)); // count indexed-array entries directly - emitter.instruction("cmp x0, #3"); // is the iterable an associative hash? - emitter.instruction(&format!("b.eq {}", hash_case)); // count hash entries directly - emitter.instruction("cmp x0, #4"); // is the iterable an object? - emitter.instruction(&format!("b.eq {}", object_case)); // count a Traversable object through Iterator dispatch - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 2"); // is the iterable an indexed array? - emitter.instruction(&format!("je {}", indexed_case)); // count indexed-array entries directly - emitter.instruction("cmp rax, 3"); // is the iterable an associative hash? - emitter.instruction(&format!("je {}", hash_case)); // count hash entries directly - emitter.instruction("cmp rax, 4"); // is the iterable an object? - emitter.instruction(&format!("je {}", object_case)); // count a Traversable object through Iterator dispatch - } - } - abi::emit_call_label(emitter, "__rt_iterable_unsupported_kind"); // unsupported iterable payloads abort with a fatal diagnostic - - emitter.label(&object_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the object pointer before Traversable counting - emit_count_loaded_traversable_object(emitter, ctx, data); - abi::emit_jump(emitter, &done); // skip array counting paths after object traversal - - emitter.label(&hash_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the hash pointer before reading its entry count - iterator_common::emit_count_loaded_array( - &PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }, - emitter, - ); - abi::emit_jump(emitter, &done); // skip indexed-array count after hash counting - - emitter.label(&indexed_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the indexed-array pointer before reading its length - iterator_common::emit_count_loaded_array(&PhpType::Array(Box::new(PhpType::Mixed)), emitter); - - emitter.label(&done); -} diff --git a/src/codegen/builtins/spl/iterator_to_array.rs b/src/codegen/builtins/spl/iterator_to_array.rs deleted file mode 100644 index b1a4b51d12..0000000000 --- a/src/codegen/builtins/spl/iterator_to_array.rs +++ /dev/null @@ -1,370 +0,0 @@ -//! Purpose: -//! Emits PHP `iterator_to_array()` calls for arrays and Iterator/IteratorAggregate objects. -//! Reuses the statement foreach iterator driver for object traversal. -//! -//! Called from: -//! - `crate::codegen::builtins::spl::emit()` -//! -//! Key details: -//! - `$preserve_keys=false` appends current() values without calling key(). -//! - `$preserve_keys=true` normalizes key() results through the associative-array hash ABI. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::coerce_to_truthiness; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::codegen::stmt::{emit_iterable_object_loop, emit_iterator_loop}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::iterator_common::{self, PreserveKeysArg}; - -/// Emits the iterator to array entry point for this module. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("iterator_to_array()"); - let preserve_keys = iterator_common::preserve_keys_arg(args); - let source_ty = emit_expr(&args[0], emitter, ctx, data); - - if matches!(preserve_keys, PreserveKeysArg::Dynamic) { - if let Some(arg) = args.get(1) { - return emit_dynamic_preserve_keys(&source_ty, arg, emitter, ctx, data); - } - } - - let PreserveKeysArg::Static(preserve_keys) = preserve_keys else { - unreachable!("dynamic preserve_keys requires a second argument") - }; - Some(emit_to_array_loaded_source( - &source_ty, - preserve_keys, - emitter, - ctx, - data, - )) -} - -/// Emits assembly for to array loaded source. -fn emit_to_array_loaded_source( - source_ty: &PhpType, - preserve_keys: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if preserve_keys { - if let Some(cloned_ty) = iterator_common::emit_clone_loaded_array(source_ty, emitter) { - return cloned_ty; - } - } else { - match source_ty.codegen_repr() { - PhpType::Array(_) => { - if let Some(cloned_ty) = iterator_common::emit_clone_loaded_array(source_ty, emitter) { - return cloned_ty; - } - } - PhpType::AssocArray { .. } => { - return crate::codegen::builtins::arrays::array_values::emit_loaded_values( - source_ty, - emitter, - ctx, - data, - ) - .unwrap_or_else(|| static_result_ty(source_ty, preserve_keys)); - } - _ => {} - } - } - - if matches!(source_ty.codegen_repr(), PhpType::Iterable) { - emit_to_array_loaded_iterable(preserve_keys, emitter, ctx, data); - return static_result_ty(source_ty, preserve_keys); - } - - let Some(class_name) = iterator_common::iterator_object_name(&source_ty) else { - return static_result_ty(source_ty, preserve_keys); - }; - - if class_name == "Traversable" { - emit_to_array_loaded_traversable_object(preserve_keys, emitter, ctx, data); - return static_result_ty(source_ty, preserve_keys); - } - - emit_to_array_loaded_iterator_object(class_name, preserve_keys, emitter, ctx, data); - static_result_ty(source_ty, preserve_keys) -} - -/// Emits assembly for dynamic preserve keys. -fn emit_dynamic_preserve_keys( - source_ty: &PhpType, - preserve_arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - if matches!(source_ty.codegen_repr(), PhpType::Array(_)) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterator_to_array() source while evaluating dynamic preserve_keys - let preserve_ty = emit_expr(preserve_arg, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &preserve_ty); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore indexed-array source; preserve_keys does not change indexed shape - return Some(emit_to_array_loaded_source( - source_ty, - true, - emitter, - ctx, - data, - )); - } - - let false_case = ctx.next_label("iterator_to_array_preserve_false"); - let done = ctx.next_label("iterator_to_array_preserve_done"); - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterator_to_array() source while evaluating dynamic preserve_keys - let preserve_ty = emit_expr(preserve_arg, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &preserve_ty); - abi::emit_branch_if_int_result_zero(emitter, &false_case); - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore source for the preserve_keys=true collection path - let true_ty = emit_to_array_loaded_source(source_ty, true, emitter, ctx, data); - emit_box_owned_result_as_mixed(&true_ty, emitter); - abi::emit_jump(emitter, &done); // skip preserve_keys=false path after producing the boxed result - - emitter.label(&false_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore source for the preserve_keys=false collection path - let false_ty = emit_to_array_loaded_source(source_ty, false, emitter, ctx, data); - emit_box_owned_result_as_mixed(&false_ty, emitter); - - emitter.label(&done); - Some(dynamic_result_ty(source_ty)) -} - -/// Emits assembly for to array loaded iterator object. -fn emit_to_array_loaded_iterator_object( - class_name: &str, - preserve_keys: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let receiver_reg = abi::nested_call_reg(emitter); - emitter.instruction(&format!( // preserve iterator receiver while allocating iterator_to_array()'s result - "mov {}, {}", - receiver_reg, - abi::int_result_reg(emitter) - )); - if preserve_keys { - iterator_common::emit_new_mixed_hash(emitter); - } else { - iterator_common::emit_new_mixed_indexed_array(emitter); - } - iterator_common::emit_save_result_under_receiver(emitter); - iterator_common::emit_restore_receiver_from_preserved_reg(emitter, receiver_reg); - - let loop_start = ctx.next_label("iterator_to_array_start"); - let loop_end = ctx.next_label("iterator_to_array_end"); - let loop_cont = ctx.next_label("iterator_to_array_cont"); - emit_iterator_loop( - class_name, - &loop_start, - &loop_end, - &loop_cont, - emitter, - ctx, - data, - |_, _, _, _| (), - |dispatch_target, emitter, ctx, data| { - if preserve_keys { - iterator_common::emit_insert_current_with_iterator_key( - dispatch_target, - emitter, - ctx, - data, - ); - } else { - iterator_common::emit_append_current_to_saved_array( - dispatch_target, - emitter, - ctx, - ); - } - }, - |_, _, _, _| {}, - ); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed iterator_to_array() result container -} - -/// Provides the Static result ty helper used by the iterator to array module. -fn static_result_ty(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)), - } -} - -/// Provides the Dynamic result ty helper used by the iterator to array module. -fn dynamic_result_ty(source_ty: &PhpType) -> PhpType { - merge_result_types( - static_result_ty(source_ty, true), - static_result_ty(source_ty, false), - ) -} - -/// Provides the Merge result types helper used by the iterator to array module. -fn merge_result_types(a: PhpType, b: PhpType) -> PhpType { - if a == b { - a - } else { - PhpType::Union(vec![a, b]) - } -} - -/// Emits assembly for box owned result as mixed. -fn emit_box_owned_result_as_mixed(result_ty: &PhpType, emitter: &mut Emitter) { - let result_ty = result_ty.codegen_repr(); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the owned iterator_to_array() result while boxing it - crate::codegen::emit_box_current_value_as_mixed(emitter, &result_ty); - emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed mixed result while releasing the original owner - emitter.instruction("ldr x0, [sp, #16]"); // reload the original iterator_to_array() result retained by the mixed box - abi::emit_decref_if_refcounted(emitter, &result_ty); - emitter.instruction("ldr x0, [sp], #16"); // restore the boxed iterator_to_array() result - emitter.instruction("add sp, sp, #16"); // discard the saved original iterator_to_array() result pointer - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the owned iterator_to_array() result while boxing it - crate::codegen::emit_box_current_value_as_mixed(emitter, &result_ty); - abi::emit_push_reg(emitter, "rax"); // preserve the boxed mixed result while releasing the original owner - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the original iterator_to_array() result retained by the mixed box - abi::emit_decref_if_refcounted(emitter, &result_ty); - abi::emit_pop_reg(emitter, "rax"); // restore the boxed iterator_to_array() result - emitter.instruction("add rsp, 16"); // discard the saved original iterator_to_array() result pointer - } - } -} - -/// Emits assembly for to array loaded traversable object. -fn emit_to_array_loaded_traversable_object( - preserve_keys: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let receiver_reg = abi::nested_call_reg(emitter); - emitter.instruction(&format!( // preserve Traversable receiver while allocating iterator_to_array()'s result - "mov {}, {}", - receiver_reg, - abi::int_result_reg(emitter) - )); - if preserve_keys { - iterator_common::emit_new_mixed_hash(emitter); - } else { - iterator_common::emit_new_mixed_indexed_array(emitter); - } - iterator_common::emit_save_result_under_receiver(emitter); - iterator_common::emit_restore_receiver_from_preserved_reg(emitter, receiver_reg); - - emit_iterable_object_loop( - "iterator_to_array_traversable", - emitter, - ctx, - data, - |_, _, _, _| (), - |dispatch_target, _, emitter, ctx, data| { - if preserve_keys { - iterator_common::emit_insert_current_with_iterator_key( - dispatch_target, - emitter, - ctx, - data, - ); - } else { - iterator_common::emit_append_current_to_saved_array( - dispatch_target, - emitter, - ctx, - ); - } - }, - |_, _, _, _| {}, - ); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed iterator_to_array() result container -} - -/// Emits assembly for to array loaded iterable. -fn emit_to_array_loaded_iterable( - preserve_keys: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let indexed_case = ctx.next_label("iterator_to_array_iterable_indexed"); - let hash_case = ctx.next_label("iterator_to_array_iterable_hash"); - let object_case = ctx.next_label("iterator_to_array_iterable_object"); - let done = ctx.next_label("iterator_to_array_iterable_done"); - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterable pointer across heap-kind probing - abi::emit_call_label(emitter, "__rt_heap_kind"); // classify the type-erased iterable payload - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #2"); // is the iterable an indexed array? - emitter.instruction(&format!("b.eq {}", indexed_case)); // convert or clone the indexed-array payload - emitter.instruction("cmp x0, #3"); // is the iterable an associative hash? - emitter.instruction(&format!("b.eq {}", hash_case)); // convert or clone the hash payload - emitter.instruction("cmp x0, #4"); // is the iterable an object? - emitter.instruction(&format!("b.eq {}", object_case)); // collect a Traversable object through Iterator dispatch - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 2"); // is the iterable an indexed array? - emitter.instruction(&format!("je {}", indexed_case)); // convert or clone the indexed-array payload - emitter.instruction("cmp rax, 3"); // is the iterable an associative hash? - emitter.instruction(&format!("je {}", hash_case)); // convert or clone the hash payload - emitter.instruction("cmp rax, 4"); // is the iterable an object? - emitter.instruction(&format!("je {}", object_case)); // collect a Traversable object through Iterator dispatch - } - } - abi::emit_call_label(emitter, "__rt_iterable_unsupported_kind"); // unsupported iterable payloads abort with a fatal diagnostic - - emitter.label(&object_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the object pointer before Traversable collection - emit_to_array_loaded_traversable_object(preserve_keys, emitter, ctx, data); - abi::emit_jump(emitter, &done); // skip array payload paths after object traversal - - emitter.label(&hash_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the hash pointer before result materialization - if preserve_keys { - iterator_common::emit_clone_loaded_runtime_hash_as_mixed(emitter); - } else { - let hash_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; - let _ = crate::codegen::builtins::arrays::array_values::emit_loaded_values( - &hash_ty, - emitter, - ctx, - data, - ); - } - abi::emit_jump(emitter, &done); // skip indexed-array payload handling after hash materialization - - emitter.label(&indexed_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the indexed-array pointer before result materialization - iterator_common::emit_clone_loaded_runtime_indexed_array_as_mixed(emitter); - - emitter.label(&done); -} diff --git a/src/codegen/builtins/spl/mod.rs b/src/codegen/builtins/spl/mod.rs deleted file mode 100644 index a84e0bdc85..0000000000 --- a/src/codegen/builtins/spl/mod.rs +++ /dev/null @@ -1,358 +0,0 @@ -//! Purpose: -//! Emits SPL autoload and object-introspection builtins. -//! Provides runtime stubs for AOT-resolved autoload behavior plus simple object ids/hashes. -//! -//! Called from: -//! - `crate::codegen::builtins::emit_builtin_call()` -//! -//! Key details: -//! - Conforming autoload registrations are consumed before codegen; remaining calls keep PHP-visible defaults. -//! - `spl_classes()` is a static snapshot of compiler-shipped SPL/core class-like names. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -mod iterator_common; -mod iterator_apply; -mod iterator_count; -mod iterator_to_array; - -const EXTS_PTR_SYMBOL: &str = "_spl_autoload_exts_ptr"; -const EXTS_LEN_SYMBOL: &str = "_spl_autoload_exts_len"; - -/// Dispatches to the appropriate SPL builtin emitter by name. -/// Returns `Some(PhpType)` if `name` matches a known SPL builtin, -/// or `None` if the builtin is not handled here. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "spl_autoload_register" | "spl_autoload_unregister" => { - Some(emit_const_bool(name, args, true, emitter, ctx, data)) - } - "spl_autoload_functions" => Some(emit_functions_array(name, args, emitter, ctx, data)), - "spl_autoload_extensions" => Some(emit_extensions(name, args, emitter, ctx, data)), - "spl_autoload_call" | "spl_autoload" => Some(emit_void(name, args, emitter, ctx, data)), - "spl_object_id" => Some(emit_object_id(args, emitter, ctx, data)), - "spl_object_hash" => Some(emit_object_hash(args, emitter, ctx, data)), - "spl_classes" => Some(emit_classes(emitter, data)), - "iterator_apply" => iterator_apply::emit(name, args, emitter, ctx, data), - "iterator_count" => iterator_count::emit(name, args, emitter, ctx, data), - "iterator_to_array" => iterator_to_array::emit(name, args, emitter, ctx, data), - _ => None, - } -} - -/// Returns the object's heap pointer as an integer. -/// -/// Unique per object and stable per process. Matches PHP's contract for -/// `spl_object_id` (PHP's IDs start at 1 and increment; ours are pointer-sized; -/// both satisfy "two distinct objects → distinct ids" within a process). -fn emit_object_id( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("spl_object_id() — return heap pointer as int"); - emit_expr(&args[0], emitter, ctx, data); - PhpType::Int -} - -/// Returns the object's heap pointer formatted as a string. -/// -/// PHP returns a 32-character hex string; we return the pointer as a decimal -/// string via `__rt_itoa`. Both forms are unique-per-object and stable -/// per-process — only the textual format differs. -fn emit_object_hash( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("spl_object_hash() — pointer formatted as decimal string"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_itoa"); // convert the heap pointer integer into the standard decimal string output - PhpType::Str -} - -/// Materialises the SPL class/interface registry as an indexed string array. -/// -/// Names mirror the compiler-shipped SPL/core interfaces, Throwable types, -/// SPL exceptions, Phase 4 containers, and Phase 5 iterator foundations. -/// The array stores (pointer, length) pairs per entry. -fn emit_classes(emitter: &mut Emitter, data: &mut DataSection) -> PhpType { - let names = SPL_CLASS_NAMES; - emitter.comment("spl_classes() — AOT snapshot of shipped SPL types"); - let cap = names.len().max(1); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", cap)); // request capacity for one entry per shipped SPL type - emitter.instruction("mov x1, #16"); // request 16-byte string slots so the array stores ptr+len pairs - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, {}", cap)); // request capacity for one entry per shipped SPL type - emitter.instruction("mov rsi, 16"); // request 16-byte string slots so the array stores ptr+len pairs - } - } - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the SPL-classes registry view through the shared array constructor - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // park the array pointer between push calls - for n in names { - let (label, len) = data.add_string(n.as_bytes()); - emitter.instruction("ldr x0, [sp]"); // reload the array pointer for this push call - abi::emit_symbol_address(emitter, "x1", &label); // load the address of this SPL type's name - emitter.instruction(&format!("mov x2, #{}", len)); // load the length of this SPL type's name - emitter.instruction("bl __rt_array_push_str"); // append the name; may grow the storage - emitter.instruction("str x0, [sp]"); // refresh the saved array pointer if the storage grew - } - emitter.instruction("ldr x0, [sp], #16"); // restore the final array pointer as the builtin result - } - Arch::X86_64 => { - emitter.instruction("push rax"); // park the array pointer between push calls - emitter.instruction("sub rsp, 8"); // keep stack 16-byte aligned for the call sequence - for n in names { - let (label, len) = data.add_string(n.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the array pointer for this push call - abi::emit_symbol_address(emitter, "rsi", &label); // load the address of this SPL type's name - emitter.instruction(&format!("mov rdx, {}", len)); // load the length of this SPL type's name - emitter.instruction("call __rt_array_push_str"); // append the name; may grow the storage - emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // refresh the saved array pointer if the storage grew - } - emitter.instruction("add rsp, 8"); // pop the alignment padding - emitter.instruction("pop rax"); // restore the final array pointer as the builtin result - } - } - PhpType::Array(Box::new(PhpType::Str)) -} - -/// The static set of SPL/core type names shipped today. Stays in sync -/// with `inject_builtin_interfaces`, `inject_builtin_spl_exceptions`, and -/// `inject_builtin_spl_classes`. -const 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", -]; - -/// Evaluate all arguments for their side effects, discarding results. -fn emit_args_for_side_effects( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - for arg in args { - emit_expr(arg, emitter, ctx, data); - } -} - -/// Stub a builtin that evaluates args for side effects and returns a boolean constant. -fn emit_const_bool( - name: &str, - args: &[Expr], - value: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("{}() — AOT stub", name)); - emit_args_for_side_effects(args, emitter, ctx, data); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), value as i64); // signal success: register/unregister always reports the call as accepted - PhpType::Bool -} - -/// Stub a builtin that evaluates args for side effects and returns void. -fn emit_void( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("{}() — AOT stub", name)); - emit_args_for_side_effects(args, emitter, ctx, data); - PhpType::Void -} - -/// Emit `spl_autoload_functions()` — returns AOT registry as int-array view. -fn emit_functions_array( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let rule_count = crate::codegen::autoload_rule_count(); - emitter.comment(&format!( - "{}() — AOT registry view ({} rule{})", - name, - rule_count, - if rule_count == 1 { "" } else { "s" } - )); - emit_args_for_side_effects(args, emitter, ctx, data); - let cap = rule_count.max(1); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", cap)); // request enough capacity to hold one entry per registered autoload rule - emitter.instruction("mov x1, #8"); // request 8-byte int slots — the introspection array stores rule indexes - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, {}", cap)); // request enough capacity to hold one entry per registered autoload rule - emitter.instruction("mov rsi, 8"); // request 8-byte int slots — the introspection array stores rule indexes - } - } - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the indexed registry view through the shared array constructor - - if rule_count > 0 { - emit_functions_array_fill(rule_count, emitter); - } - - PhpType::Array(Box::new(PhpType::Int)) -} - -/// After `__rt_array_new` returns the empty array in `x0`/`rax`, push -/// `rule_count` integer placeholders (rule indexes 0..N-1) so `count()` -/// and `foreach` see one entry per registered rule. -fn emit_functions_array_fill(rule_count: usize, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // park the indexed-array pointer while we push placeholder entries - for i in 0..rule_count { - emitter.instruction("ldr x0, [sp]"); // reload the array pointer for each push call - emitter.instruction(&format!("mov x1, #{}", i)); // load the rule-index placeholder for this slot - emitter.instruction("bl __rt_array_push_int"); // append the placeholder index, may grow the storage - emitter.instruction("str x0, [sp]"); // refresh the saved array pointer in case __rt_array_push_int grew it - } - emitter.instruction("ldr x0, [sp], #16"); // restore the final array pointer as the builtin result - } - Arch::X86_64 => { - emitter.instruction("push rax"); // park the indexed-array pointer while we push placeholder entries - emitter.instruction("sub rsp, 8"); // keep the stack 16-byte aligned for the call sequence - for i in 0..rule_count { - emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the array pointer for each push call - emitter.instruction(&format!("mov rsi, {}", i)); // load the rule-index placeholder for this slot - emitter.instruction("call __rt_array_push_int"); // append the placeholder index, may grow the storage - emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // refresh the saved array pointer in case __rt_array_push_int grew it - } - emitter.instruction("add rsp, 8"); // pop the alignment padding before restoring the array pointer - emitter.instruction("pop rax"); // restore the final array pointer as the builtin result - } - } -} - -/// Read or read+write the runtime-mutable `_spl_autoload_exts_*` globals. -/// -/// Read (no arg, or arg is the `null` literal): load (ptr, len) into the -/// string result registers. -/// -/// Write (string-typed arg): evaluate the new value, save it, load the -/// previous (ptr, len) into the result registers, and overwrite the -/// globals with the new value. Returns the previous value as PHP does. -fn emit_extensions( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let writes_new = args - .first() - .is_some_and(|arg| !matches!(arg.kind, ExprKind::Null)); - - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - - if writes_new { - emitter.comment(&format!("{}() — store new extensions, return previous", name)); - let arg = &args[0]; - emit_expr(arg, emitter, ctx, data); - // -- save the new (ptr, len) we just evaluated -- - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); - // -- load previous (ptr, len) into the string result regs -- - abi::emit_load_symbol_to_reg(emitter, ptr_reg, EXTS_PTR_SYMBOL, 0); - abi::emit_load_symbol_to_reg(emitter, len_reg, EXTS_LEN_SYMBOL, 0); - // -- pop the saved new value into scratch regs and write to globals -- - let new_ptr = abi::secondary_scratch_reg(emitter); - let new_len = abi::tertiary_scratch_reg(emitter); - abi::emit_pop_reg_pair(emitter, new_ptr, new_len); - abi::emit_store_reg_to_symbol(emitter, new_ptr, EXTS_PTR_SYMBOL, 0); - abi::emit_store_reg_to_symbol(emitter, new_len, EXTS_LEN_SYMBOL, 0); - } else { - emitter.comment(&format!("{}() — read current extensions", name)); - // -- evaluate any null arg for parity (no observable effect) -- - emit_args_for_side_effects(args, emitter, ctx, data); - abi::emit_load_symbol_to_reg(emitter, ptr_reg, EXTS_PTR_SYMBOL, 0); - abi::emit_load_symbol_to_reg(emitter, len_reg, EXTS_LEN_SYMBOL, 0); - } - - PhpType::Str -} diff --git a/src/codegen/builtins/strings/addslashes.rs b/src/codegen/builtins/strings/addslashes.rs deleted file mode 100644 index 52d2fa36fb..0000000000 --- a/src/codegen/builtins/strings/addslashes.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Purpose: -//! Emits PHP `addslashes` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code to escape single quotes, double quotes, backslashes, and NUL bytes -/// in the first argument using the `__rt_addslashes` runtime helper. -/// -/// # Arguments -/// - `args[0]` is evaluated and passed as the string to escape. -/// - Calls `__rt_addslashes` through the active target ABI. -/// -/// # Returns -/// `PhpType::Str` — the escaped string as an owned runtime value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("addslashes()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_addslashes"); // escape quotes and backslashes through the active target ABI - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/args.rs b/src/codegen/builtins/strings/args.rs deleted file mode 100644 index bd25feabc2..0000000000 --- a/src/codegen/builtins/strings/args.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Purpose: -//! Provides shared argument materialization helpers for string builtin emitters. -//! Normalizes PHP string operands into the pointer/length register convention used by runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::*::emit()`. -//! -//! Key details: -//! - Helpers must preserve temporary ownership while leaving string results in the ABI registers expected by callers. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_string, emit_expr}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Evaluates `arg`, coerces it to a string operand, and leaves the result in the -/// pointer/length register pair expected by string builtin callers. -/// Emits `__rt_mixed_cast_string` for non-string types; preserves temporary ownership. -pub(super) fn emit_string_arg( - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let ty = emit_expr(arg, emitter, ctx, data); - coerce_to_string(emitter, ctx, data, &ty); -} - -/// Evaluates `arg`, coerces it to `PhpType::Int` if needed, and pushes the result -/// onto the argument stack in ABI order. Returns the resolved type. -pub(super) fn push_int_arg( - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - crate::codegen::expr::calls::args::push_expr_arg( - arg, - Some(&PhpType::Int), - emitter, - ctx, - data, - ) -} - -/// Evaluates `arg`, pushes it as `PhpType::Int`, then pops the result into the -/// designated integer result register (e.g., `x0` on ARM64, `rax` on x86_64). -/// Returns the resolved type. -pub(super) fn emit_int_arg( - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let ty = push_int_arg(arg, emitter, ctx, data); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - ty -} - -/// Evaluates `arg`, coerces it to `PhpType::Float` if needed, and pushes the result -/// onto the argument stack in ABI order. Returns the resolved type. -pub(super) fn push_float_arg( - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - crate::codegen::expr::calls::args::push_expr_arg( - arg, - Some(&PhpType::Float), - emitter, - ctx, - data, - ) -} - -#[cfg(test)] -mod tests { - use crate::codegen::context::Context; - use crate::codegen::data_section::DataSection; - use crate::codegen::emit::Emitter; - use crate::codegen::platform::{Arch, Platform, Target}; - use crate::parser::ast::{Expr, ExprKind}; - use crate::span::Span; - use crate::types::PhpType; - - use super::*; - - /// Verifies emit string arg coerces mixed on x86_64. - #[test] - fn test_emit_string_arg_coerces_mixed_on_x86_64() { - let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); - let mut ctx = Context::new(); - let mut data = DataSection::new(); - ctx.alloc_var("value", PhpType::Mixed); - let expr = Expr { - kind: ExprKind::Variable("value".to_string()), - span: Span::dummy(), - }; - - emit_string_arg(&expr, &mut emitter, &mut ctx, &mut data); - - let asm = emitter.output(); - assert!(asm.contains("mov rax, QWORD PTR [rbp - 8]\n")); - assert!(asm.contains("call __rt_mixed_cast_string\n")); - } -} diff --git a/src/codegen/builtins/strings/base64_decode.rs b/src/codegen/builtins/strings/base64_decode.rs deleted file mode 100644 index e14dfad64a..0000000000 --- a/src/codegen/builtins/strings/base64_decode.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Purpose: -//! Emits PHP `base64_decode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `base64_decode` builtin call. -/// -/// Evaluates the first argument (the base64-encoded string) and calls the runtime -/// helper `__rt_base64_decode` to decode it. Returns `PhpType::Str` on success. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("base64_decode()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_base64_decode"); // decode the current base64 string through the target-aware runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/base64_encode.rs b/src/codegen/builtins/strings/base64_encode.rs deleted file mode 100644 index 5c56bf7213..0000000000 --- a/src/codegen/builtins/strings/base64_encode.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `base64_encode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `base64_encode($string)` call, which base64-encodes a string or scalar value. -/// -/// ## Arguments -/// - `args[0]`: the expression producing the string or scalar to encode -/// -/// ## Behavior -/// - Evaluates `args[0]` and leaves its result in the appropriate registers per ABI (x1/x2 on ARM64). -/// - Calls the target-aware runtime helper `__rt_base64_encode`. -/// - Returns `PhpType::Str`; the runtime helper allocates and owns the returned PHP string. -/// -/// ## Ownership -/// - The returned string is an owned runtime value; callers must treat it as allocated heap memory. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("base64_encode()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_base64_encode"); // encode the current string result through the target-aware base64 runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/bin2hex.rs b/src/codegen/builtins/strings/bin2hex.rs deleted file mode 100644 index 8a664c8fa9..0000000000 --- a/src/codegen/builtins/strings/bin2hex.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Purpose: -//! Emits PHP `bin2hex` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `bin2hex` builtin call. -/// -/// Evaluates `args[0]` as a string expression, then calls the `__rt_bin2hex` -/// runtime helper which allocates and returns a new hexadecimal string. -/// The original string is consumed by the helper; the result is always `PhpType::Str`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("bin2hex()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_bin2hex"); // convert the current string result to hexadecimal through the target-aware runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/chr.rs b/src/codegen/builtins/strings/chr.rs deleted file mode 100644 index cd5bb0dc14..0000000000 --- a/src/codegen/builtins/strings/chr.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `chr` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP's `chr(int $codepoint): string` function. -/// -/// Converts an integer ASCII/code-point value to a single-character string. -/// The argument is evaluated and loaded into a register via `emit_int_arg`, -/// then the target-aware runtime helper `__rt_chr` is called, which writes -/// one byte into concat storage and returns it as a PHP string. -/// -/// # Arguments -/// * `args[0]` — integer expression producing the character code -/// -/// # Returns -/// `PhpType::Str` — a single-character PHP string -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("chr()"); - super::args::emit_int_arg(&args[0], emitter, ctx, data); - // -- convert ASCII code to single-character string -- - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the integer character code into the first SysV runtime argument register before materializing the one-byte string - } - abi::emit_call_label(emitter, "__rt_chr"); // call the target-aware runtime helper that writes one byte into concat storage and returns it as a string - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/crc32.rs b/src/codegen/builtins/strings/crc32.rs deleted file mode 100644 index 6296842cdb..0000000000 --- a/src/codegen/builtins/strings/crc32.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Purpose: -//! Emits PHP `crc32()` calls: marshals the single string argument into the -//! `__rt_crc32` runtime helper, which returns the CRC-32 checksum as an int. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - `crc32($string): int` — the argument is evaluated (leaving the string -//! ptr/len in the runtime string registers) and the helper returns the -//! non-negative 32-bit checksum in the integer result register. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `crc32()` builtin, returning `PhpType::Int`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("crc32()"); - emit_expr(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_crc32"); // compute the CRC-32 of the input string → non-negative int in the result register - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/strings/ctype_alnum.rs b/src/codegen/builtins/strings/ctype_alnum.rs deleted file mode 100644 index 94bfd730ee..0000000000 --- a/src/codegen/builtins/strings/ctype_alnum.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Purpose: -//! Emits PHP `ctype_alnum` character-class predicate calls. -//! Loads string bytes for runtime classification while returning PHP boolean results. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - PHP ctype semantics operate on byte strings and empty-string behavior must match the checker/runtime contract. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ctype_alnum` builtin call. -/// -/// Loads the string argument (pointer in `x1`/`rax`, length in `x2`/`rdx` after -/// `emit_string_arg`), then scans every byte checking whether it falls into one of -/// the three ASCII alnum ranges: `A-Z`, `a-z`, or `0-9`. Returns `true` in -/// `x0`/`rax` when all bytes pass; `false` when the string is empty or any byte -/// fails the predicate. -/// -/// # Arguments -/// * `_name` – unused, matching the builtin emitter signature -/// * `args` – must contain exactly one expression evaluating to a PHP string -/// * `emitter` – target-specific instruction emission -/// * `ctx` – label name generation (`next_label`) -/// * `data` – data section for relocations (unused here) -/// -/// # Returns -/// `Some(PhpType::Bool)` to indicate the result type. -/// -/// # Assembly behavior -/// - AArch64: pointer in `x1`, length in `x2`; result in `x0`. -/// - x86_64: pointer in `rax`, length in `rdx`; result in `rax`. -/// - Empty strings jump directly to the `fail_label`, returning `false`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ctype_alnum()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - let loop_label = ctx.next_label("ctype_loop"); - let next_label = ctx.next_label("ctype_next"); - let fail_label = ctx.next_label("ctype_fail"); - let pass_label = ctx.next_label("ctype_pass"); - let end_label = ctx.next_label("ctype_end"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x2, {}", fail_label)); // empty strings fail the ctype_alnum() contract - emitter.instruction("mov x3, #0"); // x3 = current byte index inside the checked string - emitter.label(&loop_label); - emitter.instruction("cmp x3, x2"); // check whether the loop index reached the string length - emitter.instruction(&format!("b.ge {}", pass_label)); // all bytes matched the alnum predicate, so the string passes - emitter.instruction("ldrb w4, [x1, x3]"); // load the current byte from the string payload - emitter.instruction("sub w5, w4, #65"); // normalize the byte against 'A' for the upper-case range check - emitter.instruction("cmp w5, #25"); // test whether the normalized byte falls inside A-Z - emitter.instruction(&format!("b.ls {}", next_label)); // accept upper-case ASCII letters and advance to the next byte - emitter.instruction("sub w5, w4, #97"); // normalize the byte against 'a' for the lower-case range check - emitter.instruction("cmp w5, #25"); // test whether the normalized byte falls inside a-z - emitter.instruction(&format!("b.ls {}", next_label)); // accept lower-case ASCII letters and advance to the next byte - emitter.instruction("sub w5, w4, #48"); // normalize the byte against '0' for the decimal-digit range check - emitter.instruction("cmp w5, #9"); // test whether the normalized byte falls inside 0-9 - emitter.instruction(&format!("b.hi {}", fail_label)); // fail immediately when the byte is outside every ASCII alnum range - emitter.label(&next_label); - emitter.instruction("add x3, x3, #1"); // advance the byte index after accepting the current alphanumeric character - emitter.instruction(&format!("b {}", loop_label)); // continue scanning until a byte fails or the string ends - emitter.label(&fail_label); - emitter.instruction("mov x0, #0"); // return false once an empty string or non-alnum byte is observed - emitter.instruction(&format!("b {}", end_label)); // skip the success materialization after setting the false result - emitter.label(&pass_label); - emitter.instruction("mov x0, #1"); // return true once every byte in the string satisfied the alnum predicate - } - Arch::X86_64 => { - emitter.instruction("test rdx, rdx"); // empty strings fail the ctype_alnum() contract - emitter.instruction(&format!("je {}", fail_label)); // jump to the false result when the checked string is empty - emitter.instruction("xor rcx, rcx"); // rcx = current byte index inside the checked string - emitter.label(&loop_label); - emitter.instruction("cmp rcx, rdx"); // check whether the loop index reached the string length - emitter.instruction(&format!("jge {}", pass_label)); // all bytes matched the alnum predicate, so the string passes - emitter.instruction("movzx r8d, BYTE PTR [rax + rcx]"); // load the current byte from the string payload into a zero-extended scratch register - emitter.instruction("mov r9d, r8d"); // copy the current byte so the upper-case range check can normalize it in place - emitter.instruction("sub r9d, 65"); // normalize the byte against 'A' for the upper-case range check - emitter.instruction("cmp r9d, 25"); // test whether the normalized byte falls inside A-Z - emitter.instruction(&format!("jbe {}", next_label)); // accept upper-case ASCII letters and advance to the next byte - emitter.instruction("mov r9d, r8d"); // restore the current byte before attempting the lower-case range check - emitter.instruction("sub r9d, 97"); // normalize the byte against 'a' for the lower-case range check - emitter.instruction("cmp r9d, 25"); // test whether the normalized byte falls inside a-z - emitter.instruction(&format!("jbe {}", next_label)); // accept lower-case ASCII letters and advance to the next byte - emitter.instruction("sub r8d, 48"); // normalize the current byte against '0' for the decimal-digit range check - emitter.instruction("cmp r8d, 9"); // test whether the normalized byte falls inside 0-9 - emitter.instruction(&format!("ja {}", fail_label)); // fail immediately when the byte is outside every ASCII alnum range - emitter.label(&next_label); - emitter.instruction("add rcx, 1"); // advance the byte index after accepting the current alphanumeric character - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning until a byte fails or the string ends - emitter.label(&fail_label); - emitter.instruction("mov rax, 0"); // return false once an empty string or non-alnum byte is observed - emitter.instruction(&format!("jmp {}", end_label)); // skip the success materialization after setting the false result - emitter.label(&pass_label); - emitter.instruction("mov rax, 1"); // return true once every byte in the string satisfied the alnum predicate - } - } - emitter.label(&end_label); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/strings/ctype_alpha.rs b/src/codegen/builtins/strings/ctype_alpha.rs deleted file mode 100644 index 8491b712fe..0000000000 --- a/src/codegen/builtins/strings/ctype_alpha.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Purpose: -//! Emits PHP `ctype_alpha` character-class predicate calls. -//! Loads string bytes for runtime classification while returning PHP boolean results. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - PHP ctype semantics operate on byte strings and empty-string behavior must match the checker/runtime contract. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `ctype_alpha` builtin. -/// -/// `ctype_alpha($string)` returns `true` if every byte in `$string` is an -/// ASCII letter (A-Z or a-z), and `false` otherwise. Empty strings return -/// `false` per the PHP/Checker runtime contract. -/// -/// # Arguments -/// * `_name` - Unused builtin name (dispatch already performed). -/// * `args` - Must contain exactly one expression producing a string in the -/// platform string registers (x1/x2 on AArch64, rax/rdx on X86_64). -/// * `emitter` - Assembly emitter for the target architecture. -/// * `ctx` - Codegen context providing label generation and metadata. -/// * `data` - Data section for embedded literals if needed. -/// -/// # Returns -/// Always returns `Some(PhpType::Bool)`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ctype_alpha()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - let loop_label = ctx.next_label("ctype_loop"); - let next_label = ctx.next_label("ctype_next"); - let fail_label = ctx.next_label("ctype_fail"); - let pass_label = ctx.next_label("ctype_pass"); - let end_label = ctx.next_label("ctype_end"); - match emitter.target.arch { - Arch::AArch64 => { - // -- return false for empty string -- - emitter.instruction(&format!("cbz x2, {}", fail_label)); // empty strings fail the ctype_alpha() contract - emitter.instruction("mov x3, #0"); // x3 = current byte index inside the checked string - emitter.label(&loop_label); - emitter.instruction("cmp x3, x2"); // check whether the loop index reached the string length - emitter.instruction(&format!("b.ge {}", pass_label)); // all bytes matched the alpha predicate, so the string passes - emitter.instruction("ldrb w4, [x1, x3]"); // load the current byte from the string payload - emitter.instruction("sub w5, w4, #65"); // normalize the byte against 'A' for the upper-case range check - emitter.instruction("cmp w5, #25"); // test whether the normalized byte falls inside A-Z - emitter.instruction(&format!("b.ls {}", next_label)); // accept upper-case ASCII letters and advance to the next byte - emitter.instruction("sub w5, w4, #97"); // normalize the byte against 'a' for the lower-case range check - emitter.instruction("cmp w5, #25"); // test whether the normalized byte falls inside a-z - emitter.instruction(&format!("b.hi {}", fail_label)); // fail immediately when the byte is outside both ASCII alpha ranges - emitter.label(&next_label); - emitter.instruction("add x3, x3, #1"); // advance the byte index after accepting the current alpha character - emitter.instruction(&format!("b {}", loop_label)); // continue scanning until a byte fails or the string ends - emitter.label(&fail_label); - emitter.instruction("mov x0, #0"); // return false once an empty string or non-alpha byte is observed - emitter.instruction(&format!("b {}", end_label)); // skip the success materialization after setting the false result - emitter.label(&pass_label); - emitter.instruction("mov x0, #1"); // return true once every byte in the string satisfied the alpha predicate - } - Arch::X86_64 => { - // -- return false for empty string -- - emitter.instruction("test rdx, rdx"); // empty strings fail the ctype_alpha() contract - emitter.instruction(&format!("je {}", fail_label)); // jump to the false result when the checked string is empty - emitter.instruction("xor rcx, rcx"); // rcx = current byte index inside the checked string - emitter.label(&loop_label); - emitter.instruction("cmp rcx, rdx"); // check whether the loop index reached the string length - emitter.instruction(&format!("jge {}", pass_label)); // all bytes matched the alpha predicate, so the string passes - emitter.instruction("movzx r8d, BYTE PTR [rax + rcx]"); // load the current byte from the string payload into a zero-extended scratch register - emitter.instruction("mov r9d, r8d"); // copy the current byte so the upper-case range check can normalize it in place - emitter.instruction("sub r9d, 65"); // normalize the byte against 'A' for the upper-case range check - emitter.instruction("cmp r9d, 25"); // test whether the normalized byte falls inside A-Z - emitter.instruction(&format!("jbe {}", next_label)); // accept upper-case ASCII letters and advance to the next byte - emitter.instruction("mov r9d, r8d"); // restore the current byte before attempting the lower-case range check - emitter.instruction("sub r9d, 97"); // normalize the byte against 'a' for the lower-case range check - emitter.instruction("cmp r9d, 25"); // test whether the normalized byte falls inside a-z - emitter.instruction(&format!("ja {}", fail_label)); // fail immediately when the byte is outside both ASCII alpha ranges - emitter.label(&next_label); - emitter.instruction("add rcx, 1"); // advance the byte index after accepting the current alpha character - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning until a byte fails or the string ends - emitter.label(&fail_label); - emitter.instruction("mov rax, 0"); // return false once an empty string or non-alpha byte is observed - emitter.instruction(&format!("jmp {}", end_label)); // skip the success materialization after setting the false result - emitter.label(&pass_label); - emitter.instruction("mov rax, 1"); // return true once every byte in the string satisfied the alpha predicate - } - } - emitter.label(&end_label); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/strings/ctype_digit.rs b/src/codegen/builtins/strings/ctype_digit.rs deleted file mode 100644 index 92e5dcfd73..0000000000 --- a/src/codegen/builtins/strings/ctype_digit.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Purpose: -//! Emits PHP `ctype_digit` character-class predicate calls. -//! Loads string bytes for runtime classification while returning PHP boolean results. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - PHP ctype semantics operate on byte strings and empty-string behavior must match the checker/runtime contract. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `ctype_digit()` builtin. -/// -/// Reads the string argument (x1/x2 on AArch64, rax/rdx on x86_64), iterates over each byte, -/// and returns PHP `true` if all bytes are decimal digits (`0`-`9`), otherwise `false`. -/// Empty strings return `false` per the PHP ctype contract. -/// -/// # Arguments -/// - `_name`: Unused name field (present for builtin dispatcher signature uniformity). -/// - `args`: Single argument expression to evaluate into a string. -/// - `emitter`: Target-aware instruction emission. -/// - `ctx`: Compiler context providing labels and architecture. -/// - `data`: Data section for any emitted constants. -/// -/// # Returns -/// `Some(PhpType::Bool)` — the PHP boolean result. -/// -/// # ABI Behavior (AArch64) -/// Input: x1 = string pointer, x2 = string length -/// Output: x0 = 0 (false) or 1 (true) -/// -/// # ABI Behavior (x86_64) -/// Input: rax = string pointer, rdx = string length -/// Output: rax = 0 (false) or 1 (true) -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ctype_digit()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - let loop_label = ctx.next_label("ctype_loop"); - let fail_label = ctx.next_label("ctype_fail"); - let pass_label = ctx.next_label("ctype_pass"); - let end_label = ctx.next_label("ctype_end"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x2, {}", fail_label)); // empty strings fail the ctype_digit() contract - emitter.instruction("mov x3, #0"); // x3 = current byte index inside the checked string - emitter.label(&loop_label); - emitter.instruction("cmp x3, x2"); // check whether the loop index reached the string length - emitter.instruction(&format!("b.ge {}", pass_label)); // all bytes matched the digit predicate, so the string passes - emitter.instruction("ldrb w4, [x1, x3]"); // load the current byte from the string payload - emitter.instruction("sub w5, w4, #48"); // normalize the byte against '0' for the decimal-digit range check - emitter.instruction("cmp w5, #9"); // test whether the normalized byte falls inside 0-9 - emitter.instruction(&format!("b.hi {}", fail_label)); // fail immediately when the byte is outside the decimal-digit range - emitter.instruction("add x3, x3, #1"); // advance the byte index after accepting the current digit - emitter.instruction(&format!("b {}", loop_label)); // continue scanning until a byte fails or the string ends - emitter.label(&fail_label); - emitter.instruction("mov x0, #0"); // return false once an empty string or non-digit byte is observed - emitter.instruction(&format!("b {}", end_label)); // skip the success materialization after setting the false result - emitter.label(&pass_label); - emitter.instruction("mov x0, #1"); // return true once every byte in the string satisfied the digit predicate - } - Arch::X86_64 => { - emitter.instruction("test rdx, rdx"); // empty strings fail the ctype_digit() contract - emitter.instruction(&format!("je {}", fail_label)); // jump to the false result when the checked string is empty - emitter.instruction("xor rcx, rcx"); // rcx = current byte index inside the checked string - emitter.label(&loop_label); - emitter.instruction("cmp rcx, rdx"); // check whether the loop index reached the string length - emitter.instruction(&format!("jge {}", pass_label)); // all bytes matched the digit predicate, so the string passes - emitter.instruction("movzx r8d, BYTE PTR [rax + rcx]"); // load the current byte from the string payload into a zero-extended scratch register - emitter.instruction("sub r8d, 48"); // normalize the byte against '0' for the decimal-digit range check - emitter.instruction("cmp r8d, 9"); // test whether the normalized byte falls inside 0-9 - emitter.instruction(&format!("ja {}", fail_label)); // fail immediately when the byte is outside the decimal-digit range - emitter.instruction("add rcx, 1"); // advance the byte index after accepting the current digit - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning until a byte fails or the string ends - emitter.label(&fail_label); - emitter.instruction("mov rax, 0"); // return false once an empty string or non-digit byte is observed - emitter.instruction(&format!("jmp {}", end_label)); // skip the success materialization after setting the false result - emitter.label(&pass_label); - emitter.instruction("mov rax, 1"); // return true once every byte in the string satisfied the digit predicate - } - } - emitter.label(&end_label); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/strings/ctype_space.rs b/src/codegen/builtins/strings/ctype_space.rs deleted file mode 100644 index 1a364e5682..0000000000 --- a/src/codegen/builtins/strings/ctype_space.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Purpose: -//! Emits PHP `ctype_space` character-class predicate calls. -//! Loads string bytes for runtime classification while returning PHP boolean results. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - PHP ctype semantics operate on byte strings and empty-string behavior must match the checker/runtime contract. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `ctype_space` builtin, which returns `true` iff every byte -/// in the argument string is a ASCII whitespace character (space, tab, newline, -/// carriage return, vertical tab, or form feed). -/// -/// # Arguments -/// - `_name`: Unused; present for dispatcher uniformity. -/// - `args`: Must contain exactly one expression producing a PHP string (pointer in -/// `x1`/`rax`, length in `x2`/`rdx` on ARM64/x86_64 respectively). -/// - `emitter`: Target-specific instruction emission. -/// - `ctx`: Label generation and codegen context. -/// - `data`: Data section for relocations. -/// -/// # Returns -/// `Some(PhpType::Bool)` always; `ctype_space` never returns `null`. -/// -/// # ABI Notes -/// - ARM64: string pointer in `x1`, length in `x2`, result in `x0`. -/// - x86_64: string pointer in `rax`, length in `rdx`, result in `rax`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ctype_space()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - let loop_label = ctx.next_label("ctype_loop"); - let next_label = ctx.next_label("ctype_next"); - let fail_label = ctx.next_label("ctype_fail"); - let pass_label = ctx.next_label("ctype_pass"); - let end_label = ctx.next_label("ctype_end"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x2, {}", fail_label)); // empty strings fail the ctype_space() contract - emitter.instruction("mov x3, #0"); // x3 = current byte index inside the checked string - emitter.label(&loop_label); - emitter.instruction("cmp x3, x2"); // check whether the loop index reached the string length - emitter.instruction(&format!("b.ge {}", pass_label)); // all bytes matched the whitespace predicate, so the string passes - emitter.instruction("ldrb w4, [x1, x3]"); // load the current byte from the string payload - emitter.instruction("cmp w4, #32"); // test whether the current byte is an ASCII space - emitter.instruction(&format!("b.eq {}", next_label)); // accept a space and advance to the next byte - emitter.instruction("cmp w4, #9"); // test whether the current byte is a tab - emitter.instruction(&format!("b.eq {}", next_label)); // accept a tab and advance to the next byte - emitter.instruction("cmp w4, #10"); // test whether the current byte is a newline - emitter.instruction(&format!("b.eq {}", next_label)); // accept a newline and advance to the next byte - emitter.instruction("cmp w4, #13"); // test whether the current byte is a carriage return - emitter.instruction(&format!("b.eq {}", next_label)); // accept a carriage return and advance to the next byte - emitter.instruction("cmp w4, #11"); // test whether the current byte is a vertical tab - emitter.instruction(&format!("b.eq {}", next_label)); // accept a vertical tab and advance to the next byte - emitter.instruction("cmp w4, #12"); // test whether the current byte is a form feed - emitter.instruction(&format!("b.ne {}", fail_label)); // fail immediately when the byte is outside the ASCII whitespace set - emitter.label(&next_label); - emitter.instruction("add x3, x3, #1"); // advance the byte index after accepting the current whitespace character - emitter.instruction(&format!("b {}", loop_label)); // continue scanning until a byte fails or the string ends - emitter.label(&fail_label); - emitter.instruction("mov x0, #0"); // return false once an empty string or non-whitespace byte is observed - emitter.instruction(&format!("b {}", end_label)); // skip the success materialization after setting the false result - emitter.label(&pass_label); - emitter.instruction("mov x0, #1"); // return true once every byte in the string satisfied the whitespace predicate - } - Arch::X86_64 => { - emitter.instruction("test rdx, rdx"); // empty strings fail the ctype_space() contract - emitter.instruction(&format!("je {}", fail_label)); // jump to the false result when the checked string is empty - emitter.instruction("xor rcx, rcx"); // rcx = current byte index inside the checked string - emitter.label(&loop_label); - emitter.instruction("cmp rcx, rdx"); // check whether the loop index reached the string length - emitter.instruction(&format!("jge {}", pass_label)); // all bytes matched the whitespace predicate, so the string passes - emitter.instruction("movzx r8d, BYTE PTR [rax + rcx]"); // load the current byte from the string payload into a zero-extended scratch register - emitter.instruction("cmp r8d, 32"); // test whether the current byte is an ASCII space - emitter.instruction(&format!("je {}", next_label)); // accept a space and advance to the next byte - emitter.instruction("cmp r8d, 9"); // test whether the current byte is a tab - emitter.instruction(&format!("je {}", next_label)); // accept a tab and advance to the next byte - emitter.instruction("cmp r8d, 10"); // test whether the current byte is a newline - emitter.instruction(&format!("je {}", next_label)); // accept a newline and advance to the next byte - emitter.instruction("cmp r8d, 13"); // test whether the current byte is a carriage return - emitter.instruction(&format!("je {}", next_label)); // accept a carriage return and advance to the next byte - emitter.instruction("cmp r8d, 11"); // test whether the current byte is a vertical tab - emitter.instruction(&format!("je {}", next_label)); // accept a vertical tab and advance to the next byte - emitter.instruction("cmp r8d, 12"); // test whether the current byte is a form feed - emitter.instruction(&format!("jne {}", fail_label)); // fail immediately when the byte is outside the ASCII whitespace set - emitter.label(&next_label); - emitter.instruction("add rcx, 1"); // advance the byte index after accepting the current whitespace character - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning until a byte fails or the string ends - emitter.label(&fail_label); - emitter.instruction("mov rax, 0"); // return false once an empty string or non-whitespace byte is observed - emitter.instruction(&format!("jmp {}", end_label)); // skip the success materialization after setting the false result - emitter.label(&pass_label); - emitter.instruction("mov rax, 1"); // return true once every byte in the string satisfied the whitespace predicate - } - } - emitter.label(&end_label); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/strings/explode.rs b/src/codegen/builtins/strings/explode.rs deleted file mode 100644 index 8216b4df03..0000000000 --- a/src/codegen/builtins/strings/explode.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Purpose: -//! Emits PHP `explode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `explode(delimiter, string)` builtin. -/// Saves the delimiter (pointer in x1, length in x2), evaluates the subject string into -/// argument registers, restores the delimiter, then calls `__rt_explode` to split the string. -/// Returns an array of strings (`PhpType::Array(Str)`). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("explode()"); - // explode($delimiter, $string) - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - // -- save delimiter, evaluate string -- - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the delimiter pointer and length while the subject-string expression is evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the subject-string pointer into the third AArch64 string-argument register - emitter.instruction("mov x4, x2"); // move the subject-string length into the fourth AArch64 string-argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the delimiter pointer and length after evaluating the subject string - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the delimiter pointer and length while the subject-string expression is evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the subject-string pointer into the third x86_64 string-argument register - emitter.instruction("mov rsi, rdx"); // move the subject-string length into the fourth x86_64 string-argument register - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the delimiter pointer and length after evaluating the subject string - } - } - abi::emit_call_label(emitter, "__rt_explode"); // split the subject string by the delimiter through the target-aware runtime helper - - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/strings/format_args.rs b/src/codegen/builtins/strings/format_args.rs deleted file mode 100644 index 2efb2a22ee..0000000000 --- a/src/codegen/builtins/strings/format_args.rs +++ /dev/null @@ -1,276 +0,0 @@ -//! Purpose: -//! Shared argument marshalling for the `sprintf`/`printf` family. Pushes each value argument -//! as a 16-byte tagged record for `__rt_sprintf`, coercing the argument to the type its -//! conversion specifier consumes when the format string is a compile-time literal. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::sprintf::emit()` -//! - `crate::codegen::builtins::strings::printf::emit()` -//! -//! Key details: -//! - `__rt_sprintf` dispatches on the format specifier character (`f`/`e`/`g`→float, `s`→string, -//! everything else→integer), NOT on the record tag. The int/float branches reinterpret the raw -//! record payload, so the pushed payload must already match the specifier's type. This module -//! parses literal formats to coerce each argument accordingly, fixing `Mixed`/cross-type args. -//! - The specifier scanner mirrors the runtime scanner exactly (flags → width → `.precision` → -//! one type char), so spec boundaries and argument counts always agree with the runtime; for -//! non-literal formats it falls back to the legacy push-by-static-type behavior. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_result_to_type, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// The value category a printf-family conversion specifier consumes, mirroring the -/// runtime's spec-character dispatch in `__rt_sprintf`. -#[derive(Clone, Copy, PartialEq, Eq)] -enum SpecCat { - /// `%d`, `%i`, `%u`, `%x`, `%X`, `%o`, `%c`, and any other char the runtime int-branches. - Int, - /// `%f`, `%e`, `%g`. - Float, - /// `%s`. - Str, -} - -/// Parses a literal format string into the ordered value categories its specifiers consume. -/// -/// The scan mirrors `__rt_sprintf` exactly: a `%` introduces a specifier unless followed by `%` -/// (a literal percent that consumes no argument); flags (`-`, `+`, `0`, space, `#`), width digits, -/// and an optional `.precision` run are skipped; the next byte is the type char. Classification -/// matches the runtime branch precisely — `f`/`e`/`g` are floats, `s` is a string, and every other -/// type char (including positional/length-modifier bytes the runtime mis-scans) is treated as the -/// runtime's default integer branch. Because the scanner agrees with the runtime byte-for-byte, the -/// returned categories align one-to-one with the arguments the runtime will read. -/// -/// A specifier left incomplete at end-of-string is dropped, matching the runtime bailing out before -/// consuming an argument for it. -fn parse_format_spec_cats(fmt: &str) -> Vec { - let b = fmt.as_bytes(); - let mut cats = Vec::new(); - let mut i = 0; - while i < b.len() { - if b[i] != b'%' { - i += 1; - continue; - } - i += 1; // consume '%' - if i >= b.len() { - break; // lone trailing '%': runtime bails without consuming an argument - } - if b[i] == b'%' { - i += 1; // "%%" is a literal percent, no argument consumed - continue; - } - // flags - while i < b.len() && matches!(b[i], b'-' | b'+' | b'0' | b' ' | b'#') { - i += 1; - } - // width digits - while i < b.len() && b[i].is_ascii_digit() { - i += 1; - } - // optional .precision - if i < b.len() && b[i] == b'.' { - i += 1; - while i < b.len() && b[i].is_ascii_digit() { - i += 1; - } - } - if i >= b.len() { - break; // specifier ran off the end before a type char: runtime bails - } - let cat = match b[i] { - b'f' | b'e' | b'g' => SpecCat::Float, - b's' => SpecCat::Str, - _ => SpecCat::Int, - }; - cats.push(cat); - i += 1; // consume the type char - } - cats -} - -/// Emits the full sprintf-style marshalling sequence shared by `sprintf` and `printf`. -/// -/// Each value argument is pushed as a 16-byte tagged record in reverse source order; when the -/// format is a literal, the argument is first coerced to the type its specifier consumes so the -/// runtime's spec-driven branch reads a correctly typed payload. The format string is then -/// evaluated and the argument count loaded before calling `__rt_sprintf`. On return the formatted -/// string occupies the standard string-result registers (`x1`/`x2` on ARM64, `rax`/`rdx` on x86_64) -/// and the runtime has popped the pushed records from the caller's stack. -pub(super) fn emit_format_and_call( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let arg_count = args.len() - 1; // exclude the format string - - // Determine per-argument target categories from a literal format. A non-literal format - // yields no categories, so every argument falls back to push-by-static-type. - let cats = match &args[0].kind { - ExprKind::StringLiteral(s) => parse_format_spec_cats(s), - _ => Vec::new(), - }; - - // -- evaluate and push arguments in reverse order -- - for i in (1..args.len()).rev() { - let ty = emit_expr(&args[i], emitter, ctx, data); - match cats.get(i - 1).copied() { - Some(cat) => push_coerced(emitter, ctx, data, &ty, cat), - None => push_static(emitter, &ty), - } - } - - // -- evaluate format string and pass the argument count -- - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", arg_count)); // number of format arguments - } - Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "rdi", arg_count as i64); // pass the number of packed variadic records in the first SysV integer argument register - } - } - abi::emit_call_label(emitter, "__rt_sprintf"); // format the string through the target-aware sprintf runtime helper - // runtime returns ptr+len and cleans up the caller's packed variadic records -} - -/// Coerces the just-evaluated argument (result type `ty`) to the type its conversion specifier -/// consumes, then pushes a tagged record whose payload shape matches the runtime's branch for that -/// specifier. A statically-`Str` argument under a float specifier has no clean pointer/length → -/// double coercion, so it falls back to the legacy static push (a rare, pre-existing edge; `Mixed` -/// string-ish values still convert correctly via `__rt_mixed_cast_float`). -fn push_coerced( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - ty: &PhpType, - cat: SpecCat, -) { - if cat == SpecCat::Float && *ty == PhpType::Str { - push_static(emitter, ty); - return; - } - match cat { - SpecCat::Int => { - if *ty == PhpType::Str { - // PHP string→int cast for a string arg under an integer specifier. Done locally - // (rather than widening the shared `coerce_result_to_type` Str→Int contract, which - // other call sites gate coercion on) so only this sprintf path is affected. - abi::emit_call_label(emitter, "__rt_str_to_int"); - } else { - coerce_result_to_type(emitter, ctx, data, ty, &PhpType::Int); - } - push_int(emitter); - } - SpecCat::Float => { - coerce_result_to_type(emitter, ctx, data, ty, &PhpType::Float); - push_float(emitter); - } - SpecCat::Str => { - coerce_result_to_type(emitter, ctx, data, ty, &PhpType::Str); - push_str(emitter); - } - } -} - -/// Pushes a tagged record for an argument using its static type without coercion, matching the -/// historical behavior used for non-literal formats and for arguments with no matching specifier. -fn push_static(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Int => push_int(emitter), - PhpType::Float => push_float(emitter), - PhpType::Bool => push_bool(emitter), - PhpType::Str => push_str(emitter), - _ => push_zero(emitter), - } -} - -/// Pushes the integer in the integer-result register as a tag-0 record (payload in the low qword). -fn push_int(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // push the integer value as the record payload - emitter.instruction("str xzr, [sp, #8]"); // type tag 0 = int - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged argument record - emitter.instruction("mov QWORD PTR [rsp], rax"); // store the integer payload in the low half of the record - emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // tag the record as an integer operand - } - } -} - -/// Pushes the float bits in the float-result register as a tag-2 record (bits in the low qword). -fn push_float(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmov x0, d0"); // move float bits into an integer register for the record payload - emitter.instruction("str x0, [sp, #-16]!"); // push the float bits as the record payload - emitter.instruction("mov x0, #2"); // type tag 2 = float - emitter.instruction("str x0, [sp, #8]"); // store the type tag - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged argument record - emitter.instruction("movsd QWORD PTR [rsp], xmm0"); // store the float bits in the low half of the record - emitter.instruction("mov QWORD PTR [rsp + 8], 2"); // tag the record as a floating operand - } - } -} - -/// Pushes the boolean in the integer-result register as a tag-3 record. -fn push_bool(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // push the boolean value (0 or 1) as the record payload - emitter.instruction("mov x0, #3"); // type tag 3 = bool - emitter.instruction("str x0, [sp, #8]"); // store the type tag - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged argument record - emitter.instruction("mov QWORD PTR [rsp], rax"); // store the boolean payload in the low half of the record - emitter.instruction("mov QWORD PTR [rsp + 8], 3"); // tag the record as a boolean operand - } - } -} - -/// Pushes the string in the string-result registers as a tag-1 record (pointer in the low qword, -/// `length << 8 | 1` in the high qword so the runtime str branch can recover the length). -fn push_str(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x1, [sp, #-16]!"); // push the string pointer as the record payload - emitter.instruction("lsl x0, x2, #8"); // shift the length left by 8 to make room for the tag bit - emitter.instruction("orr x0, x0, #1"); // set type tag bit 0 = str - emitter.instruction("str x0, [sp, #8]"); // store tag|length - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged argument record - emitter.instruction("mov QWORD PTR [rsp], rax"); // store the string pointer in the low half of the record - emitter.instruction("mov rcx, rdx"); // copy the length before packing it into the metadata word - emitter.instruction("shl rcx, 8"); // shift the length into the upper metadata bits - emitter.instruction("or rcx, 1"); // set type tag bit 0 = str while preserving the packed length - emitter.instruction("mov QWORD PTR [rsp + 8], rcx"); // store the packed string metadata word - } - } -} - -/// Pushes a zero-valued tag-0 record for argument types that have no printf payload representation. -fn push_zero(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str xzr, [sp, #-16]!"); // push a zero payload for an unsupported operand - emitter.instruction("str xzr, [sp, #8]"); // type tag 0 - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve one 16-byte tagged argument record - emitter.instruction("mov QWORD PTR [rsp], 0"); // store a zero payload for an unsupported operand - emitter.instruction("mov QWORD PTR [rsp + 8], 0"); // tag the unsupported operand as an integer zero fallback - } - } -} diff --git a/src/codegen/builtins/strings/grapheme_strrev.rs b/src/codegen/builtins/strings/grapheme_strrev.rs deleted file mode 100644 index 313bb0b1e1..0000000000 --- a/src/codegen/builtins/strings/grapheme_strrev.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Purpose: -//! Emits PHP `grapheme_strrev` calls and boxes the `string|false` result shape. -//! Keeps grapheme-aware reversal separate from byte-wise `strrev` lowering. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - The runtime helper returns a string pointer/length pair on success or a null pointer on UTF-8 segmentation failure. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `grapheme_strrev` builtin. -/// -/// The string argument is evaluated using the normal expression lowering. The -/// runtime helper reverses the input by UTF-8 grapheme clusters and returns a -/// raw string pair on success; this wrapper boxes that pair as `Mixed` so the -/// PHP `string|false` signature remains representable to callers. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("grapheme_strrev()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_grapheme_strrev"); // reverse the input string by grapheme clusters and return string or failure sentinel - box_grapheme_strrev_result(emitter, ctx); - - Some(PhpType::Mixed) -} - -/// Boxes the raw runtime result as PHP `string|false`. -/// -/// Success returns a non-null string pointer plus length, which is persisted and -/// boxed through `__rt_mixed_from_value`. Failure returns a null pointer and is -/// boxed as boolean false, preserving PHP's `string|false` observable surface. -fn box_grapheme_strrev_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("grapheme_strrev_false"); - let done_label = ctx.next_label("grapheme_strrev_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null string pointer means UTF-8 segmentation failed - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Str); - emitter.instruction(&format!("b {}", done_label)); // skip false boxing after a successful grapheme reversal - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 for grapheme_strrev() failure - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible string|false semantics - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null string pointer means UTF-8 segmentation failed - emitter.instruction(&format!("jz {}", false_label)); // box false when the runtime reports segmentation failure - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Str); - emitter.instruction(&format!("jmp {}", done_label)); // skip false boxing after a successful grapheme reversal - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 for grapheme_strrev() failure - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false for PHP-compatible string|false semantics - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/strings/gzcompress.rs b/src/codegen/builtins/strings/gzcompress.rs deleted file mode 100644 index c401776be4..0000000000 --- a/src/codegen/builtins/strings/gzcompress.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Purpose: -//! Emits PHP `gzcompress` calls. -//! Compresses a string with the system zlib (`compressBound` + `compress2`). -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - The zlib calls are emitted inline at the call site (not as a shared -//! runtime helper) so that only programs that actually use `gzcompress` -//! carry a dependency on `libz`. The required-library declaration in the -//! checker adds `-lz` to the link for those programs. -//! - The result is an owned heap string (heap kind 1) sized by `compressBound`. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::args::emit_string_arg; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `gzcompress()` string builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("gzcompress()"); - // The data argument may arrive as a boxed mixed value, so coerce it to a - // plain string before handing the pointer/length pair to zlib. - emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the data string - if args.len() >= 2 { - emit_expr(&args[1], emitter, ctx, data); - } else { - emitter.instruction("mov x0, #-1"); // default zlib compression level - } - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the data pointer/length - // -- inline zlib compress: x0 = level, x1/x2 = data -- - emitter.instruction("sub sp, sp, #64"); // scratch frame for the compression state - emitter.instruction("str x0, [sp, #0]"); // save the compression level - emitter.instruction("str x1, [sp, #8]"); // save the source pointer - emitter.instruction("str x2, [sp, #16]"); // save the source length - emitter.instruction("mov x0, x2"); // source length into the compressBound argument - emitter.bl_c("compressBound"); // x0 = worst-case compressed size - emitter.instruction("str x0, [sp, #24]"); // seed destLen with the buffer capacity - emitter.instruction("bl __rt_heap_alloc"); // allocate the compressed-data buffer - emitter.instruction("mov x9, #1"); // heap kind 1 = persisted elephc string - emitter.instruction("str x9, [x0, #-8]"); // stamp the buffer as an owned string - emitter.instruction("str x0, [sp, #32]"); // save the destination buffer pointer - emitter.instruction("add x1, sp, #24"); // &destLen in/out parameter - emitter.instruction("ldr x2, [sp, #8]"); // source pointer - emitter.instruction("ldr x3, [sp, #16]"); // source length - emitter.instruction("ldr x4, [sp, #0]"); // compression level - emitter.bl_c("compress2"); // zlib-compress the source into the buffer - emitter.instruction("ldr x1, [sp, #32]"); // compressed buffer becomes the result pointer - emitter.instruction("ldr x2, [sp, #24]"); // compress2 wrote the compressed length here - emitter.instruction("add sp, sp, #64"); // release the scratch frame - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the data string - if args.len() >= 2 { - emit_expr(&args[1], emitter, ctx, data); - } else { - emitter.instruction("mov eax, -1"); // default zlib compression level - } - emitter.instruction("mov rdi, rax"); // compression level into a scratch register - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore the data pointer/length - // -- inline zlib compress: rdi = level, rsi/rdx = data -- - emitter.instruction("sub rsp, 64"); // scratch frame for the compression state - emitter.instruction("mov QWORD PTR [rsp + 0], rdi"); // save the compression level - emitter.instruction("mov QWORD PTR [rsp + 8], rsi"); // save the source pointer - emitter.instruction("mov QWORD PTR [rsp + 16], rdx"); // save the source length - emitter.instruction("mov rdi, rdx"); // source length into the compressBound argument - emitter.instruction("call compressBound"); // rax = worst-case compressed size - emitter.instruction("mov QWORD PTR [rsp + 24], rax"); // seed destLen with the buffer capacity - emitter.instruction("call __rt_heap_alloc"); // allocate the compressed-data buffer - emitter.instruction(&format!( // owned-string heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 1 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the buffer as an owned string - emitter.instruction("mov QWORD PTR [rsp + 32], rax"); // save the destination buffer pointer - emitter.instruction("mov rdi, rax"); // destination buffer pointer - emitter.instruction("lea rsi, [rsp + 24]"); // &destLen in/out parameter - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // source pointer - emitter.instruction("mov rcx, QWORD PTR [rsp + 16]"); // source length - emitter.instruction("mov r8, QWORD PTR [rsp + 0]"); // compression level - emitter.instruction("call compress2"); // zlib-compress the source into the buffer - emitter.instruction("mov rax, QWORD PTR [rsp + 32]"); // compressed buffer becomes the result pointer - emitter.instruction("mov rdx, QWORD PTR [rsp + 24]"); // compress2 wrote the compressed length here - emitter.instruction("add rsp, 64"); // release the scratch frame - } - } - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/gzdeflate.rs b/src/codegen/builtins/strings/gzdeflate.rs deleted file mode 100644 index a8e09979dc..0000000000 --- a/src/codegen/builtins/strings/gzdeflate.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! Purpose: -//! Emits PHP `gzdeflate` calls. -//! Compresses a string into raw DEFLATE data with the system zlib -//! (`deflateInit2_` / `deflate` / `deflateEnd`, windowBits -15). -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Raw DEFLATE (no zlib header/trailer) is what `gzinflate` and the -//! `zlib.deflate` stream filter consume; `gzcompress` differs by emitting the -//! zlib-wrapped format. -//! - The zlib calls are emitted inline at the call site (not as a shared -//! runtime helper) so only programs that use `gzdeflate` carry a `libz` -//! dependency; the checker adds `-lz` for them. -//! - The transient `z_stream` (112 bytes LP64) lives in a stack scratch frame; -//! the result is an owned heap string (heap kind 1) sized by `compressBound`. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::args::emit_string_arg; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `gzdeflate()` string builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("gzdeflate()"); - // The data argument may arrive as a boxed mixed value, so coerce it to a - // plain string before handing the pointer/length pair to zlib. - emit_string_arg(&args[0], emitter, ctx, data); - let zero = ctx.next_label("gzdeflate_zero"); - let zeroed = ctx.next_label("gzdeflate_zeroed"); - match emitter.target.arch { - Arch::AArch64 => emit_arm64(args, emitter, ctx, data, &zero, &zeroed), - Arch::X86_64 => emit_x86_64(args, emitter, ctx, data, &zero, &zeroed), - } - Some(PhpType::Str) -} - -/// ARM64: `z_stream` scratch frame holds the 112-byte struct at `[sp, #0]` -/// plus saved values at `[sp, #112..160)`. -fn emit_arm64( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - zero: &str, - zeroed: &str, -) { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the source string - if args.len() >= 2 { - emit_expr(&args[1], emitter, ctx, data); - } else { - emitter.instruction("mov x0, #-1"); // default zlib compression level - } - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the source pointer/length - - // -- reserve the z_stream (112 B) plus scratch slots -- - emitter.instruction("sub sp, sp, #160"); // z_stream frame plus saved values - emitter.instruction("str x0, [sp, #136]"); // save the compression level - emitter.instruction("str x1, [sp, #112]"); // save the source pointer - emitter.instruction("str x2, [sp, #120]"); // save the source length - - // -- size and allocate the output buffer -- - emitter.instruction("mov x0, x2"); // source length into the compressBound argument - emitter.bl_c("compressBound"); // x0 = worst-case compressed size - emitter.instruction("str x0, [sp, #144]"); // save the output buffer capacity - emitter.instruction("bl __rt_heap_alloc"); // allocate the compressed-data buffer - emitter.instruction("mov x9, #1"); // heap kind 1 = persisted elephc string - emitter.instruction("str x9, [x0, #-8]"); // stamp the buffer as an owned string - emitter.instruction("str x0, [sp, #128]"); // save the destination buffer pointer - - // -- zero the 112-byte z_stream so zalloc/zfree start NULL -- - emitter.instruction("mov x9, #0"); // z_stream byte clear index - emitter.label(zero); - emitter.instruction("cmp x9, #112"); // cleared the whole z_stream struct? - emitter.instruction(&format!("b.ge {}", zeroed)); // the struct is fully zeroed - emitter.instruction("strb wzr, [sp, x9]"); // zero one z_stream byte - emitter.instruction("add x9, x9, #1"); // advance the clear index - emitter.instruction(&format!("b {}", zero)); // continue zeroing the struct - emitter.label(zeroed); - - // -- deflateInit2_(strm, level, Z_DEFLATED, -15, memLevel, strategy, ...) -- - // windowBits -15 selects raw deflate with no zlib header or trailer. - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.instruction("ldr x1, [sp, #136]"); // arg 1 = compression level - emitter.instruction("mov x2, #8"); // arg 2 = Z_DEFLATED method - emitter.instruction("mov x3, #-15"); // arg 3 = windowBits -15: raw deflate - emitter.instruction("mov x4, #8"); // arg 4 = default memLevel - emitter.instruction("mov x5, #0"); // arg 5 = Z_DEFAULT_STRATEGY - abi::emit_symbol_address(emitter, "x6", "_zlib_version"); - emitter.instruction("mov x7, #112"); // arg 7 = sizeof(z_stream) for the ABI check - emitter.bl_c("deflateInit2_"); // initialize a raw-deflate zlib stream - - // -- point the stream at the input and output buffers -- - emitter.instruction("ldr x9, [sp, #112]"); // reload the source pointer - emitter.instruction("str x9, [sp, #0]"); // z_stream.next_in = source pointer - emitter.instruction("ldr x9, [sp, #120]"); // reload the source length - emitter.instruction("str w9, [sp, #8]"); // z_stream.avail_in = source length - emitter.instruction("ldr x9, [sp, #128]"); // reload the destination buffer pointer - emitter.instruction("str x9, [sp, #24]"); // z_stream.next_out = destination buffer - emitter.instruction("ldr x9, [sp, #144]"); // reload the output buffer capacity - emitter.instruction("str w9, [sp, #32]"); // z_stream.avail_out = output capacity - - // -- deflate the whole input in a single Z_FINISH pass -- - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.instruction("mov x1, #4"); // arg 1 = Z_FINISH - emitter.bl_c("deflate"); // compress the entire input at once - - // -- end the stream and return the compressed buffer -- - emitter.instruction("ldr x2, [sp, #40]"); // z_stream.total_out = compressed length - emitter.instruction("str x2, [sp, #152]"); // save the compressed length across deflateEnd - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.bl_c("deflateEnd"); // release zlib's internal deflate state - emitter.instruction("ldr x1, [sp, #128]"); // compressed buffer becomes the result pointer - emitter.instruction("ldr x2, [sp, #152]"); // restore the compressed length - emitter.instruction("add sp, sp, #160"); // release the z_stream scratch frame -} - -/// x86_64: same `z_stream` scratch layout; `deflateInit2_` takes its 7th and -/// 8th arguments on the stack. -fn emit_x86_64( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - zero: &str, - zeroed: &str, -) { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the source string - if args.len() >= 2 { - emit_expr(&args[1], emitter, ctx, data); - } else { - emitter.instruction("mov eax, -1"); // default zlib compression level - } - emitter.instruction("mov rdi, rax"); // hold the compression level in a scratch register - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore the data pointer/length - - // -- reserve the z_stream (112 B) plus scratch slots -- - emitter.instruction("sub rsp, 160"); // z_stream frame plus saved values - emitter.instruction("mov QWORD PTR [rsp + 136], rdi"); // save the compression level - emitter.instruction("mov QWORD PTR [rsp + 112], rsi"); // save the source pointer - emitter.instruction("mov QWORD PTR [rsp + 120], rdx"); // save the source length - - // -- size and allocate the output buffer -- - emitter.instruction("mov rdi, rdx"); // source length into the compressBound argument - emitter.instruction("call compressBound"); // rax = worst-case compressed size - emitter.instruction("mov QWORD PTR [rsp + 144], rax"); // save the output buffer capacity - emitter.instruction("call __rt_heap_alloc"); // allocate the compressed-data buffer - emitter.instruction(&format!( // owned-string heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 1 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the buffer as an owned string - emitter.instruction("mov QWORD PTR [rsp + 128], rax"); // save the destination buffer pointer - - // -- zero the 112-byte z_stream so zalloc/zfree start NULL -- - emitter.instruction("xor r9, r9"); // z_stream byte clear index - emitter.label(zero); - emitter.instruction("cmp r9, 112"); // cleared the whole z_stream struct? - emitter.instruction(&format!("jge {}", zeroed)); // the struct is fully zeroed - emitter.instruction("mov BYTE PTR [rsp + r9], 0"); // zero one z_stream byte - emitter.instruction("inc r9"); // advance the clear index - emitter.instruction(&format!("jmp {}", zero)); // continue zeroing the struct - emitter.label(zeroed); - - // -- deflateInit2_(strm, level, Z_DEFLATED, -15, memLevel, strategy, ...) -- - // windowBits -15 selects raw deflate; args 7-8 are passed on the stack. - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("mov rsi, QWORD PTR [rsp + 136]"); // arg 1 = compression level - emitter.instruction("mov edx, 8"); // arg 2 = Z_DEFLATED method - emitter.instruction("mov ecx, -15"); // arg 3 = windowBits -15: raw deflate - emitter.instruction("mov r8d, 8"); // arg 4 = default memLevel - emitter.instruction("xor r9d, r9d"); // arg 5 = Z_DEFAULT_STRATEGY - emitter.instruction("sub rsp, 16"); // reserve the two stack arguments - abi::emit_symbol_address(emitter, "rax", "_zlib_version"); // the zlib version string - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // stack arg 6 = version - emitter.instruction("mov QWORD PTR [rsp + 8], 112"); // stack arg 7 = sizeof(z_stream) - emitter.instruction("call deflateInit2_"); // initialize a raw-deflate zlib stream - emitter.instruction("add rsp, 16"); // release the stack-argument space - - // -- point the stream at the input and output buffers -- - emitter.instruction("mov r9, QWORD PTR [rsp + 112]"); // reload the source pointer - emitter.instruction("mov QWORD PTR [rsp + 0], r9"); // z_stream.next_in = source pointer - emitter.instruction("mov r9, QWORD PTR [rsp + 120]"); // reload the source length - emitter.instruction("mov DWORD PTR [rsp + 8], r9d"); // z_stream.avail_in = source length - emitter.instruction("mov r9, QWORD PTR [rsp + 128]"); // reload the destination buffer pointer - emitter.instruction("mov QWORD PTR [rsp + 24], r9"); // z_stream.next_out = destination buffer - emitter.instruction("mov r9, QWORD PTR [rsp + 144]"); // reload the output buffer capacity - emitter.instruction("mov DWORD PTR [rsp + 32], r9d"); // z_stream.avail_out = output capacity - - // -- deflate the whole input in a single Z_FINISH pass -- - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("mov esi, 4"); // arg 1 = Z_FINISH - emitter.instruction("call deflate"); // compress the entire input at once - - // -- end the stream and return the compressed buffer -- - emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // z_stream.total_out = compressed length - emitter.instruction("mov QWORD PTR [rsp + 152], rax"); // save the compressed length across deflateEnd - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("call deflateEnd"); // release zlib's internal deflate state - emitter.instruction("mov rax, QWORD PTR [rsp + 128]"); // compressed buffer becomes the result pointer - emitter.instruction("mov rdx, QWORD PTR [rsp + 152]"); // restore the compressed length - emitter.instruction("add rsp, 160"); // release the z_stream scratch frame -} diff --git a/src/codegen/builtins/strings/gzinflate.rs b/src/codegen/builtins/strings/gzinflate.rs deleted file mode 100644 index 043f8a860d..0000000000 --- a/src/codegen/builtins/strings/gzinflate.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! Purpose: -//! Emits PHP `gzinflate` calls. -//! Decompresses raw DEFLATE data with the system zlib (`inflateInit2_` / -//! `inflate` / `inflateEnd`, windowBits -15). -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Raw DEFLATE is what `gzdeflate` and the `zlib.deflate` stream filter -//! produce; `gzuncompress` differs by expecting the zlib-wrapped format. -//! - The zlib calls are emitted inline at the call site so only programs that -//! use `gzinflate` carry a `libz` dependency; the checker adds `-lz`. -//! - A non-`Z_STREAM_END` inflate status is boxed as PHP `false`, success as a -//! boxed string. The output buffer is sized at 256x the input (min 64 KiB); -//! the optional `max_length` argument is ignored in v1. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::args::emit_string_arg; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `gzinflate()` string builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("gzinflate()"); - // The compressed argument may arrive as a boxed mixed value (e.g. the - // string|false returned by file_get_contents), so coerce it to a plain - // string before handing the pointer/length pair to zlib. - emit_string_arg(&args[0], emitter, ctx, data); - let zero = ctx.next_label("gzinflate_zero"); - let zeroed = ctx.next_label("gzinflate_zeroed"); - let fail = ctx.next_label("gzinflate_fail"); - let done = ctx.next_label("gzinflate_done"); - match emitter.target.arch { - Arch::AArch64 => emit_arm64(emitter, &zero, &zeroed, &fail, &done), - Arch::X86_64 => emit_x86_64(emitter, &zero, &zeroed, &fail, &done), - } - box_string_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// ARM64: `z_stream` scratch frame holds the 112-byte struct at `[sp, #0]` -/// plus saved values at `[sp, #112..160)`. Leaves a pointer/length result in -/// `x1`/`x2`, or `x1 = 0` on a zlib error. -fn emit_arm64(emitter: &mut Emitter, zero: &str, zeroed: &str, fail: &str, done: &str) { - // -- reserve the z_stream (112 B) plus scratch slots -- - emitter.instruction("sub sp, sp, #160"); // z_stream frame plus saved values - emitter.instruction("str x1, [sp, #112]"); // save the source pointer - emitter.instruction("str x2, [sp, #120]"); // save the source length - - // -- size the output buffer at 256x the input (min 64 KiB) -- - emitter.instruction("lsl x9, x2, #8"); // budget 256x the compressed size - emitter.instruction("mov x10, #65536"); // minimum decompression buffer size - emitter.instruction("cmp x9, x10"); // is the 256x budget larger? - emitter.instruction("csel x9, x9, x10, gt"); // pick the larger buffer size - emitter.instruction("str x9, [sp, #144]"); // save the output buffer capacity - emitter.instruction("mov x0, x9"); // buffer size into the allocator argument - emitter.instruction("bl __rt_heap_alloc"); // allocate the decompressed-data buffer - emitter.instruction("mov x9, #1"); // heap kind 1 = persisted elephc string - emitter.instruction("str x9, [x0, #-8]"); // stamp the buffer as an owned string - emitter.instruction("str x0, [sp, #128]"); // save the destination buffer pointer - - // -- zero the 112-byte z_stream so zalloc/zfree start NULL -- - emitter.instruction("mov x9, #0"); // z_stream byte clear index - emitter.label(zero); - emitter.instruction("cmp x9, #112"); // cleared the whole z_stream struct? - emitter.instruction(&format!("b.ge {}", zeroed)); // the struct is fully zeroed - emitter.instruction("strb wzr, [sp, x9]"); // zero one z_stream byte - emitter.instruction("add x9, x9, #1"); // advance the clear index - emitter.instruction(&format!("b {}", zero)); // continue zeroing the struct - emitter.label(zeroed); - - // -- inflateInit2_(strm, -15, version, size): -15 selects raw inflate -- - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.instruction("mov x1, #-15"); // arg 1 = windowBits -15: raw inflate - abi::emit_symbol_address(emitter, "x2", "_zlib_version"); - emitter.instruction("mov x3, #112"); // arg 3 = sizeof(z_stream) for the ABI check - emitter.bl_c("inflateInit2_"); // initialize a raw-inflate zlib stream - - // -- point the stream at the input and output buffers -- - emitter.instruction("ldr x9, [sp, #112]"); // reload the source pointer - emitter.instruction("str x9, [sp, #0]"); // z_stream.next_in = source pointer - emitter.instruction("ldr x9, [sp, #120]"); // reload the source length - emitter.instruction("str w9, [sp, #8]"); // z_stream.avail_in = source length - emitter.instruction("ldr x9, [sp, #128]"); // reload the destination buffer pointer - emitter.instruction("str x9, [sp, #24]"); // z_stream.next_out = destination buffer - emitter.instruction("ldr x9, [sp, #144]"); // reload the output buffer capacity - emitter.instruction("str w9, [sp, #32]"); // z_stream.avail_out = output capacity - - // -- inflate the whole input in a single Z_FINISH pass -- - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.instruction("mov x1, #4"); // arg 1 = Z_FINISH - emitter.bl_c("inflate"); // decompress the entire input at once - emitter.instruction("str x0, [sp, #136]"); // save the inflate status code - emitter.instruction("ldr x2, [sp, #40]"); // z_stream.total_out = inflated length - emitter.instruction("str x2, [sp, #152]"); // save the inflated length across inflateEnd - - // -- end the stream -- - emitter.instruction("mov x0, sp"); // arg 0 = z_stream pointer - emitter.bl_c("inflateEnd"); // release zlib's internal inflate state - - // -- success only when inflate reported Z_STREAM_END -- - emitter.instruction("ldr x9, [sp, #136]"); // reload the inflate status code - emitter.instruction("cmp x9, #1"); // did inflate reach Z_STREAM_END? - emitter.instruction(&format!("b.ne {}", fail)); // a zlib error becomes a false result - emitter.instruction("ldr x1, [sp, #128]"); // decompressed buffer becomes the result - emitter.instruction("ldr x2, [sp, #152]"); // restore the inflated length - emitter.instruction(&format!("b {}", done)); // skip the failure values - emitter.label(fail); - emitter.instruction("mov x1, #0"); // a null pointer marks the zlib error - emitter.instruction("mov x2, #0"); // no length for the failure case - emitter.label(done); - emitter.instruction("add sp, sp, #160"); // release the z_stream scratch frame -} - -/// x86_64: same `z_stream` scratch layout. Leaves a pointer/length result in -/// `rax`/`rdx`, or `rax = 0` on a zlib error. -fn emit_x86_64(emitter: &mut Emitter, zero: &str, zeroed: &str, fail: &str, done: &str) { - let sized = format!("{}_sized", zero); - - // -- reserve the z_stream (112 B) plus scratch slots -- - emitter.instruction("sub rsp, 160"); // z_stream frame plus saved values - emitter.instruction("mov QWORD PTR [rsp + 112], rax"); // save the source pointer - emitter.instruction("mov QWORD PTR [rsp + 120], rdx"); // save the source length - - // -- size the output buffer at 256x the input (min 64 KiB) -- - emitter.instruction("mov r9, rdx"); // copy the compressed length - emitter.instruction("shl r9, 8"); // budget 256x the compressed size - emitter.instruction("cmp r9, 65536"); // is the 256x budget above the minimum? - emitter.instruction(&format!("jge {}", sized)); // keep the larger budget - emitter.instruction("mov r9, 65536"); // otherwise use the minimum buffer size - emitter.label(&sized); - emitter.instruction("mov QWORD PTR [rsp + 144], r9"); // save the output buffer capacity - emitter.instruction("mov rax, r9"); // buffer size into the allocator argument - emitter.instruction("call __rt_heap_alloc"); // allocate the decompressed-data buffer - emitter.instruction(&format!( // owned-string heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 1 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the buffer as an owned string - emitter.instruction("mov QWORD PTR [rsp + 128], rax"); // save the destination buffer pointer - - // -- zero the 112-byte z_stream so zalloc/zfree start NULL -- - emitter.instruction("xor r9, r9"); // z_stream byte clear index - emitter.label(zero); - emitter.instruction("cmp r9, 112"); // cleared the whole z_stream struct? - emitter.instruction(&format!("jge {}", zeroed)); // the struct is fully zeroed - emitter.instruction("mov BYTE PTR [rsp + r9], 0"); // zero one z_stream byte - emitter.instruction("inc r9"); // advance the clear index - emitter.instruction(&format!("jmp {}", zero)); // continue zeroing the struct - emitter.label(zeroed); - - // -- inflateInit2_(strm, -15, version, size): -15 selects raw inflate -- - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("mov esi, -15"); // arg 1 = windowBits -15: raw inflate - abi::emit_symbol_address(emitter, "rdx", "_zlib_version"); // arg 2 = the zlib version string - emitter.instruction("mov ecx, 112"); // arg 3 = sizeof(z_stream) for the ABI check - emitter.instruction("call inflateInit2_"); // initialize a raw-inflate zlib stream - - // -- point the stream at the input and output buffers -- - emitter.instruction("mov r9, QWORD PTR [rsp + 112]"); // reload the source pointer - emitter.instruction("mov QWORD PTR [rsp + 0], r9"); // z_stream.next_in = source pointer - emitter.instruction("mov r9, QWORD PTR [rsp + 120]"); // reload the source length - emitter.instruction("mov DWORD PTR [rsp + 8], r9d"); // z_stream.avail_in = source length - emitter.instruction("mov r9, QWORD PTR [rsp + 128]"); // reload the destination buffer pointer - emitter.instruction("mov QWORD PTR [rsp + 24], r9"); // z_stream.next_out = destination buffer - emitter.instruction("mov r9, QWORD PTR [rsp + 144]"); // reload the output buffer capacity - emitter.instruction("mov DWORD PTR [rsp + 32], r9d"); // z_stream.avail_out = output capacity - - // -- inflate the whole input in a single Z_FINISH pass -- - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("mov esi, 4"); // arg 1 = Z_FINISH - emitter.instruction("call inflate"); // decompress the entire input at once - emitter.instruction("mov QWORD PTR [rsp + 136], rax"); // save the inflate status code - emitter.instruction("mov rax, QWORD PTR [rsp + 40]"); // z_stream.total_out = inflated length - emitter.instruction("mov QWORD PTR [rsp + 152], rax"); // save the inflated length across inflateEnd - - // -- end the stream -- - emitter.instruction("mov rdi, rsp"); // arg 0 = z_stream pointer - emitter.instruction("call inflateEnd"); // release zlib's internal inflate state - - // -- success only when inflate reported Z_STREAM_END -- - emitter.instruction("cmp QWORD PTR [rsp + 136], 1"); // did inflate reach Z_STREAM_END? - emitter.instruction(&format!("jne {}", fail)); // a zlib error becomes a false result - emitter.instruction("mov rax, QWORD PTR [rsp + 128]"); // decompressed buffer becomes the result - emitter.instruction("mov rdx, QWORD PTR [rsp + 152]"); // restore the inflated length - emitter.instruction(&format!("jmp {}", done)); // skip the failure values - emitter.label(fail); - emitter.instruction("xor eax, eax"); // a null pointer marks the zlib error - emitter.instruction("xor edx, edx"); // no length for the failure case - emitter.label(done); - emitter.instruction("add rsp, 160"); // release the z_stream scratch frame -} - -/// Boxes the inflate result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("gzinflate_false"); - let done_label = ctx.next_label("gzinflate_boxed"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means a zlib error - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means a zlib error - emitter.instruction(&format!("jz {}", false_label)); // box false on a zlib error - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/strings/gzuncompress.rs b/src/codegen/builtins/strings/gzuncompress.rs deleted file mode 100644 index 2608529bcd..0000000000 --- a/src/codegen/builtins/strings/gzuncompress.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! Purpose: -//! Emits PHP `gzuncompress` calls. -//! Decompresses a zlib-compressed string with the system zlib (`uncompress`). -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - The zlib call is emitted inline at the call site so only programs that use -//! `gzuncompress` carry a `libz` dependency; the checker adds `-lz` for them. -//! - A non-zero zlib status is boxed as PHP `false`; a success as a boxed -//! string. The decompression buffer is sized at 256x the input (min 64 KiB). -//! - v1 ignores the optional `max_length` argument. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::args::emit_string_arg; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `gzuncompress()` string builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("gzuncompress()"); - // The compressed argument may arrive as a boxed mixed value (e.g. the - // string|false returned by file_get_contents), so coerce it to a plain - // string before handing the pointer/length pair to zlib. - emit_string_arg(&args[0], emitter, ctx, data); - let ok = ctx.next_label("gzuncompress_ok"); - let after = ctx.next_label("gzuncompress_after"); - match emitter.target.arch { - Arch::AArch64 => { - // -- inline zlib uncompress: x1/x2 = compressed data -- - emitter.instruction("sub sp, sp, #48"); // scratch frame for the decompression state - emitter.instruction("str x1, [sp, #0]"); // save the source pointer - emitter.instruction("str x2, [sp, #8]"); // save the source length - emitter.instruction("lsl x9, x2, #8"); // budget 256x the compressed size - emitter.instruction("mov x10, #65536"); // minimum decompression buffer size - emitter.instruction("cmp x9, x10"); // is the 256x budget larger? - emitter.instruction("csel x9, x9, x10, gt"); // pick the larger buffer size - emitter.instruction("str x9, [sp, #16]"); // seed destLen with the buffer capacity - emitter.instruction("mov x0, x9"); // buffer size into the allocator argument - emitter.instruction("bl __rt_heap_alloc"); // allocate the decompressed-data buffer - emitter.instruction("mov x9, #1"); // heap kind 1 = persisted elephc string - emitter.instruction("str x9, [x0, #-8]"); // stamp the buffer as an owned string - emitter.instruction("str x0, [sp, #24]"); // save the destination buffer pointer - emitter.instruction("add x1, sp, #16"); // &destLen in/out parameter - emitter.instruction("ldr x2, [sp, #0]"); // source pointer - emitter.instruction("ldr x3, [sp, #8]"); // source length - emitter.bl_c("uncompress"); // zlib-decompress the source - emitter.instruction(&format!("cbz x0, {}", ok)); // a zero zlib status means success - emitter.instruction("mov x1, #0"); // a zlib error becomes a null result - emitter.instruction("mov x2, #0"); // no length for the failure case - emitter.instruction(&format!("b {}", after)); // skip the success values - emitter.label(&ok); - emitter.instruction("ldr x1, [sp, #24]"); // decompressed buffer becomes the result - emitter.instruction("ldr x2, [sp, #16]"); // uncompress wrote the decompressed length - emitter.label(&after); - emitter.instruction("add sp, sp, #48"); // release the scratch frame - } - Arch::X86_64 => { - let sized = ctx.next_label("gzuncompress_sized"); - // -- inline zlib uncompress: rax/rdx = compressed data -- - emitter.instruction("sub rsp, 48"); // scratch frame for the decompression state - emitter.instruction("mov QWORD PTR [rsp + 0], rax"); // save the source pointer - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the source length - emitter.instruction("mov r9, rdx"); // copy the compressed length - emitter.instruction("shl r9, 8"); // budget 256x the compressed size - emitter.instruction("cmp r9, 65536"); // is the 256x budget above the minimum? - emitter.instruction(&format!("jge {}", sized)); // keep the larger budget - emitter.instruction("mov r9, 65536"); // otherwise use the minimum buffer size - emitter.label(&sized); - emitter.instruction("mov QWORD PTR [rsp + 16], r9"); // seed destLen with the buffer capacity - emitter.instruction("mov rax, r9"); // buffer size into the allocator argument - emitter.instruction("call __rt_heap_alloc"); // allocate the decompressed-data buffer - emitter.instruction(&format!( // owned-string heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 1 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the buffer as an owned string - emitter.instruction("mov QWORD PTR [rsp + 24], rax"); // save the destination buffer pointer - emitter.instruction("mov rdi, rax"); // destination buffer pointer - emitter.instruction("lea rsi, [rsp + 16]"); // &destLen in/out parameter - emitter.instruction("mov rdx, QWORD PTR [rsp + 0]"); // source pointer - emitter.instruction("mov rcx, QWORD PTR [rsp + 8]"); // source length - emitter.instruction("call uncompress"); // zlib-decompress the source - emitter.instruction("test rax, rax"); // a zero zlib status means success - emitter.instruction(&format!("jz {}", ok)); // take the success path - emitter.instruction("xor eax, eax"); // a zlib error becomes a null result - emitter.instruction("xor edx, edx"); // no length for the failure case - emitter.instruction(&format!("jmp {}", after)); // skip the success values - emitter.label(&ok); - emitter.instruction("mov rax, QWORD PTR [rsp + 24]"); // decompressed buffer becomes the result - emitter.instruction("mov rdx, QWORD PTR [rsp + 16]"); // uncompress wrote the decompressed length - emitter.label(&after); - emitter.instruction("add rsp, 48"); // release the scratch frame - } - } - box_string_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("gzuncompress_false"); - let done_label = ctx.next_label("gzuncompress_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means a zlib error - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means a zlib error - emitter.instruction(&format!("jz {}", false_label)); // box false on a zlib error - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/strings/hash.rs b/src/codegen/builtins/strings/hash.rs deleted file mode 100644 index a5af074665..0000000000 --- a/src/codegen/builtins/strings/hash.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Purpose: -//! Emits PHP `hash` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use super::hash_crypto; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_truthiness, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `hash($algo, $data, $binary = false)` call as a runtime helper invocation. -/// -/// The algorithm and data strings are evaluated first, in PHP source order -/// (`$algo` then `$data`), each through `emit_string_arg` so non-string values -/// (Mixed, int, float) are coerced into the string ABI register pair, and -/// preserved on the stack while the optional -/// `$binary` flag is evaluated and coerced to a 0/1 integer. Before the -/// `__rt_hash` call the arguments are materialised in the runtime ABI registers -/// (algo ptr/len, data ptr/len, and the binary flag in AArch64 `x5` / x86_64 `r10`). The -/// `elephc_crypto_hash` entry point is published into its runtime fn-pointer slot -/// immediately before the call so only hashing programs link `-lelephc_crypto`. -/// -/// # Arguments -/// - `_name`: Unused; the runtime helper handles algorithm dispatch internally. -/// - `args`: Two or three expressions — algorithm name, data string, and the -/// optional `$binary` flag (defaults to `false`/`0` when omitted). -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and metadata. -/// - `data`: Data section for relocatable constants. -/// -/// # Returns -/// `Some(PhpType::Str)` indicating the result is a PHP string. -/// -/// # Side effects -/// - Clobbers caller-saved registers appropriate to each target's ABI. -/// - The runtime helper allocates a PHP string; caller owns the returned value. -/// - An unknown algorithm throws a catchable `\ValueError` from the runtime. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hash()"); - // hash($algo, $data, $binary) — evaluate the algo string first. emit_string_arg coerces - // each string argument via coerce_to_string, so a Mixed value (e.g. a Mixed-typed - // function-call result) is cast through __rt_mixed_cast_string instead of leaving a - // boxed cell in the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the algorithm string while evaluating the data string and binary flag - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the data string (PHP evaluates $data before $binary) - emit_binary_flag(args, 2, emitter, ctx, data); - emitter.instruction("mov x5, x0"); // move the 0/1 binary flag into its runtime argument register on AArch64 - emitter.instruction("ldp x3, x4, [sp], #16"); // restore the data string into the secondary runtime argument register pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the algorithm string into the primary runtime argument register pair - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the algorithm string ptr/len while evaluating the data string and binary flag - super::args::emit_string_arg(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the data string (PHP evaluates $data before $binary) - emit_binary_flag(args, 2, emitter, ctx, data); - emitter.instruction("mov r10, rax"); // move the 0/1 binary flag into its runtime argument register on x86_64 - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the data string into the secondary x86_64 runtime argument registers - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the algorithm string ptr/len into the primary runtime argument registers - } - } - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_hash"); // call the target-aware runtime helper that hashes through elephc-crypto and returns the PHP string - Some(PhpType::Str) -} - -/// Materialises a `$binary` flag as a 0/1 integer in the int result register, -/// defaulting to `0` (PHP `false`) when `args` has no argument at `flag_index`. -/// -/// Shared by `hash()` (flag at index 2) and `md5()`/`sha1()` (flag at index 1) -/// so all three honour the same truthiness coercion for their `$binary` argument. -pub(super) fn emit_binary_flag( - args: &[Expr], - flag_index: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if args.len() > flag_index { - let ty = emit_expr(&args[flag_index], emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &ty); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); // default $binary to false (hex output) when omitted - } -} diff --git a/src/codegen/builtins/strings/hash_algos.rs b/src/codegen/builtins/strings/hash_algos.rs deleted file mode 100644 index 1cfd441b79..0000000000 --- a/src/codegen/builtins/strings/hash_algos.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Purpose: -//! Emits PHP `hash_algos()` calls — returns the array of hash algorithm names -//! elephc-crypto supports. Delegates to the `__rt_hash_algos_list` runtime helper -//! that builds the string array. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Takes no arguments; the supported-algorithm list lives in the runtime helper -//! (`runtime::strings::hash_algos::HASH_ALGOS`), kept in lockstep with the crate. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `hash_algos()` builtin call: invokes `__rt_hash_algos_list`, which -/// returns a PHP array of the supported algorithm-name strings. Returns -/// `PhpType::Array(Box::new(PhpType::Str))`. -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("hash_algos()"); - abi::emit_call_label(emitter, "__rt_hash_algos_list"); // build and return the supported-algorithm string array - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/strings/hash_context.rs b/src/codegen/builtins/strings/hash_context.rs deleted file mode 100644 index 8160bc392d..0000000000 --- a/src/codegen/builtins/strings/hash_context.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Purpose: -//! Emits PHP incremental hashing builtins `hash_init`, `hash_update`, -//! `hash_final`, `hash_copy`. A HashContext is a resource handle (Mixed tag 9), -//! produced by `hash_init`/`hash_copy` and consumed by `hash_update`/`hash_final` -//! through the elephc-crypto incremental C ABI. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Context arguments are unboxed with the shared `emit_stream_fd_arg` (tag-9 -//! resource → raw handle), the same helper `fclose` uses for streams. -//! - `hash_init` with a flags/key argument (HASH_HMAC streaming mode) is rejected -//! by the type checker — `hash_hmac()` covers HMAC. - -use super::hash::emit_binary_flag; -use super::hash_crypto; -use crate::codegen::builtins::io::stream_arg::emit_stream_fd_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits `hash_init($algo)`: evaluates the algorithm name and opens an incremental -/// HashContext via `__rt_hash_init` (which throws `\ValueError` on an unknown -/// algorithm). Returns `PhpType::Mixed` (the boxed resource). -pub fn emit_init( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hash_init()"); - // emit_string_arg coerces a Mixed algorithm argument through __rt_mixed_cast_string, - // so the string registers never hold a stale pair when the value is a boxed cell. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_hash_init"); // open the HashContext (algo ptr/len already in the string registers) - Some(PhpType::Mixed) -} - -/// Emits `hash_update($ctx, $data)`: unboxes the context handle, evaluates the -/// data string, and feeds it via `__rt_hash_update`. Returns `PhpType::Bool`. -pub fn emit_update( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hash_update()"); - emit_stream_fd_arg("hash_update", &args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the context handle while evaluating the data string - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("ldr x0, [sp], #16"); // restore the context handle into the C ABI ctx register - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the context handle while evaluating the data string - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rsi, rax"); // C ABI data_ptr = the evaluated data string pointer - abi::emit_pop_reg(emitter, "rdi"); // restore the context handle into the C ABI ctx register - } - } - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_hash_update"); // feed the data into the context (ctx/data already in C ABI registers) - Some(PhpType::Bool) -} - -/// Emits `hash_final($ctx, $binary = false)`: unboxes the context handle, -/// materialises the binary flag, and finalizes+frees the context via -/// `__rt_hash_final`. Returns `PhpType::Str`. -pub fn emit_final( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hash_final()"); - emit_stream_fd_arg("hash_final", &args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the context handle while evaluating the binary flag - emit_binary_flag(args, 1, emitter, ctx, data); - emitter.instruction("mov x5, x0"); // move the 0/1 binary flag into its runtime argument register - emitter.instruction("ldr x0, [sp], #16"); // restore the context handle into the C ABI ctx register - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the context handle while evaluating the binary flag - emit_binary_flag(args, 1, emitter, ctx, data); - emitter.instruction("mov r10, rax"); // move the 0/1 binary flag into its runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the context handle into the C ABI ctx register - } - } - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_hash_final"); // finalize+free the context and format the digest string - Some(PhpType::Str) -} - -/// Emits `hash_copy($ctx)`: unboxes the context handle and deep-clones it via -/// `__rt_hash_copy`. Returns `PhpType::Mixed` (a new boxed resource). -pub fn emit_copy( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hash_copy()"); - emit_stream_fd_arg("hash_copy", &args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the context handle into the C ABI ctx register - } - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_hash_copy"); // clone the context (handle already in the C ABI ctx register) - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/strings/hash_crypto.rs b/src/codegen/builtins/strings/hash_crypto.rs deleted file mode 100644 index 294110643e..0000000000 --- a/src/codegen/builtins/strings/hash_crypto.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! Purpose: -//! Call-site and runtime support for routing PHP `hash()` and `hash_hmac()` -//! through the elephc-crypto staticlib. Publishes the `elephc_crypto_hash` and -//! `elephc_crypto_hmac` C entry points into their runtime function-pointer slots -//! and emits the catchable `\ValueError` thrown on an unknown algorithm name (or, -//! for `hash_hmac()`, a non-cryptographic checksum). -//! -//! Called from: -//! - `crate::codegen::builtins::strings::hash::emit()` and -//! `crate::codegen::builtins::strings::hash_hmac::emit()` (each publishes the -//! fn pointers immediately before its `__rt_hash`/`__rt_hash_hmac` call). -//! - `crate::codegen::runtime::strings::hash::emit_hash()` and -//! `crate::codegen::runtime::strings::hash_hmac::emit_hash_hmac()` (emit the -//! inline unknown-algorithm `\ValueError` throw shared between both arches). -//! -//! Key details: -//! - The fn pointers are published indirectly (mirroring the `_elephc_tls_*_fn` -//! pattern) so only programs that actually call `hash()`/`hash_hmac()` reference -//! the elephc-crypto entry points and therefore pull in `-lelephc_crypto` at -//! link time. -//! - The `\ValueError` throw replicates the heap-object stamping sequence used by -//! `crate::codegen::builtins::math::clamp`. The messages live in the fixed -//! runtime data section as `_hash_unknown_algo_msg` / `_hash_hmac_unknown_algo_msg`, -//! so the runtime emitter references them by symbol instead of through a -//! per-program `DataSection`. - -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; - -/// Publishes the `elephc_crypto_hash` and `elephc_crypto_hmac` C entry points -/// into their runtime function-pointer slots so `__rt_hash` and `__rt_hash_hmac` -/// can call through them. -/// -/// Mirrors `publish_tls_function_pointers`: it stamps each extern symbol address -/// into its slot (`_elephc_crypto_hash_fn` / `_elephc_crypto_hmac_fn`) for both -/// supported architectures. Emitting this at the call site (rather than in the -/// shared runtime) is what makes a program reference the elephc-crypto entry -/// points, so only programs that call `hash()`/`hash_hmac()` link `-lelephc_crypto`. -pub(crate) fn publish_elephc_crypto_function_pointers(emitter: &mut Emitter) { - const ENTRIES: &[(&str, &str)] = &[ - ("elephc_crypto_hash", "_elephc_crypto_hash_fn"), - ("elephc_crypto_hmac", "_elephc_crypto_hmac_fn"), - ("elephc_crypto_init", "_elephc_crypto_init_fn"), - ("elephc_crypto_update", "_elephc_crypto_update_fn"), - ("elephc_crypto_final", "_elephc_crypto_final_fn"), - ("elephc_crypto_clone", "_elephc_crypto_clone_fn"), - ]; - match emitter.target.arch { - Arch::AArch64 => { - for (c_name, slot) in ENTRIES { - let extern_sym = emitter.target.extern_symbol(c_name); - abi::emit_extern_symbol_address(emitter, "x9", &extern_sym); - abi::emit_symbol_address(emitter, "x10", slot); - emitter.instruction("str x9, [x10]"); // publish the elephc-crypto hash entry into its runtime slot - } - } - Arch::X86_64 => { - for (c_name, slot) in ENTRIES { - let extern_sym = emitter.target.extern_symbol(c_name); - abi::emit_extern_symbol_address(emitter, "r9", &extern_sym); - abi::emit_store_reg_to_symbol(emitter, "r9", slot, 0); // publish the elephc-crypto hash entry into its runtime slot - } - } - } -} - -/// Emits a catchable `\ValueError` for the unknown-algorithm paths of `hash()` -/// and `hash_hmac()`. -/// -/// `message_symbol` names a fixed runtime data string (`_hash_unknown_algo_msg` -/// for `hash()`, `_hash_hmac_unknown_algo_msg` for `hash_hmac()`) and -/// `message_len` is its byte length, so both built-ins reuse one throw path. -/// The emitted code does not return; it branches into `__rt_throw_current` after -/// publishing the exception object into `_exc_value`. Replicates `clamp`'s -/// `\ValueError` stamping sequence (heap kind 6 object word, -/// `_spl_value_error_class_id` at `[obj+0]`, message ptr/len at `[obj+8]`/`[obj+16]`, -/// code 0 at `[obj+24]`). -pub(crate) fn emit_throw_unknown_algorithm_value_error( - emitter: &mut Emitter, - message_symbol: &str, - message_len: usize, -) { - match emitter.target.arch { - Arch::AArch64 => emit_throw_value_error_aarch64(emitter, message_symbol, message_len), - Arch::X86_64 => emit_throw_value_error_x86_64(emitter, message_symbol, message_len), - } -} - -/// Emits the AArch64 allocation and unwinder handoff for the `hash()` `\ValueError`. -fn emit_throw_value_error_aarch64( - emitter: &mut Emitter, - message_symbol: &str, - message_len: usize, -) { - emitter.instruction("mov x0, #32"); // request Throwable payload storage - emitter.instruction("bl __rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction("mov x9, #6"); // heap kind 6 = object instance - emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object - abi::emit_symbol_address(emitter, "x9", "_spl_value_error_class_id"); - emitter.instruction("ldr x9, [x9]"); // load ValueError's runtime class id for this program - emitter.instruction("str x9, [x0]"); // store class id at the object header - abi::emit_symbol_address(emitter, "x9", message_symbol); - emitter.instruction("str x9, [x0, #8]"); // store static ValueError message pointer - emitter.instruction(&format!("mov x9, #{}", message_len)); // load static ValueError message length - emitter.instruction("str x9, [x0, #16]"); // store exception message length - emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - abi::emit_symbol_address(emitter, "x9", "_exc_value"); - emitter.instruction("str x0, [x9]"); // publish the active exception object - emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder -} - -/// Emits the Linux x86_64 allocation and unwinder handoff for the `hash()` `\ValueError`. -fn emit_throw_value_error_x86_64( - emitter: &mut Emitter, - message_symbol: &str, - message_len: usize, -) { - emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation - emitter.instruction("mov rbp, rsp"); // establish aligned helper frame - emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned - emitter.instruction("mov rax, 32"); // request Throwable payload storage - emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object - abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_value_error_class_id", 0); // load ValueError's runtime class id for this program - emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at the object header - abi::emit_symbol_address(emitter, "r10", message_symbol); // materialize static ValueError message pointer - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static ValueError message pointer - emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", message_len)); // store static ValueError message length - emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // publish the active exception object - emitter.instruction("mov rsp, rbp"); // release helper frame before throwing - emitter.instruction("pop rbp"); // restore caller frame pointer before throwing - emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder -} diff --git a/src/codegen/builtins/strings/hash_equals.rs b/src/codegen/builtins/strings/hash_equals.rs deleted file mode 100644 index 4194854f4f..0000000000 --- a/src/codegen/builtins/strings/hash_equals.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Purpose: -//! Emits PHP `hash_equals($known, $user)` calls — a timing-safe string equality -//! check. Marshals the two string arguments into the shared two-string ABI and -//! calls the pure `__rt_hash_equals` runtime helper (no crypto library). -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Same two-string register convention as `str_contains`/`__rt_strpos`; the -//! runtime returns the PHP boolean (0/1) directly in the int-result register. - -use super::args::emit_string_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `hash_equals($known, $user)` builtin call. -/// -/// Evaluates the known string, preserves it while evaluating the user string, -/// materialises both into the two-string ABI registers, and calls -/// `__rt_hash_equals` which performs a constant-time comparison and returns a -/// PHP boolean directly. Returns `PhpType::Bool`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hash_equals()"); - emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the known string ptr/len while evaluating the user string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the user string pointer into the third comparison argument register - emitter.instruction("mov x4, x2"); // move the user string length into the fourth comparison argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the known string ptr/len after evaluating the user string - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the known string ptr/len while evaluating the user string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // move the user string length into the fourth SysV comparison argument register - emitter.instruction("mov rdx, rax"); // move the user string pointer into the third SysV comparison argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the known string ptr/len into the first two SysV argument registers - } - } - abi::emit_call_label(emitter, "__rt_hash_equals"); // constant-time compare; returns the PHP boolean (0/1) directly - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/strings/hash_hmac.rs b/src/codegen/builtins/strings/hash_hmac.rs deleted file mode 100644 index 27291ee2cc..0000000000 --- a/src/codegen/builtins/strings/hash_hmac.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Purpose: -//! Emits the PHP `hash_hmac($algo, $data, $key, $binary = false)` call, routed -//! through the elephc-crypto staticlib's `elephc_crypto_hmac` C entry point. -//! Marshals the three string arguments plus the binary flag into the registers -//! the `__rt_hash_hmac` runtime helper expects. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - The returned string pointer/length pair is an owned `_concat_buf`-backed -//! runtime value, produced by the shared `__rt_digest_to_string` formatter. - -use super::hash::emit_binary_flag; -use super::hash_crypto; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `hash_hmac($algo, $data, $key, $binary = false)` call as a -/// runtime helper invocation. -/// -/// Arguments are evaluated in PHP source order — `$algo`, then `$data`, then -/// `$key`, then the optional `$binary` flag — and each intermediate string is -/// preserved on the temporary stack while later sub-expressions evaluate. The -/// three string arguments go through `emit_string_arg` so non-string values -/// (Mixed, int, float) are coerced into the string ABI register pair. They -/// are then delivered into the `__rt_hash_hmac` entry contract: on AArch64 the -/// algorithm pair in `x1`/`x2`, the data pair in `x3`/`x4`, the key pair in -/// `x5`/`x6`, and the binary flag in `x7`; on x86_64 the algorithm pair in -/// `rax`/`rdx`, the data pair in `rdi`/`rsi`, the key pair in `r10`/`r11`, and -/// the binary flag in `rcx`. The `elephc_crypto_hmac` entry point is published -/// into its runtime fn-pointer slot immediately before the call so only HMAC -/// programs link `-lelephc_crypto`. -/// -/// # Arguments -/// - `_name`: Unused; the runtime helper handles algorithm dispatch internally. -/// - `args`: Three or four expressions — algorithm name, data string, key -/// string, and the optional `$binary` flag (defaults to `false`/`0`). -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and metadata. -/// - `data`: Data section for relocatable constants. -/// -/// # Returns -/// `Some(PhpType::Str)` indicating the result is a PHP string. -/// -/// # Side effects -/// - Clobbers caller-saved registers appropriate to each target's ABI. -/// - The runtime helper allocates a PHP string; caller owns the returned value. -/// - An unknown algorithm or a non-cryptographic checksum throws a catchable -/// `\ValueError` from the runtime. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hash_hmac()"); - match emitter.target.arch { - Arch::AArch64 => { - // -- evaluate args in PHP source order, preserving each on the stack -- - super::args::emit_string_arg(&args[0], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the algorithm string while evaluating the remaining arguments - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the data string while evaluating the remaining arguments - super::args::emit_string_arg(&args[2], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the key string while evaluating the binary flag - emit_binary_flag(args, 3, emitter, ctx, data); - // -- deliver into the __rt_hash_hmac entry contract -- - emitter.instruction("mov x7, x0"); // binary flag → entry register x7 - emitter.instruction("ldp x5, x6, [sp], #16"); // restore the key string into the key entry register pair - emitter.instruction("ldp x3, x4, [sp], #16"); // restore the data string into the data entry register pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the algorithm string into the algorithm entry register pair - } - Arch::X86_64 => { - // -- evaluate args in PHP source order, preserving each on the stack -- - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the algorithm string while evaluating the remaining arguments - super::args::emit_string_arg(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the data string while evaluating the remaining arguments - super::args::emit_string_arg(&args[2], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the key string while evaluating the binary flag - emit_binary_flag(args, 3, emitter, ctx, data); - // -- deliver into the __rt_hash_hmac entry contract -- - emitter.instruction("mov rcx, rax"); // binary flag → entry register rcx - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // restore the key string into the key entry register pair - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the data string into the data entry register pair - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the algorithm string into the algorithm entry register pair - } - } - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_hash_hmac"); // call the target-aware runtime helper that HMACs through elephc-crypto and returns the PHP string - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/hex2bin.rs b/src/codegen/builtins/strings/hex2bin.rs deleted file mode 100644 index 2d8e04441e..0000000000 --- a/src/codegen/builtins/strings/hex2bin.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits PHP `hex2bin` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `hex2bin` builtin. -/// -/// Consumes the string argument in `args[0]` via `emit_string_arg`, then calls the -/// target-aware runtime helper `__rt_hex2bin` to decode hex to raw bytes. -/// Returns `PhpType::Str` as the result is always a PHP string. -/// -/// # Arguments -/// * `_name` — builtin name (unused, always `"hex2bin"`); -/// * `args` — must contain exactly one string-typed argument; -/// * `emitter` — target assembly emitter; -/// * `ctx` — codegen context (variable layout, ownership state); -/// * `data` — data section for relocations and constants. -/// -/// # Returns -/// `Some(PhpType::Str)` — the decoded binary string produced by the runtime helper. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hex2bin()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_hex2bin"); // convert the current hexadecimal string to bytes through the target-aware runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/html_entity_decode.rs b/src/codegen/builtins/strings/html_entity_decode.rs deleted file mode 100644 index da4cc2a791..0000000000 --- a/src/codegen/builtins/strings/html_entity_decode.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `html_entity_decode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `html_entity_decode` runtime helper. -/// -/// Arguments: -/// - `args[0]`: the input string to decode (already emitted) -/// -/// Behavior: -/// - Emits the input expression, then calls `__rt_html_entity_decode` to decode HTML entities. -/// - Return type is `PhpType::Str` — caller receives an owned PHP string. -/// -/// ABI: -/// - Caller is responsible for managing input expression lifecycle. -/// - Returned string pointer/length is an owned runtime value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("html_entity_decode()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_html_entity_decode"); // call the target-aware runtime helper that decodes HTML entities back into plain characters - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/htmlentities.rs b/src/codegen/builtins/strings/htmlentities.rs deleted file mode 100644 index 68e3da2bdb..0000000000 --- a/src/codegen/builtins/strings/htmlentities.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Purpose: -//! Emits PHP `htmlentities` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `htmlentities` PHP builtin call. -/// -/// Loads the string argument (first element of `args`) and calls the shared -/// `__rt_htmlspecialchars` runtime helper, which performs the HTML entity -/// encoding. The runtime allocates and returns a new PHP string. -/// -/// # Arguments -/// * `args` - Must contain at least one expression producing a string value. -/// * `emitter` - Target-aware instruction emitter. -/// * `ctx` - Codegen context carrying variable layout and ownership state. -/// * `data` - Data section for relocations and static data. -/// -/// # Returns -/// `Some(PhpType::Str)` indicating the result is a PHP string. `None` is -/// returned only if the callee reports a type error (not applicable here). -/// -/// # Notes -/// `htmlentities()` currently delegates to the `htmlspecialchars` runtime -/// helper. Both share the same encoding logic and runtime routine. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("htmlentities()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_htmlspecialchars"); // call the shared target-aware runtime helper because htmlentities() currently aliases htmlspecialchars() - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/htmlspecialchars.rs b/src/codegen/builtins/strings/htmlspecialchars.rs deleted file mode 100644 index 2b8a449355..0000000000 --- a/src/codegen/builtins/strings/htmlspecialchars.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Purpose: -//! Emits PHP `htmlspecialchars` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for a `htmlspecialchars` builtin call. -/// -/// Marshals the string/scalar argument in `args[0]` and calls `__rt_htmlspecialchars`, -/// the target-aware runtime helper that converts special characters to HTML entities. -/// Returns `PhpType::Str` as the result type. -/// -/// Arguments: -/// - `args[0]` – the expression to encode -/// -/// Output: -/// - `PhpType::Str` indicating the returned PHP string -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("htmlspecialchars()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_htmlspecialchars"); // call the target-aware runtime helper that converts special characters to HTML entities - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/implode.rs b/src/codegen/builtins/strings/implode.rs deleted file mode 100644 index dfc6407394..0000000000 --- a/src/codegen/builtins/strings/implode.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Purpose: -//! Emits PHP `implode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `implode` builtin call. -/// -/// Compiles PHP `implode($glue, $array)` by evaluating the glue string, -/// then the array, preserving both across architecture-specific ABI registers, -/// and calling the appropriate runtime helper. -/// -/// # Arguments -/// - `args[0]`: glue string expression -/// - `args[1]`: array expression -/// -/// # Return value -/// Returns `Some(PhpType::Str)` on success. -/// -/// # ABI constraints -/// - AArch64: pushes glue (x1/x2) and array pointer (x0) to stack; restores array → x3, glue → x1/x2 before calling `__rt_implode` or `__rt_implode_int` -/// - X86_64: pushes glue (rdi/rsi) and array pointer (rax) to stack; restores array → rdx, glue → rdi/rsi before calling `__rt_implode` or `__rt_implode_int` -/// -/// # Runtime helpers -/// - `__rt_implode`: standard runtime for string/value arrays -/// - `__rt_implode_int`: specialized runtime for int/bool element arrays -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("implode()"); - // implode($glue, $array) - super::args::emit_string_arg(&args[0], emitter, ctx, data); - // -- save glue, evaluate array -- - let (glue_ptr_reg, glue_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, glue_ptr_reg, glue_len_reg); // preserve the glue string while evaluating the indexed array argument - let arr_ty = emit_expr(&args[1], emitter, ctx, data); - if matches!(arr_ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // unwrap a mixed array argument before passing its payload to implode - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // use the unboxed indexed-array payload as the implode array argument - } - Arch::X86_64 => { - emitter.instruction("mov rax, rdi"); // use the unboxed indexed-array payload as the implode array argument - } - } - } - // -- save array pointer, restore glue -- - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the indexed array pointer while restoring the glue string for the runtime call - - let is_int_array = matches!(&arr_ty, PhpType::Array(inner) if matches!(inner.as_ref(), PhpType::Int | PhpType::Bool)); - - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x3"); // restore the indexed array pointer into the runtime array-argument register - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the glue string into the runtime string-argument registers - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "rdx"); // restore the indexed array pointer into the third SysV integer argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the glue string into the first two SysV integer argument registers - } - } - - if is_int_array { - abi::emit_call_label(emitter, "__rt_implode_int"); // join integer array elements with the glue string through the integer-specialized runtime - } else { - abi::emit_call_label(emitter, "__rt_implode"); // join string array elements with the glue string through the standard runtime - } - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/inet.rs b/src/codegen/builtins/strings/inet.rs deleted file mode 100644 index 5c4610d8ab..0000000000 --- a/src/codegen/builtins/strings/inet.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Purpose: -//! Emits PHP `inet_ntop` and `inet_pton` calls. -//! Converts between IPv4 binary strings and dotted-quad presentation strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Both builtins yield `string|false`; a null runtime pointer (invalid input) -//! is boxed as PHP `false`, a successful result as a boxed string. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits codegen for PHP `inet()` string builtin calls. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}()", name)); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // input pointer becomes the first helper argument - emitter.instruction("mov x1, x2"); // input length becomes the second helper argument - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // input pointer becomes the first SysV argument - emitter.instruction("mov rsi, rdx"); // input length becomes the second SysV argument - } - } - let helper = if name == "inet_ntop" { - "__rt_inet_ntop" - } else { - "__rt_inet_pton" - }; - abi::emit_call_label(emitter, helper); - box_string_or_false(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a null pointer becomes PHP `false`, a non-null -/// pointer/length pair becomes a boxed string without copying the buffer. -fn box_string_or_false(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("inet_false"); - let done_label = ctx.next_label("inet_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x1, {}", false_label)); // a null pointer means invalid input - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the string payload across the allocation - emitter.instruction("mov x0, #24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #5"); // heap kind 5 = mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a mixed cell - emitter.instruction("mov x9, #1"); // runtime tag 1 = string - emitter.instruction("str x9, [x0]"); // store the string tag - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // reload the string pointer and length - emitter.instruction("stp x10, x11, [x0, #8]"); // store the string payload words - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // a null pointer means invalid input - emitter.instruction(&format!("jz {}", false_label)); // box false when the input was invalid - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the string payload across the allocation - emitter.instruction("mov rax, 24"); // mixed cells store a tag plus two payload words - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!( // mixed-cell heap-kind word with the x86_64 heap marker - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a mixed cell - emitter.instruction("mov r10, 1"); // runtime tag 1 = string - emitter.instruction("mov QWORD PTR [rax], r10"); // store the string tag - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // reload the string pointer and length - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string pointer - emitter.instruction("mov QWORD PTR [rax + 16], r11"); // store the string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid result - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/strings/intval.rs b/src/codegen/builtins/strings/intval.rs deleted file mode 100644 index 9438f4dde2..0000000000 --- a/src/codegen/builtins/strings/intval.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Purpose: -//! Emits PHP `intval` conversion calls from scalar expressions. -//! Keeps PHP conversion lowering close to string builtins because string parsing is the dominant path. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Conversion behavior must stay aligned with type-checker assumptions for scalar-to-int coercion. - -use crate::codegen::context::Context; -use crate::codegen::context::HeapOwnership; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{emit_expr, expr_result_heap_ownership}; -use crate::codegen::abi; -use crate::parser::ast::{BinOp, Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits code for the PHP `intval()` builtin. -/// -/// Dispatches on the argument type: -/// - `Str`: calls `__rt_str_to_int` to parse the string with PHP cast rules -/// - `Mixed`/`Union`: calls `__rt_mixed_cast_int` for runtime type coercion -/// - `Float`: truncates the floating-point result into the integer register (toward zero), matching -/// the `(int)` cast — without this the raw IEEE-754 bits would be returned as a bogus integer -/// - Other scalar types (`Int`/`Bool`/`Null`): no-op (already in the integer register) -/// -/// Returns `PhpType::Int` unconditionally, matching PHP's `intval()` return type. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("intval()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - match ty { - PhpType::Str => { - // -- convert string to integer -- - abi::emit_call_label(emitter, "__rt_str_to_int"); // parse the current string result through PHP string-to-int cast rules - } - PhpType::Mixed | PhpType::Union(_) => { - // -- coerce a boxed Mixed cell to int per PHP's casting rules -- - let release_arg_after_cast = mixed_arg_result_is_owned(&args[0]); - if release_arg_after_cast { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - abi::emit_call_label(emitter, "__rt_mixed_cast_int"); // dispatch on the runtime cell tag and return the integer payload (or coerced equivalent) - if release_arg_after_cast { - release_preserved_mixed_arg_after_int_cast(emitter); - } - } - PhpType::Float => { - // -- truncate the float result toward zero into the integer register (like the `(int)` - // cast); otherwise the raw IEEE-754 bits would be reinterpreted as a bogus integer -- - abi::emit_float_result_to_int_result(emitter); - } - _ => {} - } - Some(PhpType::Int) -} - -/// Returns true if the expression result is heap-owned and must be preserved -/// across the `__rt_mixed_cast_int` call. -/// -/// Arithmetic binary operations are included because their result may alias -/// argument temporaries that the runtime call could otherwise clobber. -fn mixed_arg_result_is_owned(arg: &Expr) -> bool { - expr_result_heap_ownership(arg) == HeapOwnership::Owned - || matches!( - arg.kind, - ExprKind::BinaryOp { - op: BinOp::Add | BinOp::Sub | BinOp::Mul, - .. - } - ) -} - -/// Restores the preserved `Mixed` argument after a `__rt_mixed_cast_int` call. -/// -/// The integer result was pushed onto the stack before the call to protect it -/// from being clobbered. This function decrefs the original `Mixed` cell and -/// pops the preserved integer back into the result register. -fn release_preserved_mixed_arg_after_int_cast(emitter: &mut Emitter) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, &PhpType::Mixed); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_release_temporary_stack(emitter, 16); -} diff --git a/src/codegen/builtins/strings/ip2long.rs b/src/codegen/builtins/strings/ip2long.rs deleted file mode 100644 index 472e64b719..0000000000 --- a/src/codegen/builtins/strings/ip2long.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Purpose: -//! Emits PHP `ip2long` calls. -//! Parses a dotted-quad IPv4 string into an integer, or PHP false when invalid. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - The `__rt_ip2long` helper returns -1 for an invalid address; that case is -//! boxed as PHP false, a valid result as a boxed integer. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `ip2long()` string builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ip2long()"); - emit_expr(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // string pointer becomes the first helper argument - emitter.instruction("mov x1, x2"); // string length becomes the second helper argument - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // string pointer becomes the first SysV argument - emitter.instruction("mov rsi, rdx"); // string length becomes the second SysV argument - } - } - abi::emit_call_label(emitter, "__rt_ip2long"); - box_ip2long_result(emitter, ctx); - Some(PhpType::Mixed) -} - -/// Boxes the helper result: a -1 sentinel becomes PHP `false`, any other value -/// becomes a boxed integer. -fn box_ip2long_result(emitter: &mut Emitter, ctx: &mut Context) { - let false_label = ctx.next_label("ip2long_false"); - let done_label = ctx.next_label("ip2long_done"); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the helper report an invalid address? - emitter.instruction(&format!("b.lt {}", false_label)); // box PHP false on the -1 sentinel - emitter.instruction("mov x1, x0"); // move the parsed integer into the mixed payload - emitter.instruction("mov x2, #0"); // integer mixed payloads have no high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = integer - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("b {}", done_label)); // skip the false path after a valid parse - emitter.label(&false_label); - emitter.instruction("mov x1, #0"); // false payload = 0 - emitter.instruction("mov x2, #0"); // bool mixed payloads have no high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the helper report an invalid address? - emitter.instruction(&format!("js {}", false_label)); // box PHP false on the -1 sentinel - emitter.instruction("mov rdi, rax"); // move the parsed integer into the mixed payload - emitter.instruction("xor esi, esi"); // integer mixed payloads have no high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = integer - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.instruction(&format!("jmp {}", done_label)); // skip the false path after a valid parse - emitter.label(&false_label); - emitter.instruction("xor edi, edi"); // false payload = 0 - emitter.instruction("xor esi, esi"); // bool mixed payloads have no high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/strings/lcfirst.rs b/src/codegen/builtins/strings/lcfirst.rs deleted file mode 100644 index 1099e46fe0..0000000000 --- a/src/codegen/builtins/strings/lcfirst.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Purpose: -//! Emits PHP `lcfirst` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `lcfirst` builtin call. -/// -/// Emits `args[0]` as a string expression, then copies the result into concat -/// storage and lowercases its first byte in place when that byte is an uppercase -/// ASCII letter (A–Z). Non-ASCII or non-uppercase first bytes are left unchanged. -/// Returns `PhpType::Str` as the result type. -/// -/// - **args**: single expression producing the input string (panics if empty) -/// - **emitter**: target-specific instruction emission -/// - **ctx**: codegen context carrying variable layout and metadata -/// - **data**: data section for relocatable string constants -/// - **returns**: `Some(PhpType::Str)` indicating the builtin produces a string -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("lcfirst()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - // -- copy string then lowercase the first character -- - abi::emit_call_label(emitter, "__rt_strcopy"); // copy the source string into concat storage before mutating its first byte in place - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cbz x2, 1f"); // skip the ASCII-case tweak when lcfirst() receives an empty string - emitter.instruction("ldrb w9, [x1]"); // load the first byte of the copied string so lcfirst() can classify its ASCII case - emitter.instruction("cmp w9, #65"); // compare the copied first byte against 'A' to detect uppercase ASCII input - emitter.instruction("b.lt 1f"); // leave bytes below 'A' unchanged because they are not uppercase ASCII letters - emitter.instruction("cmp w9, #90"); // compare the copied first byte against 'Z' to bound the uppercase ASCII range - emitter.instruction("b.gt 1f"); // leave bytes above 'Z' unchanged because they are not uppercase ASCII letters - emitter.instruction("add w9, w9, #32"); // convert uppercase ASCII to lowercase by adding the standard ASCII case delta - emitter.instruction("strb w9, [x1]"); // store the lowercased first byte back into the copied string in concat storage - emitter.raw("1:"); - } - Arch::X86_64 => { - emitter.instruction("test rdx, rdx"); // skip the ASCII-case tweak when lcfirst() receives an empty string - emitter.instruction("jz 1f"); // leave empty strings unchanged because there is no first byte to lowercase - emitter.instruction("movzx ecx, BYTE PTR [rax]"); // load the first byte of the copied string so lcfirst() can classify its ASCII case - emitter.instruction("cmp cl, 65"); // compare the copied first byte against 'A' to detect uppercase ASCII input - emitter.instruction("jb 1f"); // leave bytes below 'A' unchanged because they are not uppercase ASCII letters - emitter.instruction("cmp cl, 90"); // compare the copied first byte against 'Z' to bound the uppercase ASCII range - emitter.instruction("ja 1f"); // leave bytes above 'Z' unchanged because they are not uppercase ASCII letters - emitter.instruction("add cl, 32"); // convert uppercase ASCII to lowercase by adding the standard ASCII case delta - emitter.instruction("mov BYTE PTR [rax], cl"); // store the lowercased first byte back into the copied string in concat storage - emitter.raw("1:"); - } - } - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/long2ip.rs b/src/codegen/builtins/strings/long2ip.rs deleted file mode 100644 index c6c2648c48..0000000000 --- a/src/codegen/builtins/strings/long2ip.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Purpose: -//! Emits PHP `long2ip` calls. -//! Formats a 32-bit integer as a dotted-quad IPv4 string. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Delegates the formatting to the `__rt_long2ip` runtime helper, which -//! leaves the result string in the standard pointer/length registers. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `long2ip()` string builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("long2ip()"); - emit_expr(&args[0], emitter, ctx, data); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the IP integer into the runtime-helper argument register - } - abi::emit_call_label(emitter, "__rt_long2ip"); - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/ltrim.rs b/src/codegen/builtins/strings/ltrim.rs deleted file mode 100644 index b3b12a0663..0000000000 --- a/src/codegen/builtins/strings/ltrim.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Emits PHP `ltrim` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `ltrim` builtin. -/// -/// Handles two arities: -/// - 1-arg: strips ASCII whitespace from the left of the input string, calling `__rt_ltrim`. -/// - 2-arg: strips characters in the given mask from the left, calling `__rt_ltrim_mask`. -/// For the 2-arg variant, the source string is preserved on the stack while the mask -/// argument is evaluated, then the registers are restored before the call. -/// -/// Returns `PhpType::Str` as the result is always a new allocated string. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ltrim()"); - - if args.len() == 1 { - super::args::emit_string_arg(&args[0], emitter, ctx, data); - // -- strip whitespace from the left -- - abi::emit_call_label(emitter, "__rt_ltrim"); // call the target-aware runtime helper that trims ASCII whitespace from the start of the current string slice - } else { - // -- ltrim with character mask -- - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x1, [sp, #-16]!"); // preserve the source string pointer while the trim-mask expression is evaluated - emitter.instruction("str x2, [sp, #-16]!"); // preserve the source string length while the trim-mask expression is evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the trim-mask pointer into the secondary AArch64 trim-mask argument register pair - emitter.instruction("mov x4, x2"); // move the trim-mask length into the secondary AArch64 trim-mask argument register pair - emitter.instruction("ldr x2, [sp], #16"); // restore the source string length after evaluating the trim-mask expression - emitter.instruction("ldr x1, [sp], #16"); // restore the source string pointer after evaluating the trim-mask expression - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the source string ptr/len while the trim-mask expression is evaluated on x86_64 - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the trim-mask pointer into the secondary x86_64 trim-mask argument register - emitter.instruction("mov rsi, rdx"); // move the trim-mask length into the secondary x86_64 trim-mask argument register - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the source string ptr/len after evaluating the trim-mask expression - } - } - abi::emit_call_label(emitter, "__rt_ltrim_mask"); // call the target-aware runtime helper that trims mask bytes from the start of the current string slice - } - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/md5.rs b/src/codegen/builtins/strings/md5.rs deleted file mode 100644 index 26786dc77d..0000000000 --- a/src/codegen/builtins/strings/md5.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Emits PHP `md5($string, $binary = false)` calls. -//! Routes the data string and the optional `$binary` flag into the `__rt_md5` -//! runtime helper, which hashes through the elephc-crypto staticlib. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - The data string is evaluated first (PHP source order) and preserved on the -//! stack while the `$binary` flag is evaluated, then both are materialised in -//! the `__rt_md5` register contract (data ptr/len + flag in AArch64 x5 / -//! x86_64 r10). -//! - Returned string pointer/length pairs are owned runtime values when the -//! helper allocates. - -use super::hash::emit_binary_flag; -use super::hash_crypto; -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Lowers the PHP `md5($string, $binary = false)` call. -/// -/// Evaluates `args[0]` into the string ABI register pair, preserves it while the -/// optional `$binary` flag (`args[1]`, default `false`) is coerced to a 0/1 -/// integer, materialises the flag in AArch64 `x5` / x86_64 `r10`, publishes the -/// elephc-crypto function pointer, and calls `__rt_md5`. Returns `PhpType::Str`. -/// -/// The returned string pointer/length pair is an owned runtime value; the caller -/// owns it. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("md5()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the data string while evaluating the binary flag - emit_binary_flag(args, 1, emitter, ctx, data); - emitter.instruction("mov x5, x0"); // move the 0/1 binary flag into the runtime argument register on AArch64 - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the data string after evaluating the binary flag - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the data string ptr/len while evaluating the binary flag on x86_64 - emit_binary_flag(args, 1, emitter, ctx, data); - emitter.instruction("mov r10, rax"); // move the 0/1 binary flag into the runtime argument register on x86_64 - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the data string ptr/len after evaluating the binary flag - } - } - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_md5"); // call the target-aware runtime helper that hashes through elephc-crypto and returns the PHP string - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/mod.rs b/src/codegen/builtins/strings/mod.rs deleted file mode 100644 index 1b2f3bfa79..0000000000 --- a/src/codegen/builtins/strings/mod.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Purpose: -//! Dispatches string and byte-oriented PHP builtins to their focused codegen emitters. -//! Keeps the public builtin category surface small while leaf files own lowering details. -//! -//! Called from: -//! - `crate::codegen::builtins::emit_builtin_call()`. -//! -//! Key details: -//! - Dispatcher names must stay aligned with the builtin catalog and signature normalization layer. - -mod addslashes; -mod args; -mod base64_decode; -mod base64_encode; -mod bin2hex; -mod gzcompress; -mod gzdeflate; -mod gzinflate; -mod gzuncompress; -mod chr; -mod inet; -mod ip2long; -mod long2ip; -mod crc32; -mod ctype_alnum; -mod ctype_alpha; -mod ctype_digit; -mod ctype_space; -mod explode; -mod format_args; -mod grapheme_strrev; -mod hash; -mod hash_algos; -mod hash_context; -mod hash_equals; -mod hash_hmac; -pub(crate) mod hash_crypto; -mod hex2bin; -mod html_entity_decode; -mod htmlentities; -mod htmlspecialchars; -mod implode; -mod intval; -mod lcfirst; -mod ltrim; -mod md5; -mod nl2br; -mod number_format; -mod ord; -mod printf; -mod sprintf; -mod vprintf; -mod vsprintf; -mod rawurldecode; -mod rawurlencode; -mod rtrim; -mod sha1; -mod sscanf; -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 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; - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Dispatches a PHP string/byte builtin call to its focused codegen emitter. -/// -/// `name` must be a canonical builtin name from the catalog (case-insensitive lookup -/// is handled by the caller). Returns `Some(PhpType)` with the result type when the -/// builtin is recognized, or `None` if `name` is not a string-category builtin. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "strlen" => strlen::emit(name, args, emitter, ctx, data), - "intval" => intval::emit(name, args, emitter, ctx, data), - "number_format" => number_format::emit(name, args, emitter, ctx, data), - "substr" => substr::emit(name, args, emitter, ctx, data), - "strpos" => strpos::emit(name, args, emitter, ctx, data), - "strrpos" => strrpos::emit(name, args, emitter, ctx, data), - "strstr" => strstr::emit(name, args, emitter, ctx, data), - "strtolower" => strtolower::emit(name, args, emitter, ctx, data), - "strtoupper" => strtoupper::emit(name, args, emitter, ctx, data), - "ucfirst" => ucfirst::emit(name, args, emitter, ctx, data), - "lcfirst" => lcfirst::emit(name, args, emitter, ctx, data), - "trim" => trim::emit(name, args, emitter, ctx, data), - "ltrim" => ltrim::emit(name, args, emitter, ctx, data), - "rtrim" => rtrim::emit(name, args, emitter, ctx, data), - "chop" => rtrim::emit(name, args, emitter, ctx, data), - "str_repeat" => str_repeat::emit(name, args, emitter, ctx, data), - "strrev" => strrev::emit(name, args, emitter, ctx, data), - "grapheme_strrev" => grapheme_strrev::emit(name, args, emitter, ctx, data), - "ord" => ord::emit(name, args, emitter, ctx, data), - "chr" => chr::emit(name, args, emitter, ctx, data), - "strcmp" => strcmp::emit(name, args, emitter, ctx, data), - "strcasecmp" => strcasecmp::emit(name, args, emitter, ctx, data), - "str_contains" => str_contains::emit(name, args, emitter, ctx, data), - "str_starts_with" => str_starts_with::emit(name, args, emitter, ctx, data), - "str_ends_with" => str_ends_with::emit(name, args, emitter, ctx, data), - "str_replace" => str_replace::emit(name, args, emitter, ctx, data), - "explode" => explode::emit(name, args, emitter, ctx, data), - "implode" => implode::emit(name, args, emitter, ctx, data), - "ucwords" => ucwords::emit(name, args, emitter, ctx, data), - "str_ireplace" => str_ireplace::emit(name, args, emitter, ctx, data), - "substr_replace" => substr_replace::emit(name, args, emitter, ctx, data), - "str_pad" => str_pad::emit(name, args, emitter, ctx, data), - "str_split" => str_split::emit(name, args, emitter, ctx, data), - "addslashes" => addslashes::emit(name, args, emitter, ctx, data), - "stripslashes" => stripslashes::emit(name, args, emitter, ctx, data), - "nl2br" => nl2br::emit(name, args, emitter, ctx, data), - "wordwrap" => wordwrap::emit(name, args, emitter, ctx, data), - "bin2hex" => bin2hex::emit(name, args, emitter, ctx, data), - "ip2long" => ip2long::emit(name, args, emitter, ctx, data), - "inet_ntop" | "inet_pton" => inet::emit(name, args, emitter, ctx, data), - "long2ip" => long2ip::emit(name, args, emitter, ctx, data), - "hex2bin" => hex2bin::emit(name, args, emitter, ctx, data), - "htmlspecialchars" => htmlspecialchars::emit(name, args, emitter, ctx, data), - "htmlentities" => htmlentities::emit(name, args, emitter, ctx, data), - "html_entity_decode" => html_entity_decode::emit(name, args, emitter, ctx, data), - "urlencode" => urlencode::emit(name, args, emitter, ctx, data), - "urldecode" => urldecode::emit(name, args, emitter, ctx, data), - "rawurlencode" => rawurlencode::emit(name, args, emitter, ctx, data), - "rawurldecode" => rawurldecode::emit(name, args, emitter, ctx, data), - "base64_encode" => base64_encode::emit(name, args, emitter, ctx, data), - "base64_decode" => base64_decode::emit(name, args, emitter, ctx, data), - "gzcompress" => gzcompress::emit(name, args, emitter, ctx, data), - "gzdeflate" => gzdeflate::emit(name, args, emitter, ctx, data), - "gzinflate" => gzinflate::emit(name, args, emitter, ctx, data), - "gzuncompress" => gzuncompress::emit(name, args, emitter, ctx, data), - "ctype_alpha" => ctype_alpha::emit(name, args, emitter, ctx, data), - "ctype_digit" => ctype_digit::emit(name, args, emitter, ctx, data), - "ctype_alnum" => ctype_alnum::emit(name, args, emitter, ctx, data), - "ctype_space" => ctype_space::emit(name, args, emitter, ctx, data), - "sprintf" => sprintf::emit(name, args, emitter, ctx, data), - "vsprintf" => vsprintf::emit(name, args, emitter, ctx, data), - "md5" => md5::emit(name, args, emitter, ctx, data), - "sha1" => sha1::emit(name, args, emitter, ctx, data), - "crc32" => crc32::emit(name, args, emitter, ctx, data), - "printf" => printf::emit(name, args, emitter, ctx, data), - "vprintf" => vprintf::emit(name, args, emitter, ctx, data), - "hash" => hash::emit(name, args, emitter, ctx, data), - "hash_algos" => hash_algos::emit(name, args, emitter, ctx, data), - "hash_equals" => hash_equals::emit(name, args, emitter, ctx, data), - "hash_hmac" => hash_hmac::emit(name, args, emitter, ctx, data), - "hash_init" => hash_context::emit_init(name, args, emitter, ctx, data), - "hash_update" => hash_context::emit_update(name, args, emitter, ctx, data), - "hash_final" => hash_context::emit_final(name, args, emitter, ctx, data), - "hash_copy" => hash_context::emit_copy(name, args, emitter, ctx, data), - "sscanf" => sscanf::emit(name, args, emitter, ctx, data), - _ => None, - } -} diff --git a/src/codegen/builtins/strings/nl2br.rs b/src/codegen/builtins/strings/nl2br.rs deleted file mode 100644 index 4cfff241af..0000000000 --- a/src/codegen/builtins/strings/nl2br.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Purpose: -//! Emits PHP `nl2br` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for the PHP `nl2br(string)` builtin. -/// -/// Materializes `args[0]` (the input string) in registers per ABI, then calls the -/// target-aware runtime helper `__rt_nl2br` which allocates and returns a new PHP string -/// with every newline replaced by `
\n`. The returned string pointer/length is an -/// owned runtime value; the caller is responsible for releasing it. -/// -/// # Arguments -/// - `args[0]`: the input string expression (other parameters are currently unsupported). -/// - `ctx`: carries variable layout, ownership state, and class metadata through codegen. -/// - `data`: receives any data-section allocations required by the call sequence. -/// -/// # Output -/// Always returns `Some(PhpType::Str)` — the runtime helper produces an owned PHP string. -/// -/// # ABI -/// `emit_string_arg` materializes the input string in `x1`/`x2` (pointer/length) or equivalent -/// registers per target ABI; `__rt_nl2br` returns the result string pointer in `x1`, -/// length in `x2`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("nl2br()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_nl2br"); // call the target-aware runtime helper that expands newlines into HTML break tags - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/number_format.rs b/src/codegen/builtins/strings/number_format.rs deleted file mode 100644 index c6d8376928..0000000000 --- a/src/codegen/builtins/strings/number_format.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Purpose: -//! Emits PHP `number_format` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `number_format` builtin call. -/// -/// Prepares arguments on the stack in reverse order, then pops them into ABI registers -/// and calls `__rt_number_format`. Handles all four parameters: -/// -/// - `_name`: Unused name for dispatcher compatibility. -/// - `args[0]`: Numeric value as float (passed via `push_float_arg`). -/// - `args[1]`: Decimal count (default 0 when absent). -/// - `args[2]`: Decimal separator byte (default `.`, 46 ASCII). -/// - `args[3]`: Thousands separator byte (default `,`, 44 ASCII). -/// -/// Emits architecture-specific assembly for x86_64 and AArch64 using stacked -/// arguments and the SysV / AArch64 calling conventions respectively. -/// Returns `Some(PhpType::Str)` as the runtime helper allocates a PHP string. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("number_format()"); - // -- prepare the numeric value as a float -- - super::args::push_float_arg(&args[0], emitter, ctx, data); - - // -- prepare decimals argument -- - if args.len() >= 2 { - super::args::push_int_arg(&args[1], emitter, ctx, data); - } else { - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "rax", 0); // materialize the default zero-decimal count in the active x86_64 integer result register - } - Arch::AArch64 => { - abi::emit_load_int_immediate(emitter, "x0", 0); // materialize the default zero-decimal count in the active AArch64 integer result register - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the default decimal count while the separator arguments are evaluated - } - - // -- prepare decimal point character -- - if args.len() >= 3 { - super::args::emit_string_arg(&args[2], emitter, ctx, data); - match emitter.target.arch { - Arch::X86_64 => { - emitter.instruction("movzx eax, BYTE PTR [rax]"); // load the first byte of the decimal-separator string into the x86_64 integer result register - } - Arch::AArch64 => { - emitter.instruction("ldrb w0, [x1]"); // load the first byte of the decimal-separator string into the AArch64 integer result register - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the decimal-separator byte while the thousands-separator argument is evaluated - } else { - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "rax", 46); // materialize the default '.' decimal separator in the active x86_64 integer result register - } - Arch::AArch64 => { - abi::emit_load_int_immediate(emitter, "x0", 46); // materialize the default '.' decimal separator in the active AArch64 integer result register - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the default decimal separator while the thousands-separator argument is evaluated - } - - // -- prepare thousands separator character -- - if args.len() >= 4 { - super::args::emit_string_arg(&args[3], emitter, ctx, data); - match emitter.target.arch { - Arch::X86_64 => { - let use_zero = ctx.next_label("nf_use_zero"); - let done = ctx.next_label("nf_sep_done"); - emitter.instruction("test rdx, rdx"); // check whether the thousands-separator string is empty before dereferencing its first byte on x86_64 - emitter.instruction(&format!("jz {}", use_zero)); // use the no-separator sentinel when the thousands-separator string length is zero - emitter.instruction("movzx eax, BYTE PTR [rax]"); // load the first byte of the non-empty thousands-separator string into the x86_64 integer result register - emitter.instruction(&format!("jmp {}", done)); // skip the empty-string fallback once the thousands-separator byte has been loaded - emitter.label(&use_zero); - abi::emit_load_int_immediate(emitter, "rax", 0); // materialize the no-separator sentinel when the thousands-separator string is empty - emitter.label(&done); - } - Arch::AArch64 => { - let use_zero = ctx.next_label("nf_use_zero"); - let done = ctx.next_label("nf_sep_done"); - emitter.instruction(&format!("cbz x2, {}", use_zero)); // use the no-separator sentinel when the thousands-separator string length is zero on AArch64 - emitter.instruction("ldrb w0, [x1]"); // load the first byte of the non-empty thousands-separator string into the AArch64 integer result register - emitter.instruction(&format!("b {}", done)); // skip the empty-string fallback once the thousands-separator byte has been loaded - emitter.label(&use_zero); - abi::emit_load_int_immediate(emitter, "x0", 0); // materialize the no-separator sentinel when the thousands-separator string is empty - emitter.label(&done); - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the thousands-separator byte or no-separator sentinel until the runtime call is assembled - } else { - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "rax", 44); // materialize the default ',' thousands separator in the active x86_64 integer result register - } - Arch::AArch64 => { - abi::emit_load_int_immediate(emitter, "x0", 44); // materialize the default ',' thousands separator in the active AArch64 integer result register - } - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the default thousands separator until the runtime call is assembled - } - - // -- pop all args from stack into registers and call runtime -- - match emitter.target.arch { - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "rdx"); // restore the thousands-separator byte or no-separator sentinel into the third SysV runtime argument register - abi::emit_pop_reg(emitter, "rsi"); // restore the decimal-separator byte into the second SysV runtime argument register - abi::emit_pop_reg(emitter, "rdi"); // restore the decimal-count argument into the first SysV runtime argument register - abi::emit_pop_float_reg(emitter, "xmm0"); // restore the floating number_format() input into the first SysV floating-point runtime argument register - } - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x3"); // restore the thousands-separator byte or no-separator sentinel into the fourth AArch64 runtime argument register - abi::emit_pop_reg(emitter, "x2"); // restore the decimal-separator byte into the third AArch64 runtime argument register - abi::emit_pop_reg(emitter, "x1"); // restore the decimal-count argument into the second AArch64 runtime argument register - abi::emit_pop_float_reg(emitter, "d0"); // restore the floating number_format() input into the first AArch64 floating-point runtime argument register - } - } - abi::emit_call_label(emitter, "__rt_number_format"); // call the target-aware number_format() runtime helper to produce the formatted string - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/ord.rs b/src/codegen/builtins/strings/ord.rs deleted file mode 100644 index adbf9260a7..0000000000 --- a/src/codegen/builtins/strings/ord.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Purpose: -//! Emits PHP `ord` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `ord()` builtin, which returns the ASCII/UTF-8 code point -/// of the first character in a string argument. -/// -/// # Arguments -/// - `_name`: Unused, always "ord" (kept for interface uniformity with other builtins). -/// - `args`: Single argument — the string to extract the first code point from. -/// - `emitter`: Target-aware instruction emitter. -/// - `ctx`: Codegen context providing label generation and architecture info. -/// - `data`: Data section for relocations (unused by this builtin). -/// -/// # Returns -/// `Some(PhpType::Int)` — the numeric code point of the first character. -/// Returns 0 for empty strings (matching PHP behavior). -/// -/// # Architecture handling -/// - **AArch64**: Expects string pointer in `x1`, length in `x2`, returns result in `x0`. -/// - **x86_64**: Expects string pointer in `rax`, length in `rdx`, returns result in `eax`. -/// - Both targets set the integer register to 0 when the string is empty. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ord()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - let empty_label = ctx.next_label("ord_empty"); - let done_label = ctx.next_label("ord_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x2, {empty_label}")); // return zero when ord() receives an empty string - emitter.instruction("ldrb w0, [x1]"); // load the first byte of the string as an unsigned integer code point - emitter.instruction(&format!("b {done_label}")); // skip the empty-string fallback after loading the first byte - } - Arch::X86_64 => { - emitter.instruction("test rdx, rdx"); // return zero when ord() receives an empty string - emitter.instruction(&format!("jz {empty_label}")); // branch to the empty-string fallback when the string length is zero - emitter.instruction("movzx eax, BYTE PTR [rax]"); // load the first byte of the string as an unsigned integer code point - emitter.instruction(&format!("jmp {done_label}")); // skip the empty-string fallback after loading the first byte - } - } - emitter.label(&empty_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // return zero when ord() receives an empty string - } - Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // return zero when ord() receives an empty string - } - } - emitter.label(&done_label); - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/strings/printf.rs b/src/codegen/builtins/strings/printf.rs deleted file mode 100644 index ff41322976..0000000000 --- a/src/codegen/builtins/strings/printf.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Purpose: -//! Emits PHP `printf` string formatting calls (`sprintf` + write to stdout). -//! Marshals string/scalar arguments into the shared sprintf runtime helper, then writes the -//! formatted bytes to stdout. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. -//! - Argument marshalling (including coercing each argument to its conversion specifier's type for -//! literal formats) is shared with `sprintf` via `super::format_args`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `printf` builtin call. -/// -/// Implements `printf` as `sprintf` + `echo`: delegates argument marshalling and the -/// `__rt_sprintf` call to [`super::format_args::emit_format_and_call`], then writes the formatted -/// string (returned in the standard string-result registers) to stdout via the target write -/// syscall and returns the byte count written. -/// -/// # Arguments -/// - `_name`: unused for `printf`; required by the dispatcher signature -/// - `args`: `[format_string, arg1, arg2, ...]` — format string is always `args[0]` -/// - `emitter`: target-aware instruction emitter -/// - `ctx`: codegen context (variable layout, ownership state, class metadata) -/// - `data`: data section for relocations and static data -/// -/// # Returns -/// `Some(PhpType::Int)` — always returns the character count written (PHP printf semantics). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("printf()"); - - // printf = sprintf + echo: marshal args and format the string (result ptr/len in the - // standard string-result registers: x1/x2 on ARM64, rax/rdx on x86_64). - super::format_args::emit_format_and_call(args, emitter, ctx, data); - - // -- write result to stdout -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // fd = stdout - emitter.syscall(4); - emitter.instruction("mov x0, x2"); // return char count - } - Arch::X86_64 => { - emitter.instruction("mov r8, rdx"); // preserve the byte count in r8; the syscall instruction clobbers rcx - emitter.instruction("mov rsi, rax"); // move the formatted string pointer into the SysV write buffer register - emitter.instruction("mov rdx, r8"); // move the formatted string length into the SysV write byte-count register - emitter.instruction("mov edi, 1"); // fd = stdout for the Linux x86_64 write syscall - emitter.instruction("mov eax, 1"); // syscall 1 = write on Linux x86_64 - emitter.instruction("syscall"); // write the formatted bytes to stdout through the Linux x86_64 syscall ABI - emitter.instruction("mov rax, r8"); // return the byte count (rcx was destroyed by syscall) - } - } - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/strings/rawurldecode.rs b/src/codegen/builtins/strings/rawurldecode.rs deleted file mode 100644 index f90753b9ff..0000000000 --- a/src/codegen/builtins/strings/rawurldecode.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Purpose: -//! Emits PHP `rawurldecode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `rawurldecode` builtin call. -/// -/// Arguments: -/// - `args[0]`: the string to decode (evaluated and pushed as the runtime argument) -/// -/// Behavior: -/// - Emits `args[0]` expression to obtain the source string. -/// - Calls `__rt_urldecode` runtime helper which percent-decodes the string. -/// - The helper allocates and returns a new PHP-owned string; the caller receives -/// ownership of the returned value. -/// -/// Returns: -/// - `PhpType::Str` indicating the result is a PHP string type. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rawurldecode()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_urldecode"); // rawurldecode() currently reuses the shared target-aware percent-decoder runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/rawurlencode.rs b/src/codegen/builtins/strings/rawurlencode.rs deleted file mode 100644 index 639d8c7c1f..0000000000 --- a/src/codegen/builtins/strings/rawurlencode.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Purpose: -//! Emits PHP `rawurlencode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code to call `__rt_rawurlencode`, which encodes a PHP string using RFC 3986 -/// percent-encoding (`%XX` for unescaped characters, no `+` for spaces). -/// -/// Arguments: -/// - `args[0]`: the expression producing the string to encode -/// - `emitter`: instruction emission context -/// - `ctx`: variable layout and ownership state -/// - `data`: runtime data section for relocations and string constants -/// -/// Returns `Some(PhpType::Str)` — the helper allocates and returns the encoded string, -/// which must be treated as an owned runtime value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rawurlencode()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_rawurlencode"); // call the target-aware runtime helper that percent-encodes the current string with RFC 3986 rules - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/rtrim.rs b/src/codegen/builtins/strings/rtrim.rs deleted file mode 100644 index 7dbf6cf8de..0000000000 --- a/src/codegen/builtins/strings/rtrim.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Purpose: -//! Emits PHP `rtrim` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `rtrim(chars?)` builtin. -/// -/// Dispatches to `__rt_rtrim` (1-arg: strip ASCII whitespace from the right) -/// or `__rt_rtrim_mask` (2-arg: strip each character in `chars` from the right). -/// -/// # Arguments -/// - `_name`: Unused; present to match the builtin emitter signature. -/// - `args`: Either one argument (string to trim) or two (string + character mask). -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and class metadata. -/// - `data`: Writable data section for string literals. -/// -/// # Returns -/// Always returns `Some(PhpType::Str)`; the caller does not need to handle null. -/// -/// # ABI / Register Usage -/// Two-argument calls preserve the first string's ptr/len pair while evaluating -/// the second argument expression, then load both into the callee parameter -/// registers. AArch64 uses `x1`/`x2` for the first string and `x3`/`x4` for -/// the mask; x86_64 uses `rdi`/`rdx` and `rdi`/`rsi` respectively via a -/// push/pop register-pair protocol. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("rtrim()"); - - if args.len() == 1 { - super::args::emit_string_arg(&args[0], emitter, ctx, data); - // -- strip whitespace from the right -- - abi::emit_call_label(emitter, "__rt_rtrim"); // call the target-aware runtime helper that trims ASCII whitespace from the end of the current string slice - } else { - // -- rtrim with character mask -- - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x1, [sp, #-16]!"); // preserve the source string pointer while the trim-mask expression is evaluated - emitter.instruction("str x2, [sp, #-16]!"); // preserve the source string length while the trim-mask expression is evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the trim-mask pointer into the secondary AArch64 trim-mask argument register pair - emitter.instruction("mov x4, x2"); // move the trim-mask length into the secondary AArch64 trim-mask argument register pair - emitter.instruction("ldr x2, [sp], #16"); // restore the source string length after evaluating the trim-mask expression - emitter.instruction("ldr x1, [sp], #16"); // restore the source string pointer after evaluating the trim-mask expression - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the source string ptr/len while the trim-mask expression is evaluated on x86_64 - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the trim-mask pointer into the secondary x86_64 trim-mask argument register - emitter.instruction("mov rsi, rdx"); // move the trim-mask length into the secondary x86_64 trim-mask argument register - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the source string ptr/len after evaluating the trim-mask expression - } - } - abi::emit_call_label(emitter, "__rt_rtrim_mask"); // call the target-aware runtime helper that trims mask bytes from the end of the current string slice - } - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/sha1.rs b/src/codegen/builtins/strings/sha1.rs deleted file mode 100644 index 2886e3b10f..0000000000 --- a/src/codegen/builtins/strings/sha1.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Emits PHP `sha1($string, $binary = false)` calls. -//! Routes the data string and the optional `$binary` flag into the `__rt_sha1` -//! runtime helper, which hashes through the elephc-crypto staticlib. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - The data string is evaluated first (PHP source order) and preserved on the -//! stack while the `$binary` flag is evaluated, then both are materialised in -//! the `__rt_sha1` register contract (data ptr/len + flag in AArch64 x5 / -//! x86_64 r10). -//! - Returned string pointer/length pairs are owned runtime values when the -//! helper allocates. - -use super::hash::emit_binary_flag; -use super::hash_crypto; -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Lowers the PHP `sha1($string, $binary = false)` call. -/// -/// Evaluates `args[0]` into the string ABI register pair, preserves it while the -/// optional `$binary` flag (`args[1]`, default `false`) is coerced to a 0/1 -/// integer, materialises the flag in AArch64 `x5` / x86_64 `r10`, publishes the -/// elephc-crypto function pointer, and calls `__rt_sha1`. Returns `PhpType::Str`. -/// -/// The returned string pointer/length pair is an owned runtime value; the caller -/// owns it. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sha1()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the data string while evaluating the binary flag - emit_binary_flag(args, 1, emitter, ctx, data); - emitter.instruction("mov x5, x0"); // move the 0/1 binary flag into the runtime argument register on AArch64 - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the data string after evaluating the binary flag - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the data string ptr/len while evaluating the binary flag on x86_64 - emit_binary_flag(args, 1, emitter, ctx, data); - emitter.instruction("mov r10, rax"); // move the 0/1 binary flag into the runtime argument register on x86_64 - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the data string ptr/len after evaluating the binary flag - } - } - hash_crypto::publish_elephc_crypto_function_pointers(emitter); - abi::emit_call_label(emitter, "__rt_sha1"); // call the target-aware runtime helper that hashes through elephc-crypto and returns the PHP string - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/sprintf.rs b/src/codegen/builtins/strings/sprintf.rs deleted file mode 100644 index 8da64851df..0000000000 --- a/src/codegen/builtins/strings/sprintf.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `sprintf` string formatting calls. -//! Marshals string/scalar arguments into the shared sprintf runtime helper that allocates the -//! returned PHP string. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. -//! - Argument marshalling (including coercing each argument to its conversion specifier's type for -//! literal formats) lives in `super::format_args`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `sprintf` builtin call. -/// -/// Delegates argument marshalling and the `__rt_sprintf` call to -/// [`super::format_args::emit_format_and_call`], which pushes each value argument as a 16-byte -/// tagged record (coercing to the conversion specifier's type for literal formats), evaluates the -/// format string, and invokes the runtime helper. -/// -/// # Arguments -/// * `_name` - Unused; matches the builtin dispatch signature. -/// * `args[0]` - Format string expression. -/// * `args[1..]` - Values to substitute into the format string. -/// -/// # Returns -/// `Some(PhpType::Str)` — caller owns the returned string pointer/length. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sprintf()"); - super::format_args::emit_format_and_call(args, emitter, ctx, data); - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/sscanf.rs b/src/codegen/builtins/strings/sscanf.rs deleted file mode 100644 index 4feb431457..0000000000 --- a/src/codegen/builtins/strings/sscanf.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Purpose: -//! Emits PHP `sscanf` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `sscanf` builtin call. -/// -/// `sscanf($string, $format)` parses the input string according to a format specifier -/// and returns an array of matched values as strings. This emitter evaluates the input -/// string first, then the format string, before invoking the `__rt_sscanf` runtime helper. -/// -/// Arguments: -/// - `args[0]`: the input string to parse (string pointer in x1/rax, length in x2/rdx) -/// - `args[1]`: the format specifier string (string pointer in x1/rax, length in x2/rdx) -/// -/// ABI behavior: -/// - AArch64: saves input string to stack via `stp x1, x2, [sp, #-16]!`, emits format args into x3/x4, restores input via `ldp x1, x2, [sp], #16` -/// - X86_64: saves input string via `push_reg_pair` to stack, emits format args into rdi/rsi, restores input via `pop_reg_pair` -/// -/// Returns: `Some(PhpType::Array(Box::new(PhpType::Str)))` indicating an array of strings. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sscanf()"); - // sscanf($string, $format) → returns array of matched values as strings - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the input string while the format string expression is evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the format pointer into the secondary runtime string-argument pair - emitter.instruction("mov x4, x2"); // move the format length into the secondary runtime string-argument pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the input string into the primary runtime string-argument pair - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push the input string while the format string expression is evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the format pointer into the secondary x86_64 runtime string-argument pair - emitter.instruction("mov rsi, rdx"); // move the format length into the secondary x86_64 runtime string-argument pair - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the input string into the primary x86_64 runtime string-argument pair - } - } - abi::emit_call_label(emitter, "__rt_sscanf"); // parse the input string according to the format string through the target-aware runtime helper - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/strings/str_contains.rs b/src/codegen/builtins/strings/str_contains.rs deleted file mode 100644 index 9614766f9f..0000000000 --- a/src/codegen/builtins/strings/str_contains.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Purpose: -//! Emits PHP `str_contains` string search or comparison calls. -//! Handles string pointer/length arguments and boxes false-or-position results when PHP requires mixed output. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Return values must distinguish numeric position zero from PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use super::args::emit_string_arg; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `str_contains(haystack, needle)` builtin call. -/// -/// Saves the haystack pointer/length before evaluating the needle, then calls -/// the shared `__rt_strpos` runtime helper and normalizes the signed position result -/// to a PHP boolean (true if needle is found at any position including 0). -/// -/// # Arguments -/// - `args[0]`: haystack string expression -/// - `args[1]`: needle string expression -/// -/// # Returns -/// `PhpType::Bool` — always returns a boolean regardless of whether the needle was -/// found at position 0 or not found at all, distinguishing PHP false from integer 0. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("str_contains()"); - emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push haystack ptr and length onto the stack while evaluating the needle string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the needle pointer into the third string-helper argument register - emitter.instruction("mov x4, x2"); // move the needle length into the fourth string-helper argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the haystack pointer and length after evaluating the needle - abi::emit_call_label(emitter, "__rt_strpos"); // search the haystack for the needle through the shared runtime helper - emitter.instruction("cmp x0, #0"); // check whether strpos() returned a non-negative match position - emitter.instruction("cset x0, ge"); // normalize the signed strpos() result into a PHP boolean integer - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the haystack pointer and length while evaluating the needle string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // move the needle length into the fourth SysV string-helper argument register - emitter.instruction("mov rdx, rax"); // move the needle pointer into the third SysV string-helper argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the haystack pointer and length into the first two SysV helper argument registers - abi::emit_call_label(emitter, "__rt_strpos"); // search the haystack for the needle through the shared runtime helper - emitter.instruction("cmp rax, 0"); // check whether strpos() returned a non-negative match position - emitter.instruction("setge al"); // normalize the signed strpos() result into the low boolean byte - emitter.instruction("movzx eax, al"); // widen the normalized boolean byte into the integer result register - } - } - - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/strings/str_ends_with.rs b/src/codegen/builtins/strings/str_ends_with.rs deleted file mode 100644 index da40158546..0000000000 --- a/src/codegen/builtins/strings/str_ends_with.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Purpose: -//! Emits PHP `str_ends_with` string search or comparison calls. -//! Handles string pointer/length arguments and boxes false-or-position results when PHP requires mixed output. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Return values must distinguish numeric position zero from PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use super::args::emit_string_arg; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `str_ends_with` builtin call. -/// -/// Arguments: -/// - `args[0]`: haystack string (AArch64: pointer in x1, length in x2; X86_64: pointer in rdi, length in rdx) -/// - `args[1]`: suffix string to search for at the haystack end -/// -/// ABI behavior: -/// - AArch64: pushes haystack to stack, evaluates suffix into x3/x4, restores haystack from stack into x1/x2 -/// - X86_64: saves haystack in rax/rdx, evaluates suffix into rcx/rdx, pops haystack into rdi/rsi -/// - Calls `__rt_str_ends_with` runtime helper that returns position or false -/// -/// Returns: `PhpType::Bool` — PHP false when suffix is not found, otherwise a truthy position value -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("str_ends_with()"); - emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the haystack pointer and length while evaluating the suffix string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the suffix pointer into the third string-helper argument register - emitter.instruction("mov x4, x2"); // move the suffix length into the fourth string-helper argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the haystack pointer and length after evaluating the suffix - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the haystack pointer and length while evaluating the suffix string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // move the suffix length into the fourth SysV string-helper argument register - emitter.instruction("mov rdx, rax"); // move the suffix pointer into the third SysV string-helper argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the haystack pointer and length into the first two SysV helper argument registers - } - } - abi::emit_call_label(emitter, "__rt_str_ends_with"); // check whether the haystack ends with the provided suffix through the target-aware runtime helper - - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/strings/str_ireplace.rs b/src/codegen/builtins/strings/str_ireplace.rs deleted file mode 100644 index f673645d2c..0000000000 --- a/src/codegen/builtins/strings/str_ireplace.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Purpose: -//! Emits PHP `str_ireplace` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP's `str_ireplace` builtin, which performs case-insensitive -/// string replacement across all occurrences. -/// -/// # Arguments -/// * `_name` — the builtin function name (unused, dispatch already happened) -/// * `args` — `[search, replacement, subject]` expressions to evaluate -/// * `emitter` — target-aware assembly emitter -/// * `ctx` — codegen context with variable layout and class metadata -/// * `data` — data section for relocatable constants and string literals -/// -/// # Returns -/// `Some(PhpType::Str)` indicating the result is a PHP string. -/// -/// # ABI details -/// Arguments are passed to `__rt_str_ireplace` via register pairs: x1/x2 or -/// rax/rdx hold pointer/length for each string argument in order (search, -/// replacement, subject). ARM64 uses x1,x2 and x5,x6 for the first two and -/// third args respectively; x86_64 uses rdi,rsi and rcx,r8. The helper -/// allocates and returns a new PHP string; callers must treat the return -/// pointer/length as an owned runtime value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("str_ireplace()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the search string while evaluating the replacement and subject strings - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the replacement string while evaluating the subject string - super::args::emit_string_arg(&args[2], emitter, ctx, data); - emitter.instruction("mov x5, x1"); // move the subject pointer into the third runtime string-argument pair - emitter.instruction("mov x6, x2"); // move the subject length into the third runtime string-argument pair - emitter.instruction("ldp x3, x4, [sp], #16"); // restore the replacement string into the secondary runtime string-argument pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the search string into the primary runtime string-argument pair - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the search string while evaluating the replacement and subject strings - super::args::emit_string_arg(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the replacement string while evaluating the subject string - super::args::emit_string_arg(&args[2], emitter, ctx, data); - emitter.instruction("mov rcx, rax"); // move the subject pointer into the third x86_64 runtime string-argument pair - emitter.instruction("mov r8, rdx"); // move the subject length into the third x86_64 runtime string-argument pair - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the replacement string into the secondary x86_64 runtime string-argument pair - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the search string into the primary x86_64 string-helper input registers - } - } - abi::emit_call_label(emitter, "__rt_str_ireplace"); // replace every search-string occurrence case-insensitively through the target-aware runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/str_pad.rs b/src/codegen/builtins/strings/str_pad.rs deleted file mode 100644 index 0bfdfc5a13..0000000000 --- a/src/codegen/builtins/strings/str_pad.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Purpose: -//! Emits PHP `str_pad` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `str_pad(input, target_length, pad_string, pad_type)` builtin. -/// -/// Evaluates all arguments in source order, preserving registers across argument -/// evaluation using a stack-based save/restore pattern. Defaults to a single space -/// for `pad_string` and `STR_PAD_RIGHT` (1) for `pad_type` when those arguments are -/// omitted. Calls the target-aware `__rt_str_pad` runtime helper and returns a PHP string. -/// -/// # Arguments -/// * `_name` — builtin name (unused, dispatch is already done) -/// * `args` — `[input, target_length, pad_string?, pad_type?]` -/// * `emitter` — assembly emitter -/// * `ctx` — codegen context (types, scope) -/// * `data` — data section for string literals -/// -/// # Returns -/// `Some(PhpType::Str)` — the padded result string -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("str_pad()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the input string while evaluating the target length and optional pad arguments - super::args::push_int_arg(&args[1], emitter, ctx, data); - if args.len() >= 3 { - super::args::emit_string_arg(&args[2], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the requested pad string while evaluating the optional pad type - } else { - let (label, len) = data.add_string(b" "); - abi::emit_symbol_address(emitter, "x1", &label); // materialize the default single-space pad string when the third argument is omitted - abi::emit_load_int_immediate(emitter, "x2", len as i64); // materialize the default single-space pad-string length - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the synthesized default pad string while evaluating the optional pad type - } - if args.len() >= 4 { - super::args::emit_int_arg(&args[3], emitter, ctx, data); - emitter.instruction("mov x7, x0"); // move the requested pad type into the extra AArch64 runtime argument register - } else { - emitter.instruction("mov x7, #1"); // default to STR_PAD_RIGHT when the fourth argument is omitted - } - emitter.instruction("ldp x3, x4, [sp], #16"); // restore the pad string into the secondary AArch64 string-helper argument pair - emitter.instruction("ldr x5, [sp], #16"); // restore the requested target length into the scalar runtime argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the input string into the primary AArch64 string-helper argument pair - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the input string while evaluating the target length and optional pad arguments - super::args::push_int_arg(&args[1], emitter, ctx, data); - if args.len() >= 3 { - super::args::emit_string_arg(&args[2], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the requested pad string while evaluating the optional pad type - } else { - let (label, len) = data.add_string(b" "); - abi::emit_symbol_address(emitter, "rax", &label); // materialize the default single-space pad string when the third argument is omitted - abi::emit_load_int_immediate(emitter, "rdx", len as i64); // materialize the default single-space pad-string length - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the synthesized default pad string while evaluating the optional pad type - } - if args.len() >= 4 { - super::args::emit_int_arg(&args[3], emitter, ctx, data); - emitter.instruction("mov r8, rax"); // move the requested pad type into the extra x86_64 runtime argument register - } else { - emitter.instruction("mov r8, 1"); // default to STR_PAD_RIGHT when the fourth argument is omitted - } - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the pad string into the secondary x86_64 string-helper argument pair - abi::emit_pop_reg(emitter, "rcx"); // restore the requested target length into the scalar runtime argument register - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the input string into the primary x86_64 string-helper input registers - } - } - abi::emit_call_label(emitter, "__rt_str_pad"); // pad the input string to the requested width through the target-aware runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/str_repeat.rs b/src/codegen/builtins/strings/str_repeat.rs deleted file mode 100644 index 0bac775960..0000000000 --- a/src/codegen/builtins/strings/str_repeat.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Purpose: -//! Emits PHP `str_repeat` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `str_repeat` builtin call. -/// -/// Marshals the string operand (args[0]) and integer repeat count (args[1]) into -/// platform-specific argument registers, then calls `__rt_str_repeat` to produce -/// a repeated PHP string. Returns `PhpType::Str` indicating the result is a PHP string. -/// -/// # Arguments -/// - `_name`: Ignored; present for dispatcher signature consistency. -/// - `args[0]`: The string to repeat. -/// - `args[1]`: The integer repeat count. -/// - `emitter`: Target-aware assembly emitter; receives register allocations and instructions. -/// - `ctx`: Codegen context carrying variable layout and function metadata. -/// - `data`: Data section for relocatable literals and runtime symbols. -/// -/// # Register usage -/// - AArch64: string ptr/len in x1/x2, repeat count in x3; result ptr/len returned in x1/x2. -/// - x86_64: string ptr/len in rax/rdx, repeat count in rdi; result ptr/len returned in x1/x2. -/// -/// # Side effects -/// - Caller-saves registers are clobbered by the runtime helper. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("str_repeat()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - // -- save string, evaluate repeat count -- - let (str_ptr_reg, str_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, str_ptr_reg, str_len_reg); // preserve the source string while the repeat-count expression is evaluated - super::args::emit_int_arg(&args[1], emitter, ctx, data); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, x0"); // move the repeat count into the third AArch64 string-helper argument register - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the source string into the AArch64 runtime string-argument registers - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the repeat count into the extra x86_64 runtime argument register used by str_repeat() - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the source string into the standard x86_64 string input registers expected by string helpers - } - } - - abi::emit_call_label(emitter, "__rt_str_repeat"); // call the target-aware runtime helper that repeats the source string into concat storage - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/str_replace.rs b/src/codegen/builtins/strings/str_replace.rs deleted file mode 100644 index 629d52744f..0000000000 --- a/src/codegen/builtins/strings/str_replace.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Purpose: -//! Emits PHP `str_replace` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for the PHP `str_replace(search, replacement, subject)` builtin call. -/// -/// `args[0]` = search string, `args[1]` = replacement string, `args[2]` = subject string. -/// Each string argument is emitted as a pointer/length pair in ABI registers. -/// Stack-based preservation pattern: search is saved first, then replacement, then subject -/// is evaluated; registers are restored so the runtime helper receives search in the primary -/// pair, replacement in the secondary pair, and subject in the third pair. -/// Calls `__rt_str_replace` and returns `PhpType::Str`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("str_replace()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the search string while evaluating the replacement and subject strings - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the replacement string while evaluating the subject string - super::args::emit_string_arg(&args[2], emitter, ctx, data); - emitter.instruction("mov x5, x1"); // move the subject pointer into the third runtime string-argument pair - emitter.instruction("mov x6, x2"); // move the subject length into the third runtime string-argument pair - emitter.instruction("ldp x3, x4, [sp], #16"); // restore the replacement string into the secondary runtime string-argument pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the search string into the primary runtime string-argument pair - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the search string while evaluating the replacement and subject strings - super::args::emit_string_arg(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the replacement string while evaluating the subject string - super::args::emit_string_arg(&args[2], emitter, ctx, data); - emitter.instruction("mov rcx, rax"); // move the subject pointer into the third x86_64 runtime string-argument pair - emitter.instruction("mov r8, rdx"); // move the subject length into the third x86_64 runtime string-argument pair - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the replacement string into the secondary x86_64 runtime string-argument pair - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the search string into the primary x86_64 string-helper input registers - } - } - abi::emit_call_label(emitter, "__rt_str_replace"); // replace every search-string occurrence inside the subject through the target-aware runtime helper - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/str_split.rs b/src/codegen/builtins/strings/str_split.rs deleted file mode 100644 index add9ac3329..0000000000 --- a/src/codegen/builtins/strings/str_split.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Purpose: -//! Emits PHP `str_split` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `str_split` PHP builtin as a target-aware runtime call. -/// -/// Loads the source string into string argument registers (`x1`/`x2` on AArch64, `rax`/`rdx` on x86_64), -/// evaluates the optional chunk-length expression (defaulting to 1), preserves the string registers -/// across the chunk-length evaluation, then calls `__rt_str_split` to produce a PHP array of string -/// chunks. -/// -/// # Inputs -/// - `args[0]`: source string expression -/// - `args[1]` (optional): integer chunk length, defaults to 1 -/// -/// # Outputs -/// - Returns `Some(PhpType::Array(Box::new(PhpType::Str)))` representing the PHP array of string chunks. -/// The runtime helper allocates the returned array and its string elements. -/// -/// # ABI details -/// - The source string pointer/length must survive the optional chunk-length evaluation, so the -/// string registers are saved to the stack on entry and restored before the runtime call. -/// - AArch64: source string in `x1`/`x2`, chunk length in `x3`, helper reads `x1`/`x2`/`x3`. -/// - x86_64: source string in `rax`/`rdx`, chunk length in `rdi`, helper reads `rax`/`rdx`/`rdi`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("str_split()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the source string while evaluating the optional chunk-length expression - if args.len() >= 2 { - super::args::emit_int_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x0"); // move the requested chunk length into the AArch64 helper argument register - } else { - emitter.instruction("mov x3, #1"); // default to one-byte chunks when str_split() omits the chunk length - } - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the source string after evaluating the optional chunk-length expression - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the source string while evaluating the optional chunk-length expression - if args.len() >= 2 { - super::args::emit_int_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the requested chunk length into the extra x86_64 helper argument register - } else { - emitter.instruction("mov rdi, 1"); // default to one-byte chunks when str_split() omits the chunk length - } - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the source string into the x86_64 string-helper input registers - } - } - abi::emit_call_label(emitter, "__rt_str_split"); // split the source string into fixed-size chunks through the target-aware runtime helper - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/strings/str_starts_with.rs b/src/codegen/builtins/strings/str_starts_with.rs deleted file mode 100644 index f775e50f1f..0000000000 --- a/src/codegen/builtins/strings/str_starts_with.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Emits PHP `str_starts_with` string search or comparison calls. -//! Handles string pointer/length arguments and boxes false-or-position results when PHP requires mixed output. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Return values must distinguish numeric position zero from PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use super::args::emit_string_arg; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `str_starts_with(haystack, prefix)` builtin call. -/// -/// Evaluates `haystack` (args[0]) and `prefix` (args[1]) as strings, then calls the -/// runtime helper `__rt_str_starts_with` to check whether the haystack begins with the prefix. -/// -/// Arguments: -/// - `args[0]`: haystack string expression -/// - `args[1]`: prefix string expression -/// - `emitter`: target-aware assembly emitter; saves haystack registers, evaluates prefix, restores haystack, calls runtime -/// - `ctx`: codegen context for variable layout and ownership -/// - `data`: data section for relocations and string literals -/// -/// Returns `Some(PhpType::Bool)` — `str_starts_with` always produces a boolean. -/// -/// ABI Details: -/// - AArch64: pushes haystack ptr/length (x1/x2) on stack while evaluating prefix (x3/x4), then restores haystack before the call -/// - x86_64: pushes haystack ptr/length (rax/rdx) on stack while evaluating prefix (rcx/rdx), then pops into rdi/rsi before the call -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("str_starts_with()"); - emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the haystack pointer and length while evaluating the prefix string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the prefix pointer into the third string-helper argument register - emitter.instruction("mov x4, x2"); // move the prefix length into the fourth string-helper argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the haystack pointer and length after evaluating the prefix - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the haystack pointer and length while evaluating the prefix string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // move the prefix length into the fourth SysV string-helper argument register - emitter.instruction("mov rdx, rax"); // move the prefix pointer into the third SysV string-helper argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the haystack pointer and length into the first two SysV helper argument registers - } - } - abi::emit_call_label(emitter, "__rt_str_starts_with"); // check whether the haystack begins with the provided prefix through the target-aware runtime helper - - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/strings/strcasecmp.rs b/src/codegen/builtins/strings/strcasecmp.rs deleted file mode 100644 index 87d195a410..0000000000 --- a/src/codegen/builtins/strings/strcasecmp.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Purpose: -//! Emits PHP `strcasecmp` string search or comparison calls. -//! Handles string pointer/length arguments and boxes false-or-position results when PHP requires mixed output. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Return values must distinguish numeric position zero from PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `strcasecmp` builtin. -/// -/// Compares two strings case-insensitively via the `__rt_strcasecmp` runtime helper. -/// Stores both string pointers/lengths on the stack (ARM64) or in registers (X86_64) to -/// preserve evaluation order before the call. -/// -/// # Arguments -/// * `_name` - Unused; present for dispatcher uniformity. -/// * `args` - Two expressions: the first and second strings to compare. -/// * `emitter` - Target-specific assembly emitter. -/// * `ctx` - Codegen context (used for expression lowering). -/// * `data` - Data section for string literals and constants. -/// -/// # Returns -/// Always returns `Some(PhpType::Int)`. PHP's `strcasecmp` returns integer 0 when strings -/// are equal, a negative value if `s1` is less than `s2`, or a positive value otherwise. -/// -/// # ABI Details -/// - ARM64: first string pointer/length in x1/x2, second in x3/x4; order preserved via stack push/pop. -/// - X86_64: first string pointer/length in rdi/rsi, second in rdx/rcx; order preserved via register stack. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strcasecmp()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the first string pointer and length while evaluating the second string - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the second string pointer into the third string-helper argument register - emitter.instruction("mov x4, x2"); // move the second string length into the fourth string-helper argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the first string pointer and length after evaluating the second string - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the first string pointer and length while evaluating the second string - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // move the second string length into the fourth SysV string-helper argument register - emitter.instruction("mov rdx, rax"); // move the second string pointer into the third SysV string-helper argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the first string pointer and length into the first two SysV helper argument registers - } - } - abi::emit_call_label(emitter, "__rt_strcasecmp"); // compare both strings case-insensitively through the shared runtime helper - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/strings/strcmp.rs b/src/codegen/builtins/strings/strcmp.rs deleted file mode 100644 index be56dea2a4..0000000000 --- a/src/codegen/builtins/strings/strcmp.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Emits PHP `strcmp` string search or comparison calls. -//! Handles string pointer/length arguments and boxes false-or-position results when PHP requires mixed output. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Return values must distinguish numeric position zero from PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for a PHP `strcmp(left, right)` call. -/// -/// Compares two strings lexicographically via the `__rt_strcmp` runtime helper. -/// Evaluates both argument expressions (which must resolve to strings), materializes -/// them as pointer/length pairs in the appropriate ABI registers, and calls the runtime -/// routine. The result is always `PhpType::Int` (0 for equal, <0 or >0 for ordering). -/// -/// # Arguments -/// - `args` — exactly two expressions: the left and right strings to compare. -/// - `emitter` — target-specific instruction emission. -/// - `ctx` — codegen context carrying variable layout and metadata. -/// - `data` — data section for relocations and static strings. -/// -/// # ABI details -/// - AArch64: first string in x1/x2, second string in x3/x4 via temporary stack spill. -/// - x86_64: first string in rdi/rsi, second string in rdx/rcx via temporary stack spill. -/// - Both targets call `__rt_strcmp` and the integer result is returned in the usual -/// integer register (`x0` on AArch64, `rax` on x86_64). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strcmp()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the first string pointer and length while evaluating the second string - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the second string pointer into the third string-helper argument register - emitter.instruction("mov x4, x2"); // move the second string length into the fourth string-helper argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the first string pointer and length after evaluating the second string - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the first string pointer and length while evaluating the second string - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // move the second string length into the fourth SysV string-helper argument register - emitter.instruction("mov rdx, rax"); // move the second string pointer into the third SysV string-helper argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the first string pointer and length into the first two SysV helper argument registers - } - } - abi::emit_call_label(emitter, "__rt_strcmp"); // compare both strings lexicographically through the shared runtime helper - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/strings/stripslashes.rs b/src/codegen/builtins/strings/stripslashes.rs deleted file mode 100644 index 0c9e49541d..0000000000 --- a/src/codegen/builtins/strings/stripslashes.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Purpose: -//! Emits PHP `stripslashes` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the `stripslashes` runtime helper for the builtin `stripslashes()`. -/// -/// Inputs: -/// - `args[0]` is evaluated and passed as the string argument to strip backslashes from. -/// - The runtime helper `__rt_stripslashes` removes escape backslashes following PHP rules. -/// -/// Returns `PhpType::Str` as the result is always a PHP string. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("stripslashes()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_stripslashes"); // remove escape backslashes through the active target ABI - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/strlen.rs b/src/codegen/builtins/strings/strlen.rs deleted file mode 100644 index 4f9eb68cd1..0000000000 --- a/src/codegen/builtins/strings/strlen.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Purpose: -//! Emits PHP `strlen` string builtin calls. -//! Coordinates string argument registers and runtime helper calls for PHP-compatible results. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - String ABI uses pointer/length pairs, with boxed results only where PHP returns mixed values. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `strlen` builtin call. -/// -/// Takes one string argument and returns its length as an integer. -/// The string argument is emitted via `emit_string_arg` using the string ABI -/// (pointer/length pair). The result register receives the string-length value -/// from the ABI string result registers. -/// -/// Returns `PhpType::Int` as the result type. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strlen()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - // -- return the string length as an integer -- - let (_, len_reg) = abi::string_result_regs(emitter); - emitter.instruction(&format!("mov {}, {}", abi::int_result_reg(emitter), len_reg)); // move the ABI string-length register into the integer return register - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/strings/strpos.rs b/src/codegen/builtins/strings/strpos.rs deleted file mode 100644 index 575458a4d8..0000000000 --- a/src/codegen/builtins/strings/strpos.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Purpose: -//! Emits PHP `strpos` string search or comparison calls. -//! Handles string pointer/length arguments and boxes false-or-position results when PHP requires mixed output. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Return values must distinguish numeric position zero from PHP false. - -use super::args::emit_string_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `strpos(haystack, needle)` builtin call. -/// -/// Evaluate `haystack` first, then `needle`, arranging arguments in target-specific -/// string-helper registers before calling `__rt_strpos`. The runtime returns either a -/// non-negative byte offset (including 0 for a match at the start) or a sentinel to -/// indicate no match. Calls `box_search_result` to box the raw result as a `Mixed` -/// value so PHP can distinguish integer `0` from boolean `false`. -/// -/// Returns `Some(PhpType::Mixed)` because `strpos` returns `int|false` in PHP. -/// -/// # Arguments -/// * `_name` - Unused; the caller dispatches by name -/// * `args` - `[haystack, needle]` -/// * `emitter` - Target assembly emitter -/// * `ctx` - Codegen context for labels and target info -/// * `data` - Data section for relocations -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strpos()"); - // Coerce both operands to string (ptr/len) so a Mixed/Union haystack or - // needle — e.g. stream_socket_get_name()'s `string|false` result — is - // unboxed via __rt_mixed_cast_string rather than passed as a boxed cell. - emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the haystack pointer and length while evaluating the needle string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the needle pointer into the third string-helper argument register - emitter.instruction("mov x4, x2"); // move the needle length into the fourth string-helper argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the haystack pointer and length after evaluating the needle - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the haystack pointer and length while evaluating the needle string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // move the needle length into the fourth SysV string-helper argument register - emitter.instruction("mov rdx, rax"); // move the needle pointer into the third SysV string-helper argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the haystack pointer and length into the first two SysV helper argument registers - } - } - abi::emit_call_label(emitter, "__rt_strpos"); // find the first needle occurrence in the haystack through the shared runtime helper - box_search_result(emitter, ctx); - - Some(PhpType::Mixed) -} - -/// Box a raw `strpos` result as a `Mixed` value. -/// -/// Reads the raw integer result from `x0` (ARM64) or `rax` (x86_64). If the value -/// is non-negative, it is boxed as an integer (`tag = 0`). Otherwise, the not-found -/// sentinel is boxed as boolean `false` (`tag = 3`), preserving PHP's requirement that -/// `strpos(...) === false` and `strpos(...) !== 0` are both meaningful. -/// -/// Uses `ctx.next_label` to generate local branch labels unique to this invocation. -fn box_search_result(emitter: &mut Emitter, ctx: &mut Context) { - let found_label = ctx.next_label("strpos_found"); - let end_label = ctx.next_label("strpos_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // distinguish a valid non-negative match offset from the not-found sentinel - emitter.instruction(&format!("b.ge {}", found_label)); // box a found offset as an integer result - emitter.instruction("mov x1, #0"); // false payload = 0 for the mixed bool box - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false for strpos() not found - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false so offset 0 remains distinguishable from not found - emitter.instruction(&format!("b {}", end_label)); // skip the integer boxing path after the not-found result - emitter.label(&found_label); - emitter.instruction("mov x1, x0"); // move the found offset into the mixed helper payload register - emitter.instruction("mov x2, #0"); // integer mixed payloads do not use a high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = int for strpos() found offsets - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the found integer offset as mixed - emitter.label(&end_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // distinguish a valid non-negative match offset from the not-found sentinel - emitter.instruction(&format!("jge {}", found_label)); // box a found offset as an integer result - emitter.instruction("xor edi, edi"); // false payload = 0 for the mixed bool box - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false for strpos() not found - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false so offset 0 remains distinguishable from not found - emitter.instruction(&format!("jmp {}", end_label)); // skip the integer boxing path after the not-found result - emitter.label(&found_label); - emitter.instruction("mov rdi, rax"); // move the found offset into the mixed helper payload register - emitter.instruction("xor esi, esi"); // integer mixed payloads do not use a high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = int for strpos() found offsets - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the found integer offset as mixed - emitter.label(&end_label); - } - } -} diff --git a/src/codegen/builtins/strings/strrev.rs b/src/codegen/builtins/strings/strrev.rs deleted file mode 100644 index 996714f6d5..0000000000 --- a/src/codegen/builtins/strings/strrev.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits PHP `strrev` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `strrev` builtin. -/// -/// Arguments: -/// - `args[0]`: the input string (emitted via `emit_string_arg`) -/// - The runtime helper `__rt_strrev` reverses the string and returns an owned result slice. -/// -/// Outputs: -/// - Calls `__rt_strrev` via `abi::emit_call_label` -/// - Returns `PhpType::Str` (caller receives ownership of the returned string) -/// -/// ABI constraints: -/// - Input string passed as pointer/length pair following standard string ABI -/// - Returned string pointer in `x1`, length in `x2` (ARM64 string return convention) -/// - Caller owns the returned string; no lifetime aliasing -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strrev()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_strrev"); // reverse the input string through the target-aware runtime helper and return an owned result slice - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/strrpos.rs b/src/codegen/builtins/strings/strrpos.rs deleted file mode 100644 index 4836a0908e..0000000000 --- a/src/codegen/builtins/strings/strrpos.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Purpose: -//! Emits PHP `strrpos` string search or comparison calls. -//! Handles string pointer/length arguments and boxes false-or-position results when PHP requires mixed output. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Return values must distinguish numeric position zero from PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use super::args::emit_string_arg; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP `strrpos(haystack, needle)`. -/// -/// Pushes the haystack pointer/length registers, evaluates the needle argument, -/// loads both strings into the ABI string-helper registers, calls `__rt_strrpos`, -/// then boxes the raw integer result (position or sentinel) into a `PhpType::Mixed` -/// value so PHP's `false | int` return type is preserved correctly. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strrpos()"); - emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the haystack pointer and length while evaluating the needle string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the needle pointer into the third string-helper argument register - emitter.instruction("mov x4, x2"); // move the needle length into the fourth string-helper argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the haystack pointer and length after evaluating the needle - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the haystack pointer and length while evaluating the needle string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rcx, rdx"); // move the needle length into the fourth SysV string-helper argument register - emitter.instruction("mov rdx, rax"); // move the needle pointer into the third SysV string-helper argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the haystack pointer and length into the first two SysV helper argument registers - } - } - abi::emit_call_label(emitter, "__rt_strrpos"); // find the last needle occurrence in the haystack through the shared runtime helper - box_search_result(emitter, ctx); - - Some(PhpType::Mixed) -} - -/// Box the raw search result in `x0`/`rax` into a `PhpType::Mixed` value. -/// -/// - If the result is negative (sentinel), emits `bool false` (tag 3). -/// - If the result is non-negative (found position), emits `int` (tag 0). -/// -/// The distinction matters because PHP's `strrpos` returns `int 0` for a match -/// at position zero but `false` when nothing is found — both fit in a raw -/// integer register but have different PHP runtime representations. -fn box_search_result(emitter: &mut Emitter, ctx: &mut Context) { - let found_label = ctx.next_label("strrpos_found"); - let end_label = ctx.next_label("strrpos_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // distinguish a valid non-negative match offset from the not-found sentinel - emitter.instruction(&format!("b.ge {}", found_label)); // box a found offset as an integer result - emitter.instruction("mov x1, #0"); // false payload = 0 for the mixed bool box - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false for strrpos() not found - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false so offset 0 remains distinguishable from not found - emitter.instruction(&format!("b {}", end_label)); // skip the integer boxing path after the not-found result - emitter.label(&found_label); - emitter.instruction("mov x1, x0"); // move the found offset into the mixed helper payload register - emitter.instruction("mov x2, #0"); // integer mixed payloads do not use a high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = int for strrpos() found offsets - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the found integer offset as mixed - emitter.label(&end_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // distinguish a valid non-negative match offset from the not-found sentinel - emitter.instruction(&format!("jge {}", found_label)); // box a found offset as an integer result - emitter.instruction("xor edi, edi"); // false payload = 0 for the mixed bool box - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false for strrpos() not found - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false so offset 0 remains distinguishable from not found - emitter.instruction(&format!("jmp {}", end_label)); // skip the integer boxing path after the not-found result - emitter.label(&found_label); - emitter.instruction("mov rdi, rax"); // move the found offset into the mixed helper payload register - emitter.instruction("xor esi, esi"); // integer mixed payloads do not use a high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = int for strrpos() found offsets - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the found integer offset as mixed - emitter.label(&end_label); - } - } -} diff --git a/src/codegen/builtins/strings/strstr.rs b/src/codegen/builtins/strings/strstr.rs deleted file mode 100644 index 41f0f052f8..0000000000 --- a/src/codegen/builtins/strings/strstr.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Purpose: -//! Emits PHP `strstr` string search or comparison calls. -//! Handles string pointer/length arguments and boxes false-or-position results when PHP requires mixed output. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Return values must distinguish numeric position zero from PHP false. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use super::args::emit_string_arg; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `strstr` builtin call. -/// -/// Searches for `needle` in `haystack` (the first two call arguments) and returns -/// the haystack suffix starting at the match position. When the needle is not found, -/// returns an empty string (zero-length). Delegates to `__rt_strpos` to perform the -/// underlying search, then post-processes the result: advances the haystack pointer -/// to the match offset and shrinks the length to the remaining suffix. -/// -/// # Arguments -/// - `args[0]` — haystack string expression -/// - `args[1]` — needle string expression -/// -/// # Output -/// - `PhpType::Str` — a string pointer in `x1`/`rax` and length in `x2`/`rdx` -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strstr()"); - emit_string_arg(&args[0], emitter, ctx, data); - let found = ctx.next_label("strstr_found"); - let end = ctx.next_label("strstr_end"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the haystack pointer and length while evaluating the needle string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the needle pointer into the third string-helper argument register - emitter.instruction("mov x4, x2"); // move the needle length into the fourth string-helper argument register - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the haystack pointer and length after evaluating the needle - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the haystack again so strstr() can rebuild the matching suffix after strpos() - abi::emit_call_label(emitter, "__rt_strpos"); // find the first match position inside the haystack through the shared runtime helper - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the haystack pointer and length after the strpos() helper returns the match index - emitter.instruction("cmp x0, #0"); // check whether strpos() returned a valid match position - emitter.instruction(&format!("b.ge {}", found)); // branch to the matching-suffix path when the needle was found - emitter.instruction("mov x2, #0"); // return an empty-string length when strstr() does not find the needle - emitter.instruction(&format!("b {}", end)); // skip the suffix-construction path when strstr() does not find the needle - emitter.label(&found); - emitter.instruction("add x1, x1, x0"); // advance the haystack pointer to the start of the matching suffix - emitter.instruction("sub x2, x2, x0"); // shrink the haystack length down to the matching suffix length - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // save the haystack pointer and length while evaluating the needle string - emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov r8, rax"); // preserve the needle pointer while restoring the haystack pointer and length - emitter.instruction("mov r9, rdx"); // preserve the needle length while restoring the haystack pointer and length - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the haystack pointer and length into the standard string result registers - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push the haystack again so strstr() can rebuild the matching suffix after strpos() - emitter.instruction("mov rdi, rax"); // move the haystack pointer into the first SysV string-helper argument register - emitter.instruction("mov rsi, rdx"); // move the haystack length into the second SysV string-helper argument register - emitter.instruction("mov rdx, r8"); // move the preserved needle pointer into the third SysV string-helper argument register - emitter.instruction("mov rcx, r9"); // move the preserved needle length into the fourth SysV string-helper argument register - abi::emit_call_label(emitter, "__rt_strpos"); // find the first match position inside the haystack through the shared runtime helper - emitter.instruction("mov r8, rax"); // preserve the signed strpos() result across restoring the saved haystack pointer and length - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the haystack pointer and length after the strpos() helper returns the match index - emitter.instruction("cmp r8, 0"); // check whether strpos() returned a valid match position - emitter.instruction(&format!("jge {}", found)); // branch to the matching-suffix path when the needle was found - emitter.instruction("xor eax, eax"); // return an empty-string pointer when strstr() does not find the needle - emitter.instruction("xor edx, edx"); // return an empty-string length when strstr() does not find the needle - emitter.instruction(&format!("jmp {}", end)); // skip the suffix-construction path when strstr() does not find the needle - emitter.label(&found); - emitter.instruction("add rax, r8"); // advance the haystack pointer to the start of the matching suffix - emitter.instruction("sub rdx, r8"); // shrink the haystack length down to the matching suffix length - } - } - emitter.label(&end); - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/strtolower.rs b/src/codegen/builtins/strings/strtolower.rs deleted file mode 100644 index 4b6850085e..0000000000 --- a/src/codegen/builtins/strings/strtolower.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `strtolower` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `strtolower` call for a single string argument. -/// -/// # Arguments -/// - `args[0]`: The string expression to convert to lowercase. -/// -/// # Behavior -/// Emits code to evaluate `args[0]` as a string, then calls `__rt_strtolower` to -/// perform the case conversion and return an owned PHP string. The returned string -/// pointer/length is treated as an owned runtime value. -/// -/// # Returns -/// `PhpType::Str` — the lowered string. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strtolower()"); - // Coerce the operand to a string in x1/x2 (rdi/rsi) via emit_string_arg, so a - // Mixed argument (a `mixed` property/return value or an assoc-array element) - // is cast through __rt_mixed_cast_string rather than left as a boxed cell in - // x0 with stale string registers (which produced an empty result). - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_strtolower"); // lowercase the input string through the target-aware runtime helper and return an owned result slice - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/strtoupper.rs b/src/codegen/builtins/strings/strtoupper.rs deleted file mode 100644 index c11d5b7b55..0000000000 --- a/src/codegen/builtins/strings/strtoupper.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Purpose: -//! Emits PHP `strtoupper` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `strtoupper` builtin. -/// -/// # Arguments -/// - `_name`: Unused parameter present for dispatcher uniformity. -/// - `args`: Must contain exactly one expression producing a string value. -/// - `emitter`: Target-aware assembly emitter. -/// - `ctx`: Codegen context carrying variable layout and ownership state. -/// - `data`: Data section for relocations and static data. -/// -/// # Behavior -/// 1. Emits code to evaluate and materialize the first argument onto the stack. -/// 2. Calls `__rt_strtoupper`, a target-aware runtime helper that converts the -/// string in-place to uppercase using PHP's locale-aware rules. -/// 3. Returns `PhpType::Str` indicating the result is an owned PHP string. -/// -/// # ABI Constraints -/// The runtime helper expects the input string in the standard string ABI registers -/// (`x1`=ptr, `x2`=len on ARM64; `rdi`=ptr, `rsi`=len on x86_64) and returns the -/// transformed string pointer/length pair via the same registers. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strtoupper()"); - // Coerce the operand to a string in x1/x2 (rdi/rsi). Using emit_string_arg - // (rather than a bare emit_expr) means a Mixed argument — e.g. a `mixed` - // property/return value or an assoc-array element — is cast to a real string - // via __rt_mixed_cast_string instead of leaving a boxed cell in x0 with stale - // string registers (which produced an empty result). - super::args::emit_string_arg(&args[0], emitter, ctx, data); - // -- convert all characters to uppercase -- - abi::emit_call_label(emitter, "__rt_strtoupper"); // call the target-aware runtime helper that uppercases the current string into concat storage - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/substr.rs b/src/codegen/builtins/strings/substr.rs deleted file mode 100644 index 7fb17a4595..0000000000 --- a/src/codegen/builtins/strings/substr.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Purpose: -//! Emits PHP `substr` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `substr(string, offset, length?)` builtin call. -/// -/// Evaluates arguments in source order, materializes them into ABI registers, -/// then emits platform-specific instructions that compute the resulting substring -/// pointer and length. Handles negative offsets (converted relative to end), offset -/// clamping to string length, and optional negative length clamping to zero. -/// -/// # Arguments -/// - `_name`: unused, always `null` (name resolved by catalog lookup) -/// - `args`: exactly 2 or 3 expressions: `(string, offset, length?)` -/// - `emitter`: drives instruction emission and label allocation -/// - `ctx`: carries target arch, vtable, and local variable layout -/// - `data`: scratch area for relocatable immediates and string data -/// -/// # Returns -/// `Some(PhpType::Str)` — the result is always a PHP string. -/// -/// # Side effects -/// Clobbers temporary registers used for integer materialization. On x86_64, -/// also clobbers `r8` as a zero materialized for negative clamping. String result -/// is returned as borrowed pointer/length in `x1`/`x2` (AArch64) or `rax`/`rdx` (x86_64). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("substr()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - let neg_done = ctx.next_label("substr_neg_done"); - let len_done = ctx.next_label("substr_len_done"); - match emitter.target.arch { - Arch::AArch64 => { - // -- save string and evaluate offset -- - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push string ptr and length onto stack - super::args::push_int_arg(&args[1], emitter, ctx, data); - if args.len() >= 3 { - super::args::emit_int_arg(&args[2], emitter, ctx, data); - emitter.instruction("mov x3, x0"); // move length argument to x3 - } else { - emitter.instruction("mov x3, #-1"); // set sentinel -1: use all remaining characters - } - // -- restore offset and string from stack -- - emitter.instruction("ldr x0, [sp], #16"); // pop offset into x0 - emitter.instruction("ldp x1, x2, [sp], #16"); // pop string ptr into x1, length into x2 - // -- handle negative offset -- - emitter.instruction("cmp x0, #0"); // check if offset is negative - emitter.instruction(&format!("b.ge {}", neg_done)); // skip adjustment if offset >= 0 - emitter.instruction("add x0, x2, x0"); // convert negative offset: offset = length + offset - emitter.instruction("cmp x0, #0"); // check if adjusted offset is still negative - emitter.instruction("csel x0, xzr, x0, lt"); // clamp to 0 if offset went below zero - emitter.label(&neg_done); - // -- clamp offset to string length -- - emitter.instruction("cmp x0, x2"); // compare offset to string length - emitter.instruction("csel x0, x2, x0, gt"); // clamp offset to length if it exceeds it - // -- adjust pointer and compute result length -- - emitter.instruction("add x1, x1, x0"); // advance string pointer by offset bytes - emitter.instruction("sub x2, x2, x0"); // remaining = length - offset - // -- apply optional length argument -- - emitter.instruction("cmn x3, #1"); // test if x3 == -1 (no length arg given) - emitter.instruction(&format!("b.eq {}", len_done)); // skip length clamping if no length arg - emitter.instruction("cmp x3, #0"); // check if length arg is negative - emitter.instruction("csel x3, xzr, x3, lt"); // clamp negative length to 0 - emitter.instruction("cmp x3, x2"); // compare length arg to remaining chars - emitter.instruction("csel x2, x3, x2, lt"); // result length = min(length arg, remaining) - emitter.label(&len_done); - } - Arch::X86_64 => { - // -- save string and evaluate offset -- - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push string ptr and length onto the temporary stack - super::args::push_int_arg(&args[1], emitter, ctx, data); - if args.len() >= 3 { - super::args::emit_int_arg(&args[2], emitter, ctx, data); - emitter.instruction("mov rcx, rax"); // move the optional length argument into the x86_64 scratch register - } else { - abi::emit_load_int_immediate(emitter, "rcx", -1); // set sentinel -1 so the helper keeps the full tail when the length is omitted - } - // -- restore offset and string from stack -- - abi::emit_pop_reg(emitter, "rax"); // pop the substring offset into the primary integer result register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // pop the source string pointer and length into x86_64 scratch registers - // -- handle negative offset -- - emitter.instruction("cmp rax, 0"); // check whether the requested offset is negative - emitter.instruction(&format!("jge {}", neg_done)); // skip the negative-offset fixup when the offset is already non-negative - emitter.instruction("add rax, rsi"); // convert the negative offset into a tail-relative byte index - emitter.instruction("cmp rax, 0"); // check whether the adjusted tail-relative offset still underflowed past the start - emitter.instruction("mov r8, 0"); // materialize zero for the negative-offset clamp without depending on extra runtime data - emitter.instruction("cmovl rax, r8"); // clamp the adjusted offset back to zero when it still points before the string start - emitter.label(&neg_done); - // -- clamp offset to string length -- - emitter.instruction("cmp rax, rsi"); // compare the requested offset against the full source-string length - emitter.instruction("cmovg rax, rsi"); // clamp the offset to the full string length when it points past the end - // -- adjust pointer and compute result length -- - emitter.instruction("add rdi, rax"); // advance the source-string pointer by the final byte offset - emitter.instruction("sub rsi, rax"); // compute the remaining substring length after the final byte offset - // -- apply optional length argument -- - emitter.instruction("cmp rcx, -1"); // check whether the caller omitted the optional length argument - emitter.instruction(&format!("je {}", len_done)); // keep the full remaining tail when the optional length argument was omitted - emitter.instruction("cmp rcx, 0"); // check whether the requested substring length is negative - emitter.instruction("mov r8, 0"); // materialize zero for the negative-length clamp without depending on extra runtime data - emitter.instruction("cmovl rcx, r8"); // clamp the requested substring length back to zero when it is negative - emitter.instruction("cmp rcx, rsi"); // compare the requested substring length against the remaining tail length - emitter.instruction("cmovl rsi, rcx"); // shrink the substring length when the explicit requested length is shorter than the tail - emitter.label(&len_done); - emitter.instruction("mov rax, rdi"); // return the borrowed substring pointer in the primary x86_64 string result register - emitter.instruction("mov rdx, rsi"); // return the borrowed substring length in the secondary x86_64 string result register - } - } - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/substr_replace.rs b/src/codegen/builtins/strings/substr_replace.rs deleted file mode 100644 index 14b98911fc..0000000000 --- a/src/codegen/builtins/strings/substr_replace.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Purpose: -//! Emits PHP `substr_replace` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `substr_replace(string, replacement, start, length)` builtin call. -/// -/// Handles four-argument form where `length` is optional (defaults to replacement through -/// end of subject). Pushes arguments onto the stack in evaluation order so that the callee -/// can restore them in ABI order for the runtime helper. -/// -/// # Arguments -/// - `args[0]`: subject string to modify -/// - `args[1]`: replacement string -/// - `args[2]`: start offset (int) -/// - `args[3]` (optional): replacement length; when absent, sentinel `-1` is passed to -/// replace through end of subject string -/// -/// # Return -/// Always returns `Some(PhpType::Str)` — the runtime helper allocates a new PHP string. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("substr_replace()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the subject string while the replacement, offset, and optional length are evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push the replacement string while the offset and optional length are evaluated - super::args::push_int_arg(&args[2], emitter, ctx, data); - if args.len() >= 4 { - super::args::emit_int_arg(&args[3], emitter, ctx, data); - emitter.instruction("mov x7, x0"); // move the optional replacement length into the scalar runtime argument register - } else { - emitter.instruction("mov x7, #-1"); // set sentinel -1 so the runtime replaces through the end of the subject string - } - emitter.instruction("ldr x0, [sp], #16"); // restore the replacement offset after evaluating the optional length argument - emitter.instruction("ldp x3, x4, [sp], #16"); // restore the replacement string into the secondary runtime string-argument pair - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject string into the primary runtime string-argument pair - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push the subject string while the replacement, offset, and optional length are evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push the replacement string while the offset and optional length are evaluated - super::args::push_int_arg(&args[2], emitter, ctx, data); - if args.len() >= 4 { - super::args::emit_int_arg(&args[3], emitter, ctx, data); - emitter.instruction("mov r8, rax"); // move the optional replacement length into the scalar x86_64 runtime argument register - } else { - abi::emit_load_int_immediate(emitter, "r8", -1); // set sentinel -1 so the runtime replaces through the end of the subject string - } - abi::emit_pop_reg(emitter, "rcx"); // restore the replacement offset after evaluating the optional length argument - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // restore the replacement string into the secondary x86_64 runtime string-argument pair - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the subject string into the primary x86_64 runtime string-argument pair - } - } - abi::emit_call_label(emitter, "__rt_substr_replace"); // replace the requested subject substring through the target-aware runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/trim.rs b/src/codegen/builtins/strings/trim.rs deleted file mode 100644 index f8e639b56c..0000000000 --- a/src/codegen/builtins/strings/trim.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Purpose: -//! Emits PHP `trim` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `trim` builtin, which strips whitespace (or a specified -/// character mask) from the beginning and end of a string. -/// -/// # Arguments -/// - `_name`: Unused; caller dispatches by name so this param is ignored. -/// - `args`: Either 1 argument (string to strip) or 2 arguments (string + mask). -/// - `emitter`: Target-aware instruction emitter. -/// - `ctx`: Codegen context carrying variable layout and metadata. -/// - `data`: Writable data section for string literals and runtime symbols. -/// -/// # Behavior -/// - 1 arg: evaluates the string argument, then calls `__rt_trim` to strip -/// ASCII whitespace from both ends. -/// - 2 args: evaluates the string argument, then evaluates the mask argument -/// (source string is preserved on the stack during mask evaluation), then -/// calls `__rt_trim_mask` to strip the given character mask from both ends. -/// -/// # Returns -/// Always returns `Some(PhpType::Str)` — the result is an owned PHP string. -/// -/// # ABI notes -/// ARM64: source string ptr/len live in x1/x2; mask ptr/len are loaded into x3/x4 -/// before the call. x86_64: source string ptr/len are pushed on the stack during -/// mask evaluation, then moved to rdi/rsi for the call. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("trim()"); - - if args.len() == 1 { - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_trim"); // strip whitespace from both ends through the target-aware trim runtime helper - } else { - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x1, [sp, #-16]!"); // push the source string pointer while the trim mask expression is evaluated - emitter.instruction("str x2, [sp, #-16]!"); // push the source string length while the trim mask expression is evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x1"); // move the trim mask pointer into the secondary trim-mask runtime string-argument pair - emitter.instruction("mov x4, x2"); // move the trim mask length into the secondary trim-mask runtime string-argument pair - emitter.instruction("ldr x2, [sp], #16"); // restore the source string length after evaluating the trim mask expression - emitter.instruction("ldr x1, [sp], #16"); // restore the source string pointer after evaluating the trim mask expression - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the source string ptr/len while the trim mask expression is evaluated - super::args::emit_string_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the trim mask pointer into the secondary x86_64 trim-mask runtime string-argument slot - emitter.instruction("mov rsi, rdx"); // move the trim mask length into the secondary x86_64 trim-mask runtime string-argument slot - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the source string ptr/len after evaluating the trim mask expression - } - } - abi::emit_call_label(emitter, "__rt_trim_mask"); // strip the requested character mask from both sides through the target-aware trim runtime helper - } - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/ucfirst.rs b/src/codegen/builtins/strings/ucfirst.rs deleted file mode 100644 index 5b31b90291..0000000000 --- a/src/codegen/builtins/strings/ucfirst.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Purpose: -//! Emits PHP `ucfirst` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the PHP `ucfirst` builtin. -/// -/// Evaluates `args[0]` as a string expression, copies it via `__rt_strcopy`, then -/// uppercases the first byte in-place if it falls in the ASCII lowercase range ('a'-'z'). -/// Returns `PhpType::Str`. -/// -/// # Arguments -/// * `_name` — unused; the builtin name is always `ucfirst` -/// * `args` — must contain exactly one string/scalar argument -/// * `emitter` — target-aware instruction emitter -/// * `ctx` — variable layout and metadata context -/// * `data` — data section for relocations and constants -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ucfirst()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - // -- copy string then uppercase the first character -- - abi::emit_call_label(emitter, "__rt_strcopy"); // copy the source string into concat storage before mutating its first byte in place - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cbz x2, 1f"); // skip the ASCII-case tweak when ucfirst() receives an empty string - emitter.instruction("ldrb w9, [x1]"); // load the first byte of the copied string so ucfirst() can classify its ASCII case - emitter.instruction("cmp w9, #97"); // compare the copied first byte against 'a' to detect lowercase ASCII input - emitter.instruction("b.lt 1f"); // leave bytes below 'a' unchanged because they are not lowercase ASCII letters - emitter.instruction("cmp w9, #122"); // compare the copied first byte against 'z' to bound the lowercase ASCII range - emitter.instruction("b.gt 1f"); // leave bytes above 'z' unchanged because they are not lowercase ASCII letters - emitter.instruction("sub w9, w9, #32"); // convert lowercase ASCII to uppercase by subtracting the standard ASCII case delta - emitter.instruction("strb w9, [x1]"); // store the uppercased first byte back into the copied string in concat storage - emitter.raw("1:"); - } - Arch::X86_64 => { - emitter.instruction("test rdx, rdx"); // skip the ASCII-case tweak when ucfirst() receives an empty string - emitter.instruction("jz 1f"); // leave empty strings unchanged because there is no first byte to uppercase - emitter.instruction("movzx ecx, BYTE PTR [rax]"); // load the first byte of the copied string so ucfirst() can classify its ASCII case - emitter.instruction("cmp cl, 97"); // compare the copied first byte against 'a' to detect lowercase ASCII input - emitter.instruction("jb 1f"); // leave bytes below 'a' unchanged because they are not lowercase ASCII letters - emitter.instruction("cmp cl, 122"); // compare the copied first byte against 'z' to bound the lowercase ASCII range - emitter.instruction("ja 1f"); // leave bytes above 'z' unchanged because they are not lowercase ASCII letters - emitter.instruction("sub cl, 32"); // convert lowercase ASCII to uppercase by subtracting the standard ASCII case delta - emitter.instruction("mov BYTE PTR [rax], cl"); // store the uppercased first byte back into the copied string in concat storage - emitter.raw("1:"); - } - } - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/ucwords.rs b/src/codegen/builtins/strings/ucwords.rs deleted file mode 100644 index 02a78b7451..0000000000 --- a/src/codegen/builtins/strings/ucwords.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits PHP `ucwords` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `ucwords` builtin, which uppercases the first character of each -/// whitespace-delimited word in a string. -/// -/// # Arguments -/// - `_name`: Unused; PHP builtins are dispatched by name. -/// - `args`: Single argument — the string to transform. -/// -/// # Outputs -/// - Pushes a string pointer/length pair onto the call stack. -/// - Returns `PhpType::Str` indicating the result is a PHP string. -/// -/// # Runtime behavior -/// - Calls `__rt_ucwords` via `abi::emit_call_label`. -/// - The runtime helper allocates a new owned PHP string; the caller must -/// treat the returned pointer/length as an owned value. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("ucwords()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_ucwords"); // call the target-aware runtime helper that uppercases the first letter of each whitespace-delimited word - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/urldecode.rs b/src/codegen/builtins/strings/urldecode.rs deleted file mode 100644 index 96a7d20b23..0000000000 --- a/src/codegen/builtins/strings/urldecode.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Purpose: -//! Emits PHP `urldecode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `urldecode()` call, decoding a percent-encoded query string argument. -/// -/// Unused `name` parameter supports PHP case-insensitive builtin dispatch. -/// Arguments: args[0] must be a PHP string to decode. -/// Emits: expression evaluation for args[0], then a target-aware call to `__rt_urldecode`. -/// Returns: `Some(PhpType::Str)` — the result is an owned runtime string allocation. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("urldecode()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_urldecode"); // call the target-aware runtime helper that decodes query-style percent-encoded strings - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/urlencode.rs b/src/codegen/builtins/strings/urlencode.rs deleted file mode 100644 index 66438f9f14..0000000000 --- a/src/codegen/builtins/strings/urlencode.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Purpose: -//! Emits PHP `urlencode` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `urlencode(...)` call: evaluates the first argument as a PHP string, calls the -/// runtime helper `__rt_urlencode` to produce a percent-encoded query-string result, and -/// returns `PhpType::Str` as an owned heap-allocated PHP string. -/// -/// - Argument 0 is evaluated in source order and consumed by value. -/// - Result pointer/length is returned via the target ABI (ARM64: x1, x2; x86_64: rsi, rdx). -/// - The returned string is an owned runtime value; the caller owns the allocation. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("urlencode()"); - // Coerce the operand to a string in the string ABI registers via emit_string_arg, so a - // Mixed argument is cast through __rt_mixed_cast_string instead of leaving a boxed cell in - // the result register with stale string registers. - super::args::emit_string_arg(&args[0], emitter, ctx, data); - abi::emit_call_label(emitter, "__rt_urlencode"); // call the target-aware runtime helper that percent-encodes the current string for query-style URLs - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/vprintf.rs b/src/codegen/builtins/strings/vprintf.rs deleted file mode 100644 index caacd9349b..0000000000 --- a/src/codegen/builtins/strings/vprintf.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Emits PHP `vprintf($format, $values)` — `printf` with the arguments supplied -//! as an array. Formats through the `__rt_vsprintf` array→variadic bridge, -//! writes the result to stdout, and returns the byte count. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Identical to `vsprintf` up to the formatted string, then writes it to -//! stdout (write syscall on AArch64, `write` syscall on x86_64) and returns -//! the length, matching `printf`'s contract. Returns `PhpType::Int`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `vprintf($format, $values)` call: format via `__rt_vsprintf`, write -/// the result to stdout, return the byte count. Returns `Some(PhpType::Int)`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("vprintf()"); - emit_expr(&args[0], emitter, ctx, data); // format string → string-result pair - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("sub sp, sp, #16"); // scratch slot for the format string - emitter.instruction("stp x1, x2, [sp, #0]"); // save the format ptr/len across the array evaluation - emit_expr(&args[1], emitter, ctx, data); // arguments array → x0 - emitter.instruction("ldp x1, x2, [sp, #0]"); // restore the format ptr/len - emitter.instruction("add sp, sp, #16"); // release the scratch slot - abi::emit_call_label(emitter, "__rt_vsprintf"); // x1 = formatted ptr, x2 = formatted len - emitter.instruction("mov x0, #1"); // fd = stdout (x1/x2 already hold ptr/len) - emitter.syscall(4); // write(1, formatted, len) - emitter.instruction("mov x0, x2"); // return the byte count - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // scratch slot for the format string - emitter.instruction("mov QWORD PTR [rsp], rax"); // save the format ptr across the array evaluation - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the format len across the array evaluation - emit_expr(&args[1], emitter, ctx, data); // arguments array → rax - emitter.instruction("mov rdi, rax"); // array pointer → __rt_vsprintf first argument - emitter.instruction("mov rax, QWORD PTR [rsp]"); // restore the format ptr - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore the format len - emitter.instruction("add rsp, 16"); // release the scratch slot - abi::emit_call_label(emitter, "__rt_vsprintf"); // rax = formatted ptr, rdx = formatted len - emitter.instruction("mov r8, rdx"); // preserve the byte count in r8; the syscall instruction clobbers rcx - emitter.instruction("mov rsi, rax"); // formatted pointer → SysV write buffer register - emitter.instruction("mov rdx, r8"); // formatted length → SysV write byte-count register - emitter.instruction("mov edi, 1"); // fd = stdout - emitter.instruction("mov eax, 1"); // syscall 1 = write on Linux x86_64 - emitter.instruction("syscall"); // write the formatted bytes to stdout - emitter.instruction("mov rax, r8"); // return the byte count (rcx was destroyed by syscall) - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/strings/vsprintf.rs b/src/codegen/builtins/strings/vsprintf.rs deleted file mode 100644 index 5211f91484..0000000000 --- a/src/codegen/builtins/strings/vsprintf.rs +++ /dev/null @@ -1,58 +0,0 @@ -//! Purpose: -//! Emits PHP `vsprintf($format, $values)` — `sprintf` with the arguments -//! supplied as an array instead of a variadic list. Delegates to the -//! `__rt_vsprintf` runtime bridge, which pushes one tagged record per array -//! element and tail-calls `__rt_sprintf`. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - PHP evaluates `$format` before `$values`; the format string is preserved -//! across the array evaluation, then both are handed to `__rt_vsprintf` -//! (array pointer in the SysV first arg / x0, format in the elephc string -//! pair). Returns `PhpType::Str`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `vsprintf($format, $values)` call. Evaluates the format (preserved -/// across the array evaluation) and the arguments array, then calls -/// `__rt_vsprintf`. Returns `Some(PhpType::Str)`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("vsprintf()"); - emit_expr(&args[0], emitter, ctx, data); // format string → string-result pair - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("sub sp, sp, #16"); // scratch slot for the format string - emitter.instruction("stp x1, x2, [sp, #0]"); // save the format ptr/len across the array evaluation - emit_expr(&args[1], emitter, ctx, data); // arguments array → x0 - emitter.instruction("ldp x1, x2, [sp, #0]"); // restore the format ptr/len - emitter.instruction("add sp, sp, #16"); // release the scratch slot - abi::emit_call_label(emitter, "__rt_vsprintf"); // bridge to __rt_sprintf via the per-element records - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // scratch slot for the format string - emitter.instruction("mov QWORD PTR [rsp], rax"); // save the format ptr across the array evaluation - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the format len across the array evaluation - emit_expr(&args[1], emitter, ctx, data); // arguments array → rax - emitter.instruction("mov rdi, rax"); // array pointer → __rt_vsprintf first argument - emitter.instruction("mov rax, QWORD PTR [rsp]"); // restore the format ptr - emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore the format len - emitter.instruction("add rsp, 16"); // release the scratch slot - abi::emit_call_label(emitter, "__rt_vsprintf"); // bridge to __rt_sprintf via the per-element records - } - } - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/strings/wordwrap.rs b/src/codegen/builtins/strings/wordwrap.rs deleted file mode 100644 index 2b239e2105..0000000000 --- a/src/codegen/builtins/strings/wordwrap.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Purpose: -//! Emits PHP `wordwrap` string transformation or formatting calls. -//! Marshals string/scalar arguments into runtime helpers that allocate returned PHP strings. -//! -//! Called from: -//! - `crate::codegen::builtins::strings::emit()`. -//! -//! Key details: -//! - Returned string pointer/length pairs must be treated as owned runtime values when the helper allocates. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `wordwrap()` builtin. -/// -/// # Arguments -/// - `args[0]`: input string to wrap -/// - `args[1]` (optional): wrap width, defaults to 75 -/// - `args[2]` (optional): break string, defaults to `"\n"` -/// - `args[3]` (optional): `cut_long_words` flag, defaults to `false` -/// -/// # Register layout (AArch64) -/// - x1/x2: input string pointer/length (preserved across arg evaluation) -/// - x3: wrap width -/// - x4/x5: break string pointer/length -/// - x6: `cut_long_words` flag (0/1) -/// - calls `__rt_wordwrap` via ABI convention -/// -/// # Register layout (x86_64) -/// - rax/rdx: input string pointer/length (preserved across arg evaluation) -/// - rdi: wrap width -/// - rcx/r8: break string pointer/length -/// - r9: `cut_long_words` flag (0/1) -/// - calls `__rt_wordwrap` via System V AMD64 ABI -/// -/// # Returns -/// `Some(PhpType::Str)` — the wrapped string is owned by the runtime. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("wordwrap()"); - super::args::emit_string_arg(&args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the input string while evaluating the width and optional break string - if args.len() >= 2 { - super::args::emit_int_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov x3, x0"); // move the requested wrap width into the scalar runtime argument register - } else { - emitter.instruction("mov x3, #75"); // default to the PHP wordwrap() width of 75 when omitted - } - if args.len() >= 3 { - super::args::emit_string_arg(&args[2], emitter, ctx, data); - emitter.instruction("mov x4, x1"); // move the break-string pointer into the secondary runtime string-argument pair - emitter.instruction("mov x5, x2"); // move the break-string length into the secondary runtime string-argument pair - } else { - let (label, len) = data.add_string(b"\n"); - abi::emit_symbol_address(emitter, "x4", &label); // materialize the default newline break string when the third argument is omitted - abi::emit_load_int_immediate(emitter, "x5", len as i64); // materialize the default newline break-string length - } - if args.len() >= 4 { - super::args::emit_int_arg(&args[3], emitter, ctx, data); - emitter.instruction("mov x6, x0"); // move the cut_long_words flag into the runtime argument register - } else { - emitter.instruction("mov x6, #0"); // default cut_long_words to false when omitted - } - emitter.instruction("ldp x1, x2, [sp], #16"); // restore the input string after evaluating the width, break, and cut flag - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the input string while evaluating the width and optional break string - if args.len() >= 2 { - super::args::emit_int_arg(&args[1], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the requested wrap width into the scalar x86_64 runtime argument register - } else { - emitter.instruction("mov rdi, 75"); // default to the PHP wordwrap() width of 75 when omitted - } - if args.len() >= 3 { - super::args::emit_string_arg(&args[2], emitter, ctx, data); - emitter.instruction("mov rcx, rax"); // move the break-string pointer into the secondary x86_64 runtime string-argument pair - emitter.instruction("mov r8, rdx"); // move the break-string length into the secondary x86_64 runtime string-argument pair - } else { - let (label, len) = data.add_string(b"\n"); - abi::emit_symbol_address(emitter, "rcx", &label); // materialize the default newline break string when the third argument is omitted - abi::emit_load_int_immediate(emitter, "r8", len as i64); // materialize the default newline break-string length - } - if args.len() >= 4 { - super::args::emit_int_arg(&args[3], emitter, ctx, data); - emitter.instruction("mov r9, rax"); // move the cut_long_words flag into the x86_64 runtime argument register - } else { - emitter.instruction("mov r9, 0"); // default cut_long_words to false when omitted - } - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the input string into the primary x86_64 string-helper input registers - } - } - abi::emit_call_label(emitter, "__rt_wordwrap"); // wrap the input string at word boundaries through the target-aware runtime helper - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/checkdate.rs b/src/codegen/builtins/system/checkdate.rs deleted file mode 100644 index 8ca03ac125..0000000000 --- a/src/codegen/builtins/system/checkdate.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Purpose: -//! Lowers the PHP `checkdate()` builtin: evaluates the month/day/year arguments and calls the -//! `__rt_checkdate` runtime helper, returning a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::system` dispatch for the `checkdate` builtin. -//! -//! Key details: -//! - Mirrors `mktime`'s argument marshalling (evaluate, coerce to int, materialize in ABI order), -//! then delegates the range/leap-year validation to `__rt_checkdate`. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits `checkdate($month, $day, $year)`: evaluates the three integer arguments into the ABI -/// argument registers and calls `__rt_checkdate`, yielding a `PhpType::Bool` (1 valid / 0 invalid). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("checkdate()"); - match emitter.target.arch { - Arch::AArch64 => { - // -- evaluate month, day, year; push reversed so they pop back in order -- - for i in (0..3).rev() { - let arg_ty = emit_expr(&args[i], emitter, ctx, data); - coerce_to_int(emitter, &arg_ty); // unbox a Mixed/Union argument into a raw integer before pushing it - emitter.instruction("str x0, [sp, #-16]!"); // push the evaluated integer argument onto the temporary stack - } - emitter.instruction("ldr x0, [sp], #16"); // restore the month argument into the first integer argument register - emitter.instruction("ldr x1, [sp], #16"); // restore the day argument into the second integer argument register - emitter.instruction("ldr x2, [sp], #16"); // restore the year argument into the third integer argument register - } - Arch::X86_64 => { - // -- evaluate month, day, year; push reversed so they pop back in order -- - for i in (0..3).rev() { - let arg_ty = emit_expr(&args[i], emitter, ctx, data); - coerce_to_int(emitter, &arg_ty); // unbox a Mixed/Union argument into a raw integer before pushing it - abi::emit_push_reg(emitter, "rax"); // push the evaluated integer argument onto the temporary x86_64 stack slot - } - abi::emit_pop_reg(emitter, "rdi"); // restore the month argument into the first SysV integer argument register - abi::emit_pop_reg(emitter, "rsi"); // restore the day argument into the second SysV integer argument register - abi::emit_pop_reg(emitter, "rdx"); // restore the year argument into the third SysV integer argument register - } - } - abi::emit_call_label(emitter, "__rt_checkdate"); // validate the Gregorian date and return the PHP boolean through the active target ABI - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/system/class_attribute_args.rs b/src/codegen/builtins/system/class_attribute_args.rs deleted file mode 100644 index 04914229b4..0000000000 --- a/src/codegen/builtins/system/class_attribute_args.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! Purpose: -//! Lowers `class_attribute_args()` calls into an indexed `array` of -//! literal class-attribute arguments captured during schema construction. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Attribute matching is case-insensitive, and each captured scalar is boxed -//! into a mixed cell before being appended to the result array. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::names::php_symbol_key; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{AttrArgEntry, AttrArgValue, PhpType}; - -/// Emits code for `class_attribute_args($class, $attr_name)`. -/// -/// Returns `PhpType::Array(Box::new(PhpType::Mixed))` on success; on error -/// (non-literal args, missing class, or absent attribute) returns early with -/// the same type so the caller can proceed. `ctx` provides the class lookup -/// via `ctx.classes`; attribute arguments are matched case-insensitively and -/// then boxed into mixed cells and pushed onto a newly allocated indexed array. -/// On x86_64 the result register is `rax`; on AArch64 it is `x0`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("class_attribute_args()"); - let class_name = match args.first().map(|a| &a.kind) { - Some(ExprKind::StringLiteral(name)) => name.clone(), - _ => return Some(PhpType::Array(Box::new(PhpType::Mixed))), - }; - let attr_name = match args.get(1).map(|a| &a.kind) { - Some(ExprKind::StringLiteral(name)) => name.clone(), - _ => return Some(PhpType::Array(Box::new(PhpType::Mixed))), - }; - - let attr_key = php_symbol_key(attr_name.trim_start_matches('\\')); - let attr_args: Vec = ctx - .classes - .get(super::resolve_class_name(ctx, &class_name)?) - .and_then(|info| { - info.attribute_names.iter().enumerate().find_map(|(idx, name)| { - let candidate_key = php_symbol_key(name.trim_start_matches('\\')); - if candidate_key == attr_key { - Some( - info.attribute_args - .get(idx) - .and_then(Clone::clone) - .unwrap_or_default(), - ) - } else { - None - } - }) - }) - .unwrap_or_default(); - - let result_reg = abi::int_result_reg(emitter); - - // -- allocate an empty indexed array of mixed-cell pointers -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", attr_args.len().max(1))); // initial capacity (≥1 to avoid grow on first push) - emitter.instruction("mov x1, #8"); // element stride: one heap pointer per slot - emitter.instruction("bl __rt_array_new"); // x0 = freshly allocated array pointer - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, {}", attr_args.len().max(1))); // initial capacity (≥1) - emitter.instruction("mov rsi, 8"); // element stride: one heap pointer per slot - emitter.instruction("call __rt_array_new"); // rax = array pointer - } - } - - // Stamp the array's value_type so later iteration knows each slot is a - // boxed mixed cell. The stamp lives in the heap header alongside the - // indexed-array marker — without it `foreach` would not unbox the - // mixed cells when the user iterates the result. - crate::codegen::expr::arrays::emit_array_value_type_stamp( - emitter, - result_reg, - &PhpType::Mixed, - ); - - // -- box each captured arg as a mixed cell and push the boxed pointer -- - for entry in &attr_args { - let arg = &entry.value; - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, result_reg); // save the array pointer across the boxing helper call - emit_box_arg_aarch64(arg, emitter, data); // x0 = boxed mixed-cell pointer for this arg - emitter.instruction("mov x1, x0"); // x1 = mixed-cell pointer (push helper's value arg) - emitter.instruction("ldr x0, [sp]"); // x0 = array pointer (push helper's array arg) - emitter.instruction("bl __rt_array_push_int"); // x0 = (possibly realloc'd) array pointer - abi::emit_release_temporary_stack(emitter, 16); // drop the saved array slot now that the helper returned the up-to-date pointer - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, result_reg); // save the array pointer across the boxing helper call - emit_box_arg_x86_64(arg, emitter, data); // rax = boxed mixed-cell pointer for this arg - emitter.instruction("mov rsi, rax"); // rsi = mixed-cell pointer (push helper's value arg) - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // rdi = array pointer (push helper's array arg) - emitter.instruction("call __rt_array_push_int"); // rax = updated array pointer - abi::emit_release_temporary_stack(emitter, 16); // drop the saved slot now that the helper returned the up-to-date pointer - } - } - } - - Some(PhpType::Array(Box::new(PhpType::Mixed))) -} - -/// Emits AArch64 instructions to box `arg` into a runtime mixed cell. -/// -/// Sets `x0` = runtime tag, `x1` = low word, `x2` = high word per the -/// mixed-cell ABI, then calls `__rt_mixed_from_value`. Caller saves the -/// array pointer on the stack before this call (see call site in `emit`). -/// For `Str` args the string is added to `data` as a literal and the symbol -/// address is materialized into `x1`; `data` is only mutated for `Str`. -fn emit_box_arg_aarch64(arg: &AttrArgValue, emitter: &mut Emitter, data: &mut DataSection) { - // Set (tag in x0, lo in x1, hi in x2) per the mixed-cell ABI, then call - // __rt_mixed_from_value. The helper persists strings and retains - // refcounted heap children; scalars (int/bool/null) flow straight to - // the alloc path with no ownership work. - match arg { - AttrArgValue::Null => { - emitter.instruction("mov x0, #8"); // runtime tag 8 = null payload - emitter.instruction("mov x1, xzr"); // null mixed payloads carry no low word - emitter.instruction("mov x2, xzr"); // null mixed payloads carry no high word - } - AttrArgValue::Int(value) => { - emitter.instruction("mov x0, #0"); // runtime tag 0 = integer payload - emitter.instruction(&format!("mov x1, #{}", value)); // x1 = int value (low word) - emitter.instruction("mov x2, xzr"); // integer mixed payloads do not use the high word - } - AttrArgValue::Float(bits) => { - emitter.instruction("mov x0, #2"); // runtime tag 2 = float payload - abi::emit_load_int_immediate(emitter, "x1", *bits as i64); // x1 = IEEE-754 bit pattern - emitter.instruction("mov x2, xzr"); // float mixed payloads do not use the high word - } - AttrArgValue::Bool(value) => { - emitter.instruction("mov x0, #3"); // runtime tag 3 = boolean payload - emitter.instruction(&format!("mov x1, #{}", *value as u64)); // x1 = 0 or 1 boolean low word - emitter.instruction("mov x2, xzr"); // boolean mixed payloads do not use the high word - } - AttrArgValue::Str(value) => { - let bytes = crate::string_bytes::literal_bytes(value); - let (sym, len) = data.add_string(&bytes); - emitter.instruction("mov x0, #1"); // runtime tag 1 = string payload - abi::emit_symbol_address(emitter, "x1", &sym); // x1 = string data address - emitter.instruction(&format!("mov x2, #{}", len)); // x2 = string length - } - AttrArgValue::Array(_) | AttrArgValue::ConstRef(_) | AttrArgValue::ScopedConst(..) => { - // Frozen legacy AST backend: nested arrays and deferred symbolic - // references (global/class constants, enum cases) are not - // materialized here; emit a null placeholder. The active EIR path - // builds the real value. - emitter.instruction("mov x0, #8"); // runtime tag 8 = null placeholder - emitter.instruction("mov x1, xzr"); // null carries no low word - emitter.instruction("mov x2, xzr"); // null carries no high word - } - } - emitter.instruction("bl __rt_mixed_from_value"); // box the captured payload into an owned mixed cell -} - -/// Emits x86_64 instructions to box `arg` into a runtime mixed cell. -/// -/// Sets `rax` = runtime tag, `rdi` = low word, `rsi` = high word per the -/// mixed-cell ABI, then calls `__rt_mixed_from_value`. Caller saves the -/// array pointer on the stack before this call (see call site in `emit`). -/// For `Str` args the string is added to `data` as a literal and the symbol -/// address is materialized into `rdi`; `data` is only mutated for `Str`. -fn emit_box_arg_x86_64(arg: &AttrArgValue, emitter: &mut Emitter, data: &mut DataSection) { - // Set (tag in rax, lo in rdi, hi in rsi) per the mixed-cell ABI on x86_64. - match arg { - AttrArgValue::Null => { - emitter.instruction("mov rax, 8"); // runtime tag 8 = null payload - emitter.instruction("xor rdi, rdi"); // null mixed payloads carry no low word - emitter.instruction("xor rsi, rsi"); // null mixed payloads carry no high word - } - AttrArgValue::Int(value) => { - emitter.instruction("mov rax, 0"); // runtime tag 0 = integer payload - emitter.instruction(&format!("mov rdi, {}", value)); // rdi = int value (low word) - emitter.instruction("xor rsi, rsi"); // integer mixed payloads do not use the high word - } - AttrArgValue::Float(bits) => { - emitter.instruction("mov rax, 2"); // runtime tag 2 = float payload - abi::emit_load_int_immediate(emitter, "rdi", *bits as i64); // rdi = IEEE-754 bit pattern - emitter.instruction("xor rsi, rsi"); // float mixed payloads do not use the high word - } - AttrArgValue::Bool(value) => { - emitter.instruction("mov rax, 3"); // runtime tag 3 = boolean payload - emitter.instruction(&format!("mov rdi, {}", *value as u64)); // rdi = 0 or 1 boolean low word - emitter.instruction("xor rsi, rsi"); // boolean mixed payloads do not use the high word - } - AttrArgValue::Str(value) => { - let bytes = crate::string_bytes::literal_bytes(value); - let (sym, len) = data.add_string(&bytes); - emitter.instruction("mov rax, 1"); // runtime tag 1 = string payload - abi::emit_symbol_address(emitter, "rdi", &sym); // rdi = string data address - emitter.instruction(&format!("mov rsi, {}", len)); // rsi = string length - } - AttrArgValue::Array(_) | AttrArgValue::ConstRef(_) | AttrArgValue::ScopedConst(..) => { - // Frozen legacy AST backend: nested arrays and deferred symbolic - // references (global/class constants, enum cases) are not - // materialized here; emit a null placeholder. The active EIR path - // builds the real value. - emitter.instruction("mov rax, 8"); // runtime tag 8 = null placeholder - emitter.instruction("xor rdi, rdi"); // null carries no low word - emitter.instruction("xor rsi, rsi"); // null carries no high word - } - } - emitter.instruction("call __rt_mixed_from_value"); // box the captured payload into an owned mixed cell -} diff --git a/src/codegen/builtins/system/class_attribute_names.rs b/src/codegen/builtins/system/class_attribute_names.rs deleted file mode 100644 index 79cfbe0528..0000000000 --- a/src/codegen/builtins/system/class_attribute_names.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Purpose: -//! Lowers `class_attribute_names()` calls into an indexed array of class-level -//! PHP attribute names captured during schema construction. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Class lookup is case-insensitive and resolved at compile time from a -//! string literal so the emitted code can unroll one push per attribute. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// `class_attribute_names($class)`: return an array of attribute name -/// strings attached to a class declaration. Currently the class argument -/// must be a compile-time string literal — at codegen time we look up -/// the `ClassInfo.attribute_names` list and emit a sequence of -/// `__rt_array_push_str` calls for each name. Dynamic class lookup -/// (string variable → class_id) is reserved for a future iteration. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("class_attribute_names()"); - let class_name = match args.first().map(|a| &a.kind) { - Some(ExprKind::StringLiteral(name)) => name.clone(), - _ => { - // Type checker already rejects non-literal arguments — this is a - // defensive fallback that returns an empty array of strings. - return Some(PhpType::Array(Box::new(PhpType::Str))); - } - }; - let names: Vec = ctx - .classes - .get(super::resolve_class_name(ctx, &class_name)?) - .map(|info| info.attribute_names.clone()) - .unwrap_or_default(); - - let result_reg = abi::int_result_reg(emitter); - - // -- allocate an empty indexed array -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", names.len().max(1))); // initial capacity (≥1 to avoid grow on first push) - emitter.instruction("mov x1, #16"); // element stride: ptr (8 B) + len (8 B) per string slot - emitter.instruction("bl __rt_array_new"); // x0 = freshly allocated array pointer - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, {}", names.len().max(1))); // initial capacity (≥1) - emitter.instruction("mov rsi, 16"); // element stride: ptr (8 B) + len (8 B) - emitter.instruction("call __rt_array_new"); // rax = array pointer - } - } - - // -- push each name string in source order -- - for name in &names { - let (sym, len) = data.add_string(name.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, result_reg); // save the array pointer across the push helper call - abi::emit_symbol_address(emitter, "x1", &sym); // x1 = attribute name string address - emitter.instruction(&format!("mov x2, #{}", len)); // x2 = attribute name string length - emitter.instruction("ldr x0, [sp]"); // reload the array pointer for the push helper - emitter.instruction("bl __rt_array_push_str"); // x0 = (possibly realloc'd) array pointer - abi::emit_release_temporary_stack(emitter, 16); // drop the saved array pointer slot now that the push returned the up-to-date pointer - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, result_reg); // save the array pointer across the push helper call - abi::emit_symbol_address(emitter, "rsi", &sym); // rsi = attribute name string address (System V arg 1 for str ptr) - emitter.instruction(&format!("mov rdx, {}", len)); // rdx = attribute name length (System V arg 2) - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // rdi = current array pointer for the push helper - emitter.instruction("call __rt_array_push_str"); // rax = updated array pointer - abi::emit_release_temporary_stack(emitter, 16); // drop the saved slot now that the helper returned the up-to-date pointer - } - } - } - - Some(PhpType::Array(Box::new(PhpType::Str))) -} diff --git a/src/codegen/builtins/system/class_get_attributes.rs b/src/codegen/builtins/system/class_get_attributes.rs deleted file mode 100644 index 41c1e88f98..0000000000 --- a/src/codegen/builtins/system/class_get_attributes.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Purpose: -//! Lowers `class_get_attributes()` into an indexed array of populated -//! synthetic `ReflectionAttribute` objects for class-level attributes. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Attribute-object construction is shared with `ReflectionClass`, -//! `ReflectionMethod`, and `ReflectionProperty` codegen so `getName()`, -//! `getArguments()`, and `newInstance()` agree across all reflection paths. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits codegen for `class_get_attributes($class)`. -/// -/// Returns an indexed array of populated `ReflectionAttribute` instances, -/// one per attribute attached to the class declaration. -/// -/// ## Arguments -/// - `$class` must be a compile-time string literal naming the class. -/// At codegen time, `ClassInfo.attribute_names` and `ClassInfo.attribute_args` -/// are walked to fully unroll the construction sequence. -/// -/// ## Fallback behavior -/// - If `$class` is not a string literal, returns `Some(Array>)` -/// without emitting any instructions. -/// - If the class cannot be resolved, returns `Some(Array>)` -/// without emitting any instructions. -/// -/// ## Ownership -/// - `class_info` is cloned from `ctx.classes`; no ownership is transferred. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("class_get_attributes()"); - let class_name = match args.first().map(|a| &a.kind) { - Some(ExprKind::StringLiteral(name)) => name.clone(), - _ => { - return Some(PhpType::Array(Box::new(PhpType::Object( - "ReflectionAttribute".to_string(), - )))) - } - }; - - let Some(class_info) = super::resolve_class_name(ctx, &class_name) - .and_then(|resolved| ctx.classes.get(resolved)) - .cloned() - else { - return Some(PhpType::Array(Box::new(PhpType::Object( - "ReflectionAttribute".to_string(), - )))); - }; - - Some(crate::codegen::reflection::emit_reflection_attribute_array( - &class_info.attribute_names, - &class_info.attribute_args, - emitter, - ctx, - data, - )) -} diff --git a/src/codegen/builtins/system/date.rs b/src/codegen/builtins/system/date.rs deleted file mode 100644 index 06316be345..0000000000 --- a/src/codegen/builtins/system/date.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Purpose: -//! Emits PHP `date` / `gmdate` time/date builtin calls. -//! Marshals timestamp and format arguments into runtime helpers that consult wall-clock state. -//! `gmdate` reuses the same marshalling and only targets the UTC runtime entry `__rt_gmdate`. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Time calls are effectful/non-deterministic and must preserve PHP scalar return conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `date(format[, timestamp])` and `gmdate(format[, timestamp])` builtins. -/// -/// Compiles to a call into `__rt_date` (local time) or `__rt_gmdate` (UTC) with the format -/// string pointer/length in the first string-argument registers and the timestamp in the -/// first integer register. When the timestamp argument is omitted, `-1` is passed to signal -/// the runtime to use the current wall-clock time. The runtime entry is chosen from `name`, -/// so both builtins share identical argument marshalling. -/// -/// # Arguments -/// - `name`: builtin name (`"date"` or `"gmdate"`), selects the runtime entry -/// - `args`: first arg is the format string, optional second arg is the Unix timestamp -/// - `emitter`: target-aware instruction emitter -/// - `ctx`: current codegen context (used by `emit_expr`) -/// - `data`: data section for relocatable strings/labels -/// -/// # Returns -/// `Some(PhpType::Str)` since `date()`/`gmdate()` always return a string. -/// -/// # Architecture behavior -/// - **AArch64**: format ptr/length in x1/x2, timestamp in x0, result in x0 -/// - **x86_64**: format ptr/length in rdi/rsi, timestamp in rax, result in rax -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let is_gmt = name == "gmdate"; - let runtime_label = if is_gmt { "__rt_gmdate" } else { "__rt_date" }; - emitter.comment(if is_gmt { "gmdate()" } else { "date()" }); - - match emitter.target.arch { - Arch::AArch64 => { - if args.len() == 2 { - // -- evaluate timestamp argument first -- - let ts_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &ts_ty); // unbox a Mixed/Union timestamp into a raw integer - emitter.instruction("str x0, [sp, #-16]!"); // push timestamp onto stack - - // -- evaluate format string -- - emit_expr(&args[0], emitter, ctx, data); - // x1=format ptr, x2=format len - - // -- pop timestamp into x0 -- - emitter.instruction("ldr x0, [sp], #16"); // pop timestamp from stack - } else { - // -- evaluate format string -- - emit_expr(&args[0], emitter, ctx, data); - // x1=format ptr, x2=format len - - // -- use -1 to signal "use current time" -- - emitter.instruction("mov x0, #-1"); // timestamp -1 = use current time - } - } - Arch::X86_64 => { - if args.len() == 2 { - // -- evaluate timestamp argument first -- - let ts_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &ts_ty); // unbox a Mixed/Union timestamp into a raw integer - abi::emit_push_reg(emitter, "rax"); // save the timestamp while the format-string expression is evaluated - - // -- evaluate format string -- - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the format-string pointer into the first x86_64 string-argument register - emitter.instruction("mov rsi, rdx"); // move the format-string length into the paired x86_64 string-argument register - abi::emit_pop_reg(emitter, "rax"); // restore the timestamp into the x86_64 integer result register - } else { - // -- evaluate format string -- - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the format-string pointer into the first x86_64 string-argument register - emitter.instruction("mov rsi, rdx"); // move the format-string length into the paired x86_64 string-argument register - emitter.instruction("mov rax, -1"); // timestamp -1 = use current time - } - } - } - - // -- call runtime: aarch64 x0/x1/x2, x86_64 rax/rdi/rsi -- - abi::emit_call_label(emitter, runtime_label); // format the timestamp through the local (date) or UTC (gmdate) runtime helper - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/date_default_timezone_get.rs b/src/codegen/builtins/system/date_default_timezone_get.rs deleted file mode 100644 index 687095c4e6..0000000000 --- a/src/codegen/builtins/system/date_default_timezone_get.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Purpose: -//! Emits PHP `date_default_timezone_get()` calls. -//! Delegates to the runtime helper that returns the stored timezone identifier (or `"UTC"`). -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Reads process-global timezone state; the returned pointer/length is an owned PHP string in the -//! string-result registers (`x1`/`x2` on ARM64, `rax`/`rdx` on x86_64). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP's `date_default_timezone_get()` builtin. -/// -/// Calls `__rt_date_default_timezone_get`, which returns the identifier set by -/// `date_default_timezone_set` (or the literal `"UTC"` when none was set) in the string-result -/// registers. -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("date_default_timezone_get()"); - abi::emit_call_label(emitter, "__rt_date_default_timezone_get"); // return the stored timezone identifier (or "UTC") in the string-result registers - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/date_default_timezone_set.rs b/src/codegen/builtins/system/date_default_timezone_set.rs deleted file mode 100644 index 9027bb60ff..0000000000 --- a/src/codegen/builtins/system/date_default_timezone_set.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Purpose: -//! Emits PHP `date_default_timezone_set()` calls. -//! Materializes the timezone-identifier string and hands it to the runtime helper that applies it -//! via libc `putenv`/`tzset`. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - The runtime sets process-global timezone state, so this call has observable side effects and -//! must not be folded away. Returns PHP `true`. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_result_to_type, emit_expr}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP's `date_default_timezone_set($timezoneId)` builtin. -/// -/// Evaluates the argument, coerces it to a string in the string-result registers -/// (`x1`/`x2` on ARM64, `rax`/`rdx` on x86_64), then calls `__rt_date_default_timezone_set` -/// which writes `"TZ="` to the static env buffer, applies it through libc `putenv`+`tzset`, -/// and returns the PHP boolean `true`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("date_default_timezone_set()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - // Coerce the identifier to a string so the runtime helper receives it in the string ABI - // registers (a Str is a no-op; a Mixed/Int is unboxed/stringified). - coerce_result_to_type(emitter, ctx, data, &ty, &PhpType::Str); - abi::emit_call_label(emitter, "__rt_date_default_timezone_set"); // apply TZ via libc; returns PHP true in the integer-result register - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/system/define.rs b/src/codegen/builtins/system/define.rs deleted file mode 100644 index 0385b4ac43..0000000000 --- a/src/codegen/builtins/system/define.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Purpose: -//! Emits PHP `define` calls for compile-time and runtime constant registration. -//! Tracks generated symbols that guard repeated defines and constant lookups. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Constant visibility must stay consistent with resolver/type-checker handling of PHP global constants. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::names::define_seen_symbol; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -const DEFINE_ALREADY_DEFINED_WARNING: &str = - "Warning: define(): Constant already defined\n"; - -/// Emits code for the PHP `define(name, value)` builtin. -/// -/// Stores the constant value in the context for compile-time resolution and -/// emits a runtime guard that checks whether the constant was already defined. -/// On repeated defines, emits a duplicate warning and returns `false`; -/// on first define, marks the constant as seen and returns `true`. -/// -/// # Arguments -/// * `name` - The builtin name (unused, dispatch is by arity/signature) -/// * `args` - `[name_expr, value_expr]` where `name_expr` must be a string literal -/// -/// # Returns -/// `Some(PhpType::Bool)` — `define()` always returns a boolean in PHP -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - // define("NAME", value) — store constant for compile-time resolution - let const_name = match &args[0].kind { - ExprKind::StringLiteral(s) => s.clone(), - _ => panic!("define() first argument must be a string literal"), - }; - - let ty = match &args[1].kind { - ExprKind::IntLiteral(_) => PhpType::Int, - ExprKind::FloatLiteral(_) => PhpType::Float, - ExprKind::StringLiteral(_) => PhpType::Str, - ExprKind::BoolLiteral(_) => PhpType::Bool, - ExprKind::Null => PhpType::Void, - _ => PhpType::Int, - }; - - ctx.constants - .entry(const_name.clone()) - .or_insert((args[1].kind.clone(), ty)); - - let flag_symbol = data.add_comm(define_seen_symbol(&const_name), 8); - emit_runtime_define_result(&flag_symbol, emitter, ctx); - - Some(PhpType::Bool) -} - -/// Emits the runtime portion of `define()` that guards against duplicate definitions. -/// -/// Reads the `flag_symbol` sentinel to determine if this is the first or a repeated -/// `define()` call at runtime. On first execution, stores `1` to the sentinel and -/// returns `true`. On repeated execution, emits a duplicate warning and returns `false`. -/// -/// # Arguments -/// * `flag_symbol` - BSS symbol that tracks whether this constant has been defined -fn emit_runtime_define_result(flag_symbol: &str, emitter: &mut Emitter, ctx: &mut Context) { - let first_label = ctx.next_label("define_first"); - let done_label = ctx.next_label("define_done"); - let result_reg = abi::int_result_reg(emitter); - - abi::emit_load_symbol_to_reg(emitter, result_reg, flag_symbol, 0); - abi::emit_branch_if_int_result_zero(emitter, &first_label); // first runtime execution defines the constant successfully - emit_duplicate_warning(emitter); - abi::emit_load_int_immediate(emitter, result_reg, 0); - abi::emit_jump(emitter, &done_label); // skip the first-define path after reporting the duplicate - - emitter.label(&first_label); - abi::emit_load_int_immediate(emitter, result_reg, 1); - abi::emit_store_reg_to_symbol(emitter, result_reg, flag_symbol, 0); - - emitter.label(&done_label); -} - -/// Emits a runtime warning for duplicate `define()` calls. -/// -/// Loads the `_diag_define_already_defined_msg` string pointer and length -/// into ABI argument registers and calls `__rt_diag_warning`. Target-specific: -/// - ARM64: loads into `x1` (pointer) and `x2` (length) -/// - x86_64: loads into `rdi` (pointer) and `esi` (length) -fn emit_duplicate_warning(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", "_diag_define_already_defined_msg"); - emitter.instruction(&format!("mov x2, #{}", DEFINE_ALREADY_DEFINED_WARNING.len())); // pass the warning byte length to the diagnostic helper - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", "_diag_define_already_defined_msg"); // pass the define() duplicate warning pointer to the diagnostic helper - emitter.instruction(&format!("mov esi, {}", DEFINE_ALREADY_DEFINED_WARNING.len())); // pass the warning byte length to the diagnostic helper - } - } - abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the duplicate define() runtime warning -} diff --git a/src/codegen/builtins/system/defined.rs b/src/codegen/builtins/system/defined.rs deleted file mode 100644 index 8023d3b853..0000000000 --- a/src/codegen/builtins/system/defined.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Purpose: -//! Emits PHP `defined()` checks for constants known to the ahead-of-time compiler. -//! Connects predefined and user-discovered constants to PHP boolean introspection. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - AOT mode requires a string-literal constant name so the result can be resolved during codegen. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits a boolean result for `defined("CONSTANT")`. -/// -/// The type checker rejects non-literal source calls. This emitter still handles -/// non-literal generated calls defensively by evaluating the argument and -/// returning `false` instead of panicking during deferred codegen. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("defined()"); - let constant_name = match &args[0].kind { - ExprKind::StringLiteral(name) => name.trim_start_matches('\\'), - _ => { - emit_expr(&args[0], emitter, ctx, _data); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - return Some(PhpType::Bool); - } - }; - let exists = ctx.constants.contains_key(constant_name); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), i64::from(exists)); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/system/exec_fn.rs b/src/codegen/builtins/system/exec_fn.rs deleted file mode 100644 index deee6c6d66..0000000000 --- a/src/codegen/builtins/system/exec_fn.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Purpose: -//! Emits PHP `exec` process-control or shell execution builtin calls. -//! Marshals command/status arguments into runtime helpers with PHP-visible output and exit behavior. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Process calls are effectful and may terminate or emit output, so lowering must preserve evaluation order. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the `exec()` builtin, which executes a shell command and returns its output. -/// Takes a command string expression, evaluates it, then calls `__rt_shell_exec` to run the command. -/// Returns the captured output as a string (last line of stdout) via x1=ptr, x2=len. -/// This is effectful: execution may emit output, terminate the process, or produce side effects. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("exec()"); - // -- evaluate command string -- - emit_expr(&args[0], emitter, ctx, data); - // -- call runtime to execute command and capture output -- - abi::emit_call_label(emitter, "__rt_shell_exec"); // execute command via the target-aware shell helper → ptr/len result regs - // exec() returns the last line of output (same as shell_exec for simplicity) - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/exit.rs b/src/codegen/builtins/system/exit.rs deleted file mode 100644 index b8f17470e2..0000000000 --- a/src/codegen/builtins/system/exit.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Purpose: -//! Emits PHP `exit` process-control or shell execution builtin calls. -//! Marshals command/status arguments into runtime helpers with PHP-visible output and exit behavior. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Process calls are effectful and may terminate or emit output, so lowering must preserve evaluation order. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::{Arch, Platform}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `exit` builtin, which terminates the current process. -/// -/// If `args` contains an expression, it is evaluated first and its integer value is used as -/// the exit code. If `args` is empty, the exit code is 0. -/// -/// Arguments are evaluated in source order before the exit syscall/instruction is emitted, -/// ensuring any side effects (e.g. `echo`) are observable. After evaluation the process is -/// terminated via the target's native exit ABI — there is no return. -/// -/// Returns `PhpType::Void`. All platforms set the integer result register to the exit code -/// before invoking the exit syscall/instruction. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("exit()"); - if let Some(arg) = args.first() { - emit_expr(arg, emitter, ctx, data); - } else { - // -- default exit code when no argument given -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // set exit code to 0 (success) - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // set exit code to 0 (success) in the native integer result register - } - } - } - // -- terminate the process using the target's native exit ABI -- - match (emitter.target.platform, emitter.target.arch) { - (Platform::MacOS, Arch::AArch64) | (Platform::Linux, Arch::AArch64) => { - emitter.syscall(1); // invoke the platform exit syscall using the integer result register as the code - } - (Platform::Linux, Arch::X86_64) => { - emitter.instruction("mov rdi, rax"); // move the computed exit code into the SysV first-argument register - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate the process through the Linux x86_64 syscall ABI - } - (Platform::MacOS, Arch::X86_64) => { - panic!("exit() is not implemented yet for target macos-x86_64"); - } - } - - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/system/getdate.rs b/src/codegen/builtins/system/getdate.rs deleted file mode 100644 index 8e4c7c8979..0000000000 --- a/src/codegen/builtins/system/getdate.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Purpose: -//! Lowers the PHP `getdate()` builtin: evaluates the optional timestamp argument (defaulting to the -//! current time) and calls the `__rt_getdate` runtime helper, which returns the associative array. -//! -//! Called from: -//! - `crate::codegen::builtins::system` dispatch for the `getdate` builtin. -//! -//! Key details: -//! - With no argument, passes the `-1` sentinel so the runtime substitutes the current time (matching -//! the `date()`/`__rt_date` convention). The result is the Mixed assoc-array hash pointer. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits `getdate([$timestamp])`: materializes the timestamp (or `-1` for the current time) in the -/// integer result register, then calls `__rt_getdate`, yielding the `PhpType::Mixed` assoc array. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("getdate()"); - if args.is_empty() { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #-1"); // -1 sentinel → runtime uses the current time - } - Arch::X86_64 => { - emitter.instruction("mov rax, -1"); // -1 sentinel → runtime uses the current time - } - } - } else { - let arg_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &arg_ty); // unbox a Mixed/Union timestamp into a raw integer - } - abi::emit_call_label(emitter, "__rt_getdate"); // build the getdate associative array → hash pointer - // Box the raw hash pointer into a Mixed cell (runtime tag 5 = assoc array), like stat(). - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // x1 = hash pointer (low payload word) - emitter.instruction("mov x2, #0"); // x2 = high payload word (unused) - emitter.instruction("mov x0, #5"); // x0 = runtime tag 5 (assoc array) - emitter.instruction("bl __rt_mixed_from_value"); // → x0 = boxed mixed cell - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // rdi = hash pointer (low payload word) - emitter.instruction("mov rsi, 0"); // rsi = high payload word (unused) - emitter.instruction("mov rax, 5"); // rax = runtime tag 5 (assoc array) - emitter.instruction("call __rt_mixed_from_value"); // → rax = boxed mixed cell - } - } - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/system/getenv.rs b/src/codegen/builtins/system/getenv.rs deleted file mode 100644 index 173aaa6bcc..0000000000 --- a/src/codegen/builtins/system/getenv.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Emits PHP `getenv` environment/platform information builtin calls. -//! Delegates host environment lookup or platform string construction to runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Environment and platform state are observable and must not be folded as compile-time constants here. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `getenv()` builtin. -/// -/// Emits the environment variable name expression, then calls `__rt_getenv` -/// to perform the runtime environment lookup. The result is always a string -/// on success, or `false` if the variable is not set. -/// -/// # Arguments -/// * `_name` — unused, matches the dispatcher signature -/// * `args` — must contain exactly one expression: the environment variable name -/// * `emitter` — target assembly emitter -/// * `ctx` — codegen context (variables, scope) -/// * `data` — data section for literal payloads -/// -/// # Returns -/// `Some(PhpType::Str)` — the lookup result is typed as a string (may be false at runtime) -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("getenv()"); - // -- evaluate the environment variable name string -- - emit_expr(&args[0], emitter, ctx, data); - // -- convert to C string and call getenv -- - abi::emit_call_label(emitter, "__rt_getenv"); // get env var through the target-aware runtime helper → ptr/len result regs - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/hrtime.rs b/src/codegen/builtins/system/hrtime.rs deleted file mode 100644 index d405151ad1..0000000000 --- a/src/codegen/builtins/system/hrtime.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Purpose: -//! Lowers the PHP `hrtime()` builtin: evaluates the optional as-number flag and calls the -//! `__rt_hrtime` runtime helper, which returns the already-boxed Mixed result. -//! -//! Called from: -//! - `crate::codegen::builtins::system` dispatch for the `hrtime` builtin. -//! -//! Key details: -//! - The as-number flag defaults to `0` (return a `[sec, nsec]` array). `__rt_hrtime` boxes its own -//! result (a Mixed int or a Mixed assoc array), so the emitter just forwards it. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits `hrtime([$as_number])`: materializes the flag (defaulting to `0`) in the integer argument -/// register and calls `__rt_hrtime`, returning the `PhpType::Mixed` result it boxes. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("hrtime()"); - if args.is_empty() { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // as_number defaults to false (return the array) - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // as_number defaults to false (return the array) - } - } - } else { - let arg_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &arg_ty); // coerce the bool/int as-number flag to an integer - } - abi::emit_call_label(emitter, "__rt_hrtime"); // read the monotonic clock and return the boxed Mixed result - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/system/json_decode.rs b/src/codegen/builtins/system/json_decode.rs deleted file mode 100644 index 04eb29e60f..0000000000 --- a/src/codegen/builtins/system/json_decode.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Purpose: -//! Emits PHP `json_decode` JSON builtin calls. -//! Marshals PHP scalar, array, and Mixed values into runtime JSON helpers and error state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - JSON error state is runtime-global observable state and must stay coupled to json_last_error(). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_string, coerce_to_truthiness, emit_expr}; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits the `json_decode` builtin call. -/// -/// Handles PHP argument evaluation order: all arguments are evaluated before the -/// builtin mutates JSON error state or call configuration. Emits zero to -/// `_json_last_error`, `_json_active_depth`, and `_json_active_flags` before -/// decoding. The depth argument is decremented by 1 to match PHP's strict -/// `active_depth >= depth` semantics (depth=1 → limit=0 → top-level fails). -/// Returns `PhpType::Mixed` regardless of decode success/failure; callers -/// receive a uniform boxed result shape. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("json_decode()"); - - let json_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_string(emitter, ctx, data, &json_ty); - abi::emit_call_label(emitter, "__rt_str_persist"); // keep the JSON source stable while optional arguments evaluate - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the JSON source until validation and decoding - - let assoc_arg = evaluate_assoc_arg(args, emitter, ctx, data); - if let Some(depth_expr) = args.get(2) { - emit_expr(depth_expr, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the depth argument until later arguments have evaluated - } - if let Some(flag_expr) = args.get(3) { - emit_expr(flag_expr, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the flags argument until runtime JSON state is updated - } - - // PHP evaluates every argument before the builtin mutates JSON error - // state or call configuration. - abi::emit_store_zero_to_symbol(emitter, "_json_last_error", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_active_depth", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_error_location_active", 0); - - if args.get(3).is_some() { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_json_active_flags", - 0, - ); - if matches!(assoc_arg, AssocArg::FromFlags) { - write_assoc_from_flags(emitter); - } - } else { - abi::emit_store_zero_to_symbol(emitter, "_json_active_flags", 0); - if matches!(assoc_arg, AssocArg::FromFlags) { - abi::emit_store_zero_to_symbol(emitter, "_json_decode_assoc", 0); // missing/null associative arg and no flag → stdClass - } - } - // PHP json_decode rejects nesting when active_depth >= depth (strict). - // The shared __rt_json_depth_enter compares `active <= limit` so we - // subtract 1 from the user-supplied depth here to get the same - // observable behavior (depth=1 → limit=0 → top-level container fails). - if args.get(2).is_some() { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - let reg = abi::int_result_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("sub {reg}, {reg}, #1")), // strict-semantic offset for json_decode - Arch::X86_64 => emitter.instruction(&format!("sub {reg}, 1")), // strict-semantic offset for json_decode - } - abi::emit_store_reg_to_symbol(emitter, reg, "_json_depth_limit", 0); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 511); - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_json_depth_limit", - 0, - ); - } - - if matches!(assoc_arg, AssocArg::Explicit) { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_json_decode_assoc", - 0, - ); - } - - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - abi::emit_store_reg_to_symbol(emitter, ptr_reg, "_json_error_source_ptr", 0); - let done_label = ctx.next_label("json_decode_done"); - match emitter.target.arch { - Arch::AArch64 => { - // x1 = string ptr, x2 = string len after emit_expr. - emitter.instruction("bl __rt_json_decode_mixed"); // checked structural decoder; returns 0 after recording JSON errors - emitter.instruction(&format!("cbnz x0, {}", done_label)); // valid input already returned a boxed Mixed value - // Invalid: return Mixed(null) after the decoder recorded the error. - emitter.instruction("mov x0, #8"); // tag = 8 (null) - emitter.instruction("mov x1, #0"); // value_lo = 0 - emitter.instruction("mov x2, #0"); // value_hi = 0 - emitter.instruction("bl __rt_mixed_from_value"); // box Mixed(null) so callers see a uniform result shape - emitter.label(&done_label); - } - Arch::X86_64 => { - // rax = string ptr, rdx = string len after emit_expr. - emitter.instruction("call __rt_json_decode_mixed"); // checked structural decoder honoring _json_decode_assoc - emitter.instruction("test rax, rax"); // valid input returns a boxed Mixed pointer - emitter.instruction(&format!("jne {}", done_label)); // non-zero result is ready for the caller - // Invalid: return Mixed(null) after the decoder recorded the error. - emitter.instruction("mov rax, 8"); // tag = 8 (null) - emitter.instruction("mov rdi, 0"); // value_lo = 0 - emitter.instruction("mov rsi, 0"); // value_hi = 0 - emitter.instruction("call __rt_mixed_from_value"); // box Mixed(null) so callers see a uniform result shape - emitter.label(&done_label); - } - } - - Some(PhpType::Mixed) -} - -/// Evaluate the `$associative` argument in PHP source order. -/// -/// PHP semantics: missing or `null` → false (stdClass), `false` → false, -/// `true` → true. When `$associative` is missing/null, `JSON_OBJECT_AS_ARRAY` -/// in the flags argument chooses the shape. Dynamic non-null expressions use -/// normal PHP truthiness and are stored after all later arguments evaluate. -fn evaluate_assoc_arg( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> AssocArg { - if args.len() < 2 || matches!(args[1].kind, ExprKind::Null) { - return AssocArg::FromFlags; - } - if let ExprKind::BoolLiteral(value) = args[1].kind { - let scratch = abi::int_result_reg(emitter); - abi::emit_load_int_immediate(emitter, scratch, if value { 1 } else { 0 }); - abi::emit_push_reg(emitter, scratch); // preserve the explicit associative literal until JSON state is updated - return AssocArg::Explicit; - } - let ty = emit_expr(&args[1], emitter, ctx, data); - if ty == PhpType::Void { - return AssocArg::FromFlags; - } - coerce_to_truthiness(emitter, ctx, &ty); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the truthiness result until later arguments have evaluated - AssocArg::Explicit -} - -/// Extracts `JSON_OBJECT_AS_ARRAY` from the flags register and stores it in -/// `_json_decode_assoc`. Called when `$associative` is null/missing and flags -/// were provided; the flag bit determines whether decoded objects become arrays -/// or stdClass instances. -fn write_assoc_from_flags(emitter: &mut Emitter) { - let reg = abi::int_result_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("and {reg}, {reg}, #1")); // keep JSON_OBJECT_AS_ARRAY when associative is null/missing - } - Arch::X86_64 => { - emitter.instruction(&format!("and {reg}, 1")); // keep JSON_OBJECT_AS_ARRAY when associative is null/missing - } - } - abi::emit_store_reg_to_symbol(emitter, reg, "_json_decode_assoc", 0); -} - -/// Tracks how the `$associative` argument was provided to `json_decode`. -enum AssocArg { - Explicit, - FromFlags, -} diff --git a/src/codegen/builtins/system/json_encode.rs b/src/codegen/builtins/system/json_encode.rs deleted file mode 100644 index 99a8e3f428..0000000000 --- a/src/codegen/builtins/system/json_encode.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! Purpose: -//! Emits PHP `json_encode` JSON builtin calls. -//! Marshals PHP scalar, array, and Mixed values into runtime JSON helpers and error state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - JSON error state is runtime-global observable state and must stay coupled to json_last_error(). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `json_encode(value, flags, depth)` builtin call. -/// -/// Dispatches to the appropriate runtime JSON encoder based on the PHP type of -/// `value`. All arguments are evaluated before any global JSON state is mutated, -/// preserving PHP's left-to-right evaluation semantics. -/// -/// # Arguments -/// * `_name` — unused, matches the builtin dispatcher signature -/// * `args[0]` — the value to encode -/// * `args[1]` — optional JSON encoding flags (e.g. `JSON_PRETTY_PRINT`) -/// * `args[2]` — optional maximum nesting depth -/// -/// # Returns -/// Always returns `PhpType::Mixed` since `json_encode` can produce a string or `false`. -/// -/// # Side effects -/// Resets `_json_last_error`, `_json_active_depth`, `_json_indent_depth`, -/// `_json_depth_limit`, and `_json_active_flags` global symbols before dispatch. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("json_encode()"); - - let ty = emit_expr(&args[0], emitter, ctx, data); - persist_string_result_if_needed(&ty, emitter); - abi::emit_push_result_value(emitter, &ty); - - if let Some(flag_expr) = args.get(1) { - emit_expr(flag_expr, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep json_encode flags stable while later arguments evaluate - } - if let Some(depth_expr) = args.get(2) { - emit_expr(depth_expr, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep json_encode depth stable until all argument side effects are done - } - - // PHP evaluates every argument before the builtin mutates global JSON - // error/configuration state. - abi::emit_store_zero_to_symbol(emitter, "_json_last_error", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_active_depth", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_indent_depth", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_error_location_active", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_error_source_ptr", 0); - - if args.get(2).is_some() { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_json_depth_limit", - 0, - ); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 512); - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_json_depth_limit", - 0, - ); - } - if args.get(1).is_some() { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_json_active_flags", - 0, - ); - } else { - abi::emit_store_zero_to_symbol(emitter, "_json_active_flags", 0); - } - - restore_result_value(emitter, &ty); - - match ty { - PhpType::Int => { - // -- convert integer to JSON (just itoa) -- - abi::emit_call_label(emitter, "__rt_itoa"); // convert the integer payload into a JSON decimal string for the active target ABI - } - PhpType::Float => { - // -- convert float to JSON, rejecting Inf/NaN -- - abi::emit_call_label(emitter, "__rt_json_encode_float"); // detect Inf/NaN, set JSON_ERROR_INF_OR_NAN, throw if requested, then encode - } - PhpType::Bool => { - // -- convert bool to JSON "true"/"false" -- - abi::emit_call_label(emitter, "__rt_json_encode_bool"); // convert the bool payload into the JSON literals true/false for the active target ABI - } - PhpType::Str => { - // -- wrap string with JSON quotes and escape special chars -- - abi::emit_call_label(emitter, "__rt_json_encode_str"); // escape and quote the string payload into JSON using the active target ABI - } - PhpType::Void => { - // -- null → "null" -- - abi::emit_call_label(emitter, "__rt_json_encode_null"); // produce the JSON null literal using the active target ABI - } - PhpType::Array(ref elem_ty) => { - match elem_ty.as_ref() { - PhpType::Int => { - // x0 = array pointer - abi::emit_call_label(emitter, "__rt_json_encode_array_int"); // encode an integer array to JSON using the active target ABI - } - PhpType::Str => { - // x0 = array pointer - abi::emit_call_label(emitter, "__rt_json_encode_array_str"); // encode a string array to JSON using the active target ABI - } - _ => { - // Fallback: inspect the packed runtime value_type tag per array - abi::emit_call_label(emitter, "__rt_json_encode_array_dynamic"); // encode the array to JSON by inspecting its runtime value_type tag - } - } - } - PhpType::AssocArray { .. } => { - // x0 = hash table pointer - abi::emit_call_label(emitter, "__rt_json_encode_assoc"); // encode the associative array to JSON using the active target ABI - } - PhpType::Iterable => { - emit_json_encode_iterable(emitter, ctx); - } - PhpType::Object(class_name) => { - if crate::types::checker::builtin_stdclass::is_stdclass(&class_name) { - // stdClass has no static descriptor; encode the dynamic - // property hash through the assoc-array encoder. - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldr x0, [x0, #8]"); // load the dynamic-property hash from obj+8 - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // load the dynamic-property hash from obj+8 - } - } - abi::emit_call_label(emitter, "__rt_json_encode_stdclass"); // encode the hash through the stdClass-aware encoder (empty hash → `{}`) - } else { - // x0 = object pointer; dispatches to JsonSerializable when present. - abi::emit_call_label(emitter, "__rt_json_encode_object"); // encode the object via the per-class JSON descriptor walker - } - } - PhpType::Mixed => { - // x0 = boxed mixed pointer - abi::emit_call_label(emitter, "__rt_json_encode_mixed"); // inspect the boxed payload and encode it as JSON for the active target ABI - } - _ => { - // Fallback: encode as "null" - abi::emit_call_label(emitter, "__rt_json_encode_null"); // produce the JSON null literal for unsupported payloads - } - } - - box_json_encode_result(emitter, ctx); - - Some(PhpType::Mixed) -} - -/// Emits dispatch code for `PhpType::Iterable` values. -/// -/// Probes the iterable's runtime heap kind via `__rt_heap_kind` and branches to -/// the appropriate JSON encoder: indexed array, associative array, object, or null. -/// -/// Preserves the iterable pointer on the stack while probing and restores it for the -/// selected encoder. Uses target-specific comparison instructions (AArch64 `cmp`/`b.eq` -/// vs x86_64 `cmp`/`je`). -fn emit_json_encode_iterable(emitter: &mut Emitter, ctx: &mut Context) { - let indexed_case = ctx.next_label("json_encode_iter_indexed"); - let assoc_case = ctx.next_label("json_encode_iter_assoc"); - let object_case = ctx.next_label("json_encode_iter_object"); - let null_case = ctx.next_label("json_encode_iter_null"); - let done = ctx.next_label("json_encode_iter_done"); - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterable pointer while probing its heap kind - abi::emit_call_label(emitter, "__rt_heap_kind"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #2"); // check whether the iterable is backed by an indexed array - emitter.instruction(&format!("b.eq {}", indexed_case)); // encode indexed-array iterables with the array encoder - emitter.instruction("cmp x0, #3"); // check whether the iterable is backed by a hash table - emitter.instruction(&format!("b.eq {}", assoc_case)); // encode hash-backed iterables with the associative encoder - emitter.instruction("cmp x0, #4"); // check whether the iterable is backed by an object - emitter.instruction(&format!("b.eq {}", object_case)); // encode object-backed iterables with the object encoder - emitter.instruction(&format!("b {}", null_case)); // unknown iterable heap kinds encode as JSON null - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 2"); // check whether the iterable is backed by an indexed array - emitter.instruction(&format!("je {}", indexed_case)); // encode indexed-array iterables with the array encoder - emitter.instruction("cmp rax, 3"); // check whether the iterable is backed by a hash table - emitter.instruction(&format!("je {}", assoc_case)); // encode hash-backed iterables with the associative encoder - emitter.instruction("cmp rax, 4"); // check whether the iterable is backed by an object - emitter.instruction(&format!("je {}", object_case)); // encode object-backed iterables with the object encoder - emitter.instruction(&format!("jmp {}", null_case)); // unknown iterable heap kinds encode as JSON null - } - } - - emitter.label(&indexed_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the iterable array pointer for JSON encoding - abi::emit_call_label(emitter, "__rt_json_encode_array_dynamic"); - abi::emit_jump(emitter, &done); - - emitter.label(&assoc_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the iterable hash pointer for JSON encoding - abi::emit_call_label(emitter, "__rt_json_encode_assoc"); - abi::emit_jump(emitter, &done); - - emitter.label(&object_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the iterable object pointer for JSON encoding - abi::emit_call_label(emitter, "__rt_json_encode_object"); - abi::emit_jump(emitter, &done); - - emitter.label(&null_case); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // discard the unknown iterable pointer before encoding null - abi::emit_call_label(emitter, "__rt_json_encode_null"); - - emitter.label(&done); -} - -/// Persists the string result of `emit_expr` if the type is `PhpType::Str`. -/// -/// Calls `__rt_str_persist` to ensure the string value is not invalidated by -/// subsequent argument evaluations (e.g. nested `json_encode` calls). -fn persist_string_result_if_needed(ty: &PhpType, emitter: &mut Emitter) { - if ty.codegen_repr() == PhpType::Str { - abi::emit_call_label(emitter, "__rt_str_persist"); // keep the string value stable while later json_encode arguments evaluate - } -} - -/// Restores the expression result value from the stack after flags/depth are evaluated. -/// -/// Pops the appropriate register or register pair depending on the type: -/// * `Float` → `float_result_reg` -/// * `Str` → register pair (ptr, len) -/// * `Void`/`Never` → nothing -/// * other → `int_result_reg` -fn restore_result_value(emitter: &mut Emitter, ty: &PhpType) { - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - } - } -} - -/// Boxes the raw JSON string result into a `PhpType::Mixed` heap cell. -/// -/// Checks `_json_last_error` after encoding. If an error occurred and -/// `JSON_PARTIAL_OUTPUT_ON_ERROR` is not set, returns `false` as a `PhpType::Bool` -/// boxed cell. Otherwise returns the JSON string as a `PhpType::Str` boxed cell. -/// -/// Preserves the string result on the stack while checking error state; the string -/// pointer/length are in `x1`/`x2` (AArch64) or `rax`/`rdx` (x86_64) after the -/// runtime encoder returns. -fn box_json_encode_result(emitter: &mut Emitter, ctx: &mut Context) { - let string_label = ctx.next_label("json_encode_string_result"); - let done_label = ctx.next_label("json_encode_boxed_result"); - - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the encoded JSON string while checking failure state - abi::emit_load_symbol_to_reg(emitter, "x9", "_json_last_error", 0); - emitter.instruction(&format!("cbz x9, {}", string_label)); // no JSON error means the string result is valid - abi::emit_load_symbol_to_reg(emitter, "x9", "_json_active_flags", 0); - emitter.instruction("tst x9, #512"); // JSON_PARTIAL_OUTPUT_ON_ERROR keeps the partial string result - emitter.instruction(&format!("b.ne {}", string_label)); // partial-output flag means return the encoded string - abi::emit_pop_reg_pair(emitter, "x10", "x11"); // discard the partial string result before returning false - emitter.instruction("mov x0, #0"); // false payload for json_encode failure - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Bool); - emitter.instruction(&format!("b {}", done_label)); // skip the string boxing path after returning false - emitter.label(&string_label); - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the successful JSON string result - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Str); - emitter.label(&done_label); - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // preserve the encoded JSON string while checking failure state - abi::emit_load_symbol_to_reg(emitter, "r10", "_json_last_error", 0); // load the current JSON error code - emitter.instruction("test r10, r10"); // check whether the encoder reported an error - emitter.instruction(&format!("jz {}", string_label)); // no JSON error means the string result is valid - abi::emit_load_symbol_to_reg(emitter, "r10", "_json_active_flags", 0); // load the active JSON flag bitmask - emitter.instruction("test r10, 512"); // JSON_PARTIAL_OUTPUT_ON_ERROR keeps the partial string result - emitter.instruction(&format!("jnz {}", string_label)); // partial-output flag means return the encoded string - abi::emit_pop_reg_pair(emitter, "r10", "r11"); // discard the partial string result before returning false - emitter.instruction("xor eax, eax"); // false payload for json_encode failure - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Bool); - emitter.instruction(&format!("jmp {}", done_label)); // skip the string boxing path after returning false - emitter.label(&string_label); - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the successful JSON string result - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Str); - emitter.label(&done_label); - } - } -} diff --git a/src/codegen/builtins/system/json_last_error.rs b/src/codegen/builtins/system/json_last_error.rs deleted file mode 100644 index db8fcd6a31..0000000000 --- a/src/codegen/builtins/system/json_last_error.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Purpose: -//! Emits PHP `json_last_error` JSON builtin calls. -//! Loads the runtime-global JSON error state as an integer result. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - JSON error state is runtime-global observable state and must stay coupled to json_last_error(). - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `json_last_error()` builtin call. -/// -/// Loads the last JSON error code from the runtime's `_json_last_error` BSS symbol -/// into the ABI-defined integer result register and returns `PhpType::Int`. -/// -/// The symbol is updated by `json_encode`/`json_decode`/`json_validate` runtime -/// routines and zeroed on each successful entry; until those wirings land it -/// stays at 0 (JSON_ERROR_NONE). -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("json_last_error()"); - // Loads the last JSON error code from the runtime's BSS symbol. The - // symbol is updated by encode/decode/validate runtimes and zeroed at - // each successful entry; until those wirings land it stays at 0 - // (JSON_ERROR_NONE). - abi::emit_load_symbol_to_reg(emitter, abi::int_result_reg(emitter), "_json_last_error", 0); - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/system/json_last_error_msg.rs b/src/codegen/builtins/system/json_last_error_msg.rs deleted file mode 100644 index 356b8c11de..0000000000 --- a/src/codegen/builtins/system/json_last_error_msg.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Purpose: -//! Emits codegen for `json_last_error_msg()`. -//! Bridges the PHP builtin to the runtime lookup table that materializes the current JSON error string. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()` when lowering builtin calls. -//! -//! Key details: -//! - The runtime owns message selection; this emitter only performs the call and reports a string result. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `json_last_error_msg()` builtin, which returns the error message -/// from the last `json_encode()`, `json_decode()`, or `json_validate()` call. -/// -/// Calls the runtime routine `__rt_json_last_error_msg`, which reads the runtime-global -/// JSON error state and returns a pointer/length string to the appropriate error description. -/// Returns `PhpType::Str`. Arguments are ignored (the function takes no parameters). -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("json_last_error_msg()"); - abi::emit_call_label(emitter, "__rt_json_last_error_msg"); - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/json_validate.rs b/src/codegen/builtins/system/json_validate.rs deleted file mode 100644 index d9487c0639..0000000000 --- a/src/codegen/builtins/system/json_validate.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Purpose: -//! Emits codegen for `json_validate()`. -//! Evaluates JSON source, depth, and flags in PHP source order before configuring runtime validation state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()` when lowering builtin calls. -//! -//! Key details: -//! - The JSON string is persisted before optional arguments run so side effects cannot invalidate the input slice. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_string, emit_expr}; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `json_validate()` builtin call. -/// -/// Evaluates arguments in PHP source order before configuring runtime JSON validation state. -/// - `json_str` (arg 0): JSON string to validate; coerced to string and persisted before optional args evaluate. -/// - `depth` (arg 1, optional): Nesting limit; defaults to 511, then decremented by 1 for strict semantics. -/// - `flags` (arg 2, optional): Only `JSON_INVALID_UTF8_IGNORE` is accepted; all other flags are masked out. -/// -/// Configures the runtime symbols `_json_last_error`, `_json_active_depth`, `_json_depth_limit`, -/// and `_json_active_flags`. Calls `__rt_json_validate` and returns `PhpType::Bool`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("json_validate()"); - - let json_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_string(emitter, ctx, data, &json_ty); - abi::emit_call_label(emitter, "__rt_str_persist"); // keep the JSON source stable while optional arguments evaluate - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the JSON source until the validator call - - if let Some(depth_expr) = args.get(1) { - emit_expr(depth_expr, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the depth argument until flags have evaluated - } - if let Some(flag_expr) = args.get(2) { - emit_expr(flag_expr, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the flag argument until JSON runtime state is updated - } - - // PHP evaluates arguments before the builtin clears error state or writes - // runtime JSON configuration. - abi::emit_store_zero_to_symbol(emitter, "_json_last_error", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_active_depth", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_error_location_active", 0); - abi::emit_store_zero_to_symbol(emitter, "_json_error_source_ptr", 0); - - if args.get(2).is_some() { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - mask_json_validate_flags(emitter); - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_json_active_flags", - 0, - ); - } else { - abi::emit_store_zero_to_symbol(emitter, "_json_active_flags", 0); - } - - // PHP json_validate rejects nesting when active_depth >= depth (strict). - // The shared __rt_json_depth_enter compares `active <= limit` so we - // subtract 1 from the user-supplied depth to align (depth=1 → limit=0 - // → top-level container fails). - if args.get(1).is_some() { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - let reg = abi::int_result_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("sub {reg}, {reg}, #1")), // strict-semantic offset for json_validate - Arch::X86_64 => emitter.instruction(&format!("sub {reg}, 1")), // strict-semantic offset for json_validate - } - abi::emit_store_reg_to_symbol(emitter, reg, "_json_depth_limit", 0); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 511); - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_json_depth_limit", - 0, - ); - } - - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - abi::emit_call_label(emitter, "__rt_json_validate"); - Some(PhpType::Bool) -} - -/// Masks unsupported validate flags, keeping only `JSON_INVALID_UTF8_IGNORE` (bit 20). -/// -/// Reads and modifies the current integer result register in-place. -fn mask_json_validate_flags(emitter: &mut Emitter) { - let reg = abi::int_result_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x9, #1048576"); // mask = JSON_INVALID_UTF8_IGNORE, the only json_validate flag PHP allows - emitter.instruction(&format!("and {reg}, {reg}, x9")); // ignore dynamically supplied unsupported validate flags - } - Arch::X86_64 => { - emitter.instruction(&format!("and {reg}, 1048576")); // keep only JSON_INVALID_UTF8_IGNORE for dynamic validate flags - } - } -} diff --git a/src/codegen/builtins/system/localtime.rs b/src/codegen/builtins/system/localtime.rs deleted file mode 100644 index 22885a4415..0000000000 --- a/src/codegen/builtins/system/localtime.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Purpose: -//! Lowers the PHP `localtime()` builtin: evaluates the optional timestamp and associative-keys flag, -//! calls the `__rt_localtime` runtime helper, and boxes the resulting array into a Mixed cell. -//! -//! Called from: -//! - `crate::codegen::builtins::system` dispatch for the `localtime` builtin. -//! -//! Key details: -//! - The timestamp defaults to the current time (passed as the `-1` sentinel) and the associative -//! flag defaults to `0`. The arguments are materialized in the runtime's input registers -//! (timestamp first, flag second), then the raw hash pointer is boxed into a Mixed assoc array. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits `localtime([$timestamp [, $associative]])`: materializes the timestamp (or `-1`) and the -/// associative-keys flag (or `0`), calls `__rt_localtime`, and boxes the hash pointer into a Mixed -/// assoc array (runtime tag 5), like `getdate`/`stat`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("localtime()"); - match emitter.target.arch { - Arch::AArch64 => { - if args.len() >= 2 { - let flag_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &flag_ty); // associative flag → integer - emitter.instruction("str x0, [sp, #-16]!"); // push the flag while the timestamp is evaluated - let ts_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &ts_ty); // timestamp → integer in x0 - emitter.instruction("ldr x1, [sp], #16"); // restore the flag into the second argument register - } else if args.len() == 1 { - let ts_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &ts_ty); // timestamp → integer in x0 - emitter.instruction("mov x1, #0"); // associative flag defaults to 0 (numeric keys) - } else { - emitter.instruction("mov x0, #-1"); // -1 sentinel → runtime uses the current time - emitter.instruction("mov x1, #0"); // associative flag defaults to 0 (numeric keys) - } - } - Arch::X86_64 => { - if args.len() >= 2 { - let flag_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &flag_ty); // associative flag → integer - abi::emit_push_reg(emitter, "rax"); // push the flag while the timestamp is evaluated - let ts_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &ts_ty); // timestamp → integer in rax - abi::emit_pop_reg(emitter, "rsi"); // restore the flag into the second argument register - } else if args.len() == 1 { - let ts_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &ts_ty); // timestamp → integer in rax - emitter.instruction("mov rsi, 0"); // associative flag defaults to 0 (numeric keys) - } else { - emitter.instruction("mov rax, -1"); // -1 sentinel → runtime uses the current time - emitter.instruction("mov rsi, 0"); // associative flag defaults to 0 (numeric keys) - } - } - } - abi::emit_call_label(emitter, "__rt_localtime"); // build the localtime array → hash pointer - // Box the raw hash pointer into a Mixed cell (runtime tag 5 = assoc array), like getdate(). - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // x1 = hash pointer (low payload word) - emitter.instruction("mov x2, #0"); // x2 = high payload word (unused) - emitter.instruction("mov x0, #5"); // x0 = runtime tag 5 (assoc array) - emitter.instruction("bl __rt_mixed_from_value"); // → x0 = boxed mixed cell - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // rdi = hash pointer (low payload word) - emitter.instruction("mov rsi, 0"); // rsi = high payload word (unused) - emitter.instruction("mov rax, 5"); // rax = runtime tag 5 (assoc array) - emitter.instruction("call __rt_mixed_from_value"); // → rax = boxed mixed cell - } - } - Some(PhpType::Mixed) -} diff --git a/src/codegen/builtins/system/microtime.rs b/src/codegen/builtins/system/microtime.rs deleted file mode 100644 index b5db51fc3d..0000000000 --- a/src/codegen/builtins/system/microtime.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Purpose: -//! Emits PHP `microtime` time/date builtin calls. -//! Marshals timestamp and format arguments into runtime helpers that consult wall-clock state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Time calls are effectful/non-deterministic and must preserve PHP scalar return conventions. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the `microtime([get_as_float])` builtin. -/// -/// `microtime()` returns the current Unix timestamp with microsecond precision as a -/// float when `get_as_float` is true. The arguments are ignored—callers always invoke -/// `__rt_microtime` regardless of argument values. -/// -/// Calls the target-aware runtime helper `__rt_microtime`, which is effectful -/// (reads wall-clock state) and non-deterministic. The result is returned in the -/// native float register. -/// -/// Returns `PhpType::Float` unconditionally. -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("microtime(true)"); - abi::emit_call_label(emitter, "__rt_microtime"); // call the target-aware runtime helper that returns the current Unix timestamp with microsecond precision in the native float result register - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/system/mktime.rs b/src/codegen/builtins/system/mktime.rs deleted file mode 100644 index 3faa52e513..0000000000 --- a/src/codegen/builtins/system/mktime.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Purpose: -//! Emits PHP `mktime` time/date builtin calls. -//! Marshals timestamp and format arguments into runtime helpers that consult wall-clock state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Time calls are effectful/non-deterministic and must preserve PHP scalar return conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Lowers a PHP `mktime(hour, min, sec, month, day, year)` call. -/// -/// Evaluates all six integer arguments in source order, coercing each to a raw -/// integer via `coerce_to_int` (so a `Mixed`/`Union` argument — e.g. a value -/// produced by boxed arithmetic — is unboxed instead of being pushed as a heap -/// pointer), pushes them onto the temporary stack in reverse order, then pops -/// them into the target ABI integer registers (AArch64: x0–x5; x86_64: rdi, rsi, -/// rdx, rcx, r8, r9). Calls the `__rt_mktime` runtime helper, which builds a -/// libc `struct tm` from the six fields and invokes `mktime(3)`. Returns the -/// Unix timestamp as `PhpType::Int`. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(name); - - match emitter.target.arch { - Arch::AArch64 => { - // -- evaluate all 6 arguments: hour, min, sec, month, day, year -- - // Push them on stack in reverse order so they come off in order - for i in (0..6).rev() { - let arg_ty = emit_expr(&args[i], emitter, ctx, data); - coerce_to_int(emitter, &arg_ty); // unbox a Mixed/Union argument into a raw integer before pushing it - emitter.instruction("str x0, [sp, #-16]!"); // push the evaluated integer argument onto the temporary stack - } - - // -- pop args into registers: x0=hour, x1=min, x2=sec, x3=month, x4=day, x5=year -- - emitter.instruction("ldr x0, [sp], #16"); // restore the hour argument into the first integer argument register - emitter.instruction("ldr x1, [sp], #16"); // restore the minute argument into the second integer argument register - emitter.instruction("ldr x2, [sp], #16"); // restore the second argument into the third integer argument register - emitter.instruction("ldr x3, [sp], #16"); // restore the month argument into the fourth integer argument register - emitter.instruction("ldr x4, [sp], #16"); // restore the day argument into the fifth integer argument register - emitter.instruction("ldr x5, [sp], #16"); // restore the year argument into the sixth integer argument register - } - Arch::X86_64 => { - // -- evaluate all 6 arguments: hour, min, sec, month, day, year -- - // Push them on stack in reverse order so they come off in order - for i in (0..6).rev() { - let arg_ty = emit_expr(&args[i], emitter, ctx, data); - coerce_to_int(emitter, &arg_ty); // unbox a Mixed/Union argument into a raw integer before pushing it - abi::emit_push_reg(emitter, "rax"); // push the evaluated integer argument onto the temporary x86_64 stack slot - } - - // -- pop args into SysV integer registers: rdi=hour, rsi=min, rdx=sec, rcx=month, r8=day, r9=year -- - abi::emit_pop_reg(emitter, "rdi"); // restore the hour argument into the first SysV integer argument register - abi::emit_pop_reg(emitter, "rsi"); // restore the minute argument into the second SysV integer argument register - abi::emit_pop_reg(emitter, "rdx"); // restore the second argument into the third SysV integer argument register - abi::emit_pop_reg(emitter, "rcx"); // restore the month argument into the fourth SysV integer argument register - abi::emit_pop_reg(emitter, "r8"); // restore the day argument into the fifth SysV integer argument register - abi::emit_pop_reg(emitter, "r9"); // restore the year argument into the sixth SysV integer argument register - } - } - - // -- call the runtime to build struct tm and convert it: mktime() local, timegm() (gmmktime) UTC -- - let rt = if name == "gmmktime" { "__rt_gmmktime" } else { "__rt_mktime" }; - abi::emit_call_label(emitter, rt); // build a libc struct tm and return the resulting Unix timestamp through the active target ABI - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/system/mod.rs b/src/codegen/builtins/system/mod.rs deleted file mode 100644 index d5699b5bfd..0000000000 --- a/src/codegen/builtins/system/mod.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Purpose: -//! Dispatches process, environment, time, JSON, regex, and constant builtins to their focused codegen emitters. -//! Keeps the public builtin category surface small while leaf files own lowering details. -//! -//! Called from: -//! - `crate::codegen::builtins::emit_builtin_call()`. -//! -//! Key details: -//! - Dispatcher names must stay aligned with the builtin catalog and signature normalization layer. - -mod class_attribute_args; -mod class_attribute_names; -mod class_get_attributes; -mod checkdate; -mod date; -mod getdate; -mod hrtime; -mod localtime; -mod date_default_timezone_get; -mod date_default_timezone_set; -mod define; -mod defined; -mod exec_fn; -mod exit; -mod getenv; -mod json_decode; -mod json_encode; -mod json_last_error; -mod json_last_error_msg; -mod json_validate; -mod microtime; -mod mktime; -mod passthru; -mod php_uname; -mod phpversion; -mod preg_match; -mod preg_match_all; -mod preg_replace_callback; -mod preg_replace; -mod preg_split; -mod putenv; -mod shell_exec; -mod sleep; -mod strtotime; -mod system_fn; -mod time; -mod usleep; - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::names::php_symbol_key; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Dispatches system builtins (process, environment, time, JSON, regex, constants) -/// to their focused codegen leaf emitters. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "exit" | "die" => exit::emit(name, args, emitter, ctx, data), - "define" => define::emit(name, args, emitter, ctx, data), - "defined" => defined::emit(name, args, emitter, ctx, data), - "time" => time::emit(name, args, emitter, ctx, data), - "microtime" => microtime::emit(name, args, emitter, ctx, data), - "sleep" => sleep::emit(name, args, emitter, ctx, data), - "usleep" => usleep::emit(name, args, emitter, ctx, data), - "getenv" => getenv::emit(name, args, emitter, ctx, data), - "putenv" => putenv::emit(name, args, emitter, ctx, data), - "date_default_timezone_set" => { - date_default_timezone_set::emit(name, args, emitter, ctx, data) - } - "date_default_timezone_get" => { - date_default_timezone_get::emit(name, args, emitter, ctx, data) - } - "php_uname" => php_uname::emit(name, args, emitter, ctx, data), - "phpversion" => phpversion::emit(name, args, emitter, ctx, data), - "class_attribute_args" => class_attribute_args::emit(name, args, emitter, ctx, data), - "class_attribute_names" => class_attribute_names::emit(name, args, emitter, ctx, data), - "class_get_attributes" => class_get_attributes::emit(name, args, emitter, ctx, data), - "exec" => exec_fn::emit(name, args, emitter, ctx, data), - "shell_exec" => shell_exec::emit(name, args, emitter, ctx, data), - "system" => system_fn::emit(name, args, emitter, ctx, data), - "passthru" => passthru::emit(name, args, emitter, ctx, data), - "date" | "gmdate" => date::emit(name, args, emitter, ctx, data), - "mktime" | "gmmktime" => mktime::emit(name, args, emitter, ctx, data), - "checkdate" => checkdate::emit(name, args, emitter, ctx, data), - "getdate" => getdate::emit(name, args, emitter, ctx, data), - "localtime" => localtime::emit(name, args, emitter, ctx, data), - "hrtime" => hrtime::emit(name, args, emitter, ctx, data), - "strtotime" | "__elephc_strtotime_raw" => strtotime::emit(name, args, emitter, ctx, data), - "json_encode" => json_encode::emit(name, args, emitter, ctx, data), - "json_decode" => json_decode::emit(name, args, emitter, ctx, data), - "json_last_error" => json_last_error::emit(name, args, emitter, ctx, data), - "json_last_error_msg" => json_last_error_msg::emit(name, args, emitter, ctx, data), - "json_validate" => json_validate::emit(name, args, emitter, ctx, data), - "preg_match" => preg_match::emit(name, args, emitter, ctx, data), - "preg_match_all" => preg_match_all::emit(name, args, emitter, ctx, data), - "preg_replace_callback" => preg_replace_callback::emit(name, args, emitter, ctx, data), - "preg_replace" => preg_replace::emit(name, args, emitter, ctx, data), - "preg_split" => preg_split::emit(name, args, emitter, ctx, data), - _ => None, - } -} - -/// Resolves a class name to its canonical form stored in the context's class table. -/// -/// Uses `php_symbol_key` for case-insensitive matching, stripping any leading backslash -/// from `class_name` before lookup. Returns `None` if the class is not declared. -/// -/// # Arguments -/// * `ctx` - Compilation context containing known class declarations -/// * `class_name` - PHP class name, optionally prefixed with `\` -/// -/// # Returns -/// The canonical class name as stored in `ctx.classes`, or `None` if not found. -fn resolve_class_name<'a>(ctx: &'a Context, class_name: &str) -> Option<&'a str> { - let class_key = php_symbol_key(class_name.trim_start_matches('\\')); - ctx.classes - .keys() - .find(|existing| php_symbol_key(existing) == class_key) - .map(String::as_str) -} diff --git a/src/codegen/builtins/system/passthru.rs b/src/codegen/builtins/system/passthru.rs deleted file mode 100644 index 3c253f60e2..0000000000 --- a/src/codegen/builtins/system/passthru.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Emits PHP `passthru` process-control or shell execution builtin calls. -//! Marshals command/status arguments into runtime helpers with PHP-visible output and exit behavior. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Process calls are effectful and may terminate or emit output, so lowering must preserve evaluation order. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `passthru` call by executing a null-terminated command string via libc `system()`. -/// The command is evaluated and null-terminated through `__rt_cstr` before the call. -/// On x86_64 the null-terminated pointer is passed in `rdi` (SysV first-argument register). -/// Output from the command writes directly to stdout and is not captured or returned. -/// Returns `PhpType::Void`. The call is effectful and may terminate or emit output. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("passthru()"); - // -- evaluate command string -- - emit_expr(&args[0], emitter, ctx, data); - // -- null-terminate and call libc system() which outputs directly to stdout -- - abi::emit_call_label(emitter, "__rt_cstr"); // null-terminate the command string through the target-aware C-string helper - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // pass the null-terminated command pointer in the SysV first-argument register - } - emitter.bl_c("system"); // execute command, output goes directly to stdout - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/system/php_uname.rs b/src/codegen/builtins/system/php_uname.rs deleted file mode 100644 index 97f05eac3c..0000000000 --- a/src/codegen/builtins/system/php_uname.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Emits PHP `php_uname` environment/platform information builtin calls. -//! Delegates host environment lookup or platform string construction to runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Environment and platform state are observable and must not be folded as compile-time constants here. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `php_uname` builtin call. -/// -/// `php_uname()` returns a string describing the operating system PHP was built on. -/// When no mode argument is given, it defaults to `"a"` (architecture info). -/// -/// # Arguments -/// * `_name` — builtin name (unused, resolved via catalog) -/// * `args` — optional mode string argument; defaults to `"a"` if empty -/// * `emitter` — assembly emitter for the target -/// * `ctx` — codegen context (variable layout, ownership, class metadata) -/// * `data` — data section for static strings/labels -/// -/// # Returns -/// `Some(PhpType::Str)` — the mode string result from the runtime helper. -/// -/// # Behavior -/// * Zero args: materializes the default `"a"` mode string in the string result registers, then calls `__rt_php_uname`. -/// * One arg: evaluates the mode expression first, then calls `__rt_php_uname`. -/// * The runtime helper `__rt_php_uname` handles all mode-to-result mapping. -/// -/// # ABI -/// * Mode string passed in `x1` (pointer) / `x2` (length) registers. -/// * Result string returned via `x1` (pointer) / `x2` (length). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("php_uname()"); - - // -- materialize the PHP default mode when no explicit mode was passed -- - if args.is_empty() { - let (label, len) = data.add_string(b"a"); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_symbol_address(emitter, ptr_reg, &label); // materialize php_uname() default mode "a" in the string pointer result register - abi::emit_load_int_immediate(emitter, len_reg, len as i64); // publish the one-byte default mode length in the paired string-length result register - } else { - emit_expr(&args[0], emitter, ctx, data); - } - - // -- query the target runtime's uname data and select the requested PHP mode -- - abi::emit_call_label(emitter, "__rt_php_uname"); // call the target-aware uname helper with the mode string in the native string result registers - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/phpversion.rs b/src/codegen/builtins/system/phpversion.rs deleted file mode 100644 index 8cec6946cf..0000000000 --- a/src/codegen/builtins/system/phpversion.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Purpose: -//! Emits PHP `phpversion` environment/platform information builtin calls. -//! Delegates host environment lookup or platform string construction to runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Environment and platform state are observable and must not be folded as compile-time constants here. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `phpversion()` builtin call. -/// -/// Returns the compiler's Cargo package version string as a PHP string. -/// The version string address is materialized in `ptr_reg` and its byte length -/// in `len_reg` per the target ABI string return convention. -/// -/// # Arguments -/// * `_name` — the builtin name (unused, dispatch already occurred) -/// * `_args` — the call arguments (phpversion takes none, ignored) -/// * `emitter` — the assembly emitter -/// * `_ctx` — codegen context (unused by this builtin) -/// * `data` — the data section where the version string is stored -/// -/// # Returns -/// `Some(PhpType::Str)` since phpversion() always returns a string -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("phpversion()"); - // -- return the Cargo package version string -- - let version = env!("CARGO_PKG_VERSION").as_bytes(); - let (label, len) = data.add_string(version); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_symbol_address(emitter, ptr_reg, &label); // materialize the Cargo package version string in the active string-pointer result register - abi::emit_load_int_immediate(emitter, len_reg, len as i64); // publish the Cargo package version string length in the paired string-length result register - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/preg_match.rs b/src/codegen/builtins/system/preg_match.rs deleted file mode 100644 index 768c93a9c2..0000000000 --- a/src/codegen/builtins/system/preg_match.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Purpose: -//! Emits PHP `preg_match` PCRE-style regex builtin calls. -//! Connects pattern/subject arguments and optional match arrays to runtime regex helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Match arrays and false/error results must use PHP-compatible Mixed array payloads. - -use crate::codegen::abi; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits a `preg_match` call against a PCRE pattern. -/// -/// `args[0]` is the pattern (string), `args[1]` is the subject (string). -/// Calls `__rt_preg_match` which returns 1 in the result register on match, 0 otherwise. -/// -/// AArch64: pattern ptr/len in x0/x1, subject ptr/len pushed then popped into x3/x4, result in x0. -/// X86_64: pattern ptr/len in rdi/rsi (SysV), subject ptr/len pushed then popped into rdx/rcx, result in rax. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("preg_match()"); - - match emitter.target.arch { - Arch::AArch64 => { - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push subject ptr and len - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("ldp x3, x4, [sp], #16"); // pop subject ptr/len into x3/x4 - if let Some(matches_arg) = args.get(2) { - emitter.instruction("bl __rt_preg_match_capture"); // regex match → x0=match flag, x1=matches array - emit_store_matches_arg(emitter, ctx, matches_arg); - } else { - emitter.instruction("bl __rt_preg_match"); // regex match → x0=1 if matched, 0 if not - } - } - Arch::X86_64 => { - emit_expr(&args[1], emitter, ctx, data); - crate::codegen::abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push subject ptr and len - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // pass the pattern pointer in the first SysV integer argument register - emitter.instruction("mov rsi, rdx"); // pass the pattern length in the second SysV integer argument register - crate::codegen::abi::emit_pop_reg_pair(emitter, "rdx", "rcx"); // pop subject ptr/len into the remaining SysV argument registers - if let Some(matches_arg) = args.get(2) { - crate::codegen::abi::emit_call_label(emitter, "__rt_preg_match_capture"); // regex match → rax=match flag, rdx=matches array - emit_store_matches_arg(emitter, ctx, matches_arg); - } else { - crate::codegen::abi::emit_call_label(emitter, "__rt_preg_match"); // regex match → rax=1 if matched, 0 if not - } - } - } - - Some(PhpType::Int) -} - -/// Stores the runtime-built `$matches` array back into the by-reference argument. -fn emit_store_matches_arg(emitter: &mut Emitter, ctx: &mut Context, arg: &Expr) { - let ExprKind::Variable(name) = &arg.kind else { - return; - }; - match emitter.target.arch { - Arch::AArch64 => { - if ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)) { - let label = format!("_gvar_{}", name); - abi::emit_symbol_address(emitter, "x9", &label); // load page of the global preg_match matches slot - emitter.instruction("str x1, [x9]"); // store the matches array into the global variable - } else if ctx.ref_params.contains(name) { - let offset = ctx - .variables - .get(name) - .expect("codegen bug: missing preg_match matches ref slot") - .stack_offset; - abi::load_at_offset(emitter, "x9", offset); // load the by-reference matches variable storage pointer - emitter.instruction("str x1, [x9]"); // store the matches array through the referenced storage slot - } else if let Some(var) = ctx.variables.get(name) { - abi::store_at_offset(emitter, "x1", var.stack_offset); // store the matches array into the local variable slot - } - } - Arch::X86_64 => { - if ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)) { - let label = format!("_gvar_{}", name); - abi::emit_store_reg_to_symbol(emitter, "rdx", &label, 0); // store the matches array into the global variable - } else if ctx.ref_params.contains(name) { - let offset = ctx - .variables - .get(name) - .expect("codegen bug: missing preg_match matches ref slot") - .stack_offset; - abi::load_at_offset(emitter, "r11", offset); // load the by-reference matches variable storage pointer - abi::emit_store_to_address(emitter, "rdx", "r11", 0); // store the matches array through the referenced storage slot - } else if let Some(var) = ctx.variables.get(name) { - abi::store_at_offset(emitter, "rdx", var.stack_offset); // store the matches array into the local variable slot - } - } - } - let matches_ty = PhpType::Array(Box::new(PhpType::Str)); - ctx.update_var_type_static_and_ownership( - name, - matches_ty.clone(), - matches_ty, - HeapOwnership::Owned, - ); -} diff --git a/src/codegen/builtins/system/preg_match_all.rs b/src/codegen/builtins/system/preg_match_all.rs deleted file mode 100644 index 04aa5ae442..0000000000 --- a/src/codegen/builtins/system/preg_match_all.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Purpose: -//! Emits PHP `preg_match_all` PCRE-style regex builtin calls. -//! Connects pattern/subject arguments and optional match arrays to runtime regex helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Match arrays and false/error results must use PHP-compatible Mixed array payloads. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a `preg_match_all` call, passing pattern (args[0]) and subject (args[1]) -/// to `__rt_preg_match_all` via platform ABI. Returns Int (match count). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("preg_match_all()"); - - match emitter.target.arch { - Arch::AArch64 => { - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push subject ptr and len - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("ldp x3, x4, [sp], #16"); // pop subject ptr/len into x3/x4 - emitter.instruction("bl __rt_preg_match_all"); // count all regex matches → x0=match count - } - Arch::X86_64 => { - emit_expr(&args[1], emitter, ctx, data); - crate::codegen::abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push subject ptr and len - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // pass the pattern pointer in the first SysV integer argument register - emitter.instruction("mov rsi, rdx"); // pass the pattern length in the second SysV integer argument register - crate::codegen::abi::emit_pop_reg_pair(emitter, "rdx", "rcx"); // pop subject ptr/len into the remaining SysV argument registers - crate::codegen::abi::emit_call_label(emitter, "__rt_preg_match_all"); // count all regex matches → rax=match count - } - } - - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/system/preg_replace.rs b/src/codegen/builtins/system/preg_replace.rs deleted file mode 100644 index 8a69f5440c..0000000000 --- a/src/codegen/builtins/system/preg_replace.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Purpose: -//! Emits PHP `preg_replace` PCRE-style regex builtin calls. -//! Connects pattern/subject arguments and optional match arrays to runtime regex helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Match arrays and false/error results must use PHP-compatible Mixed array payloads. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `preg_replace` builtin call. -/// -/// Evaluates arguments in PHP source order (pattern=arg[0], replacement=arg[1], subject=arg[2]), -/// then materializes them into ABI registers and calls `__rt_preg_replace`. -/// -/// # Arguments -/// - `args[0]`: pattern string expression -/// - `args[1]`: replacement string expression -/// - `args[2]`: subject string expression -/// -/// # Return -/// Always returns `Some(PhpType::Str)` — the result is a PHP string. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("preg_replace()"); - - match emitter.target.arch { - Arch::AArch64 => { - // -- evaluate subject string (arg 2) first -- - emit_expr(&args[2], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push subject ptr and len - - // -- evaluate replacement string (arg 1) -- - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push replacement ptr and len - - // -- evaluate pattern string (arg 0) -- - emit_expr(&args[0], emitter, ctx, data); - - // -- pop replacement into x3/x4 -- - emitter.instruction("ldp x3, x4, [sp], #16"); // pop replacement ptr/len into x3/x4 - - // -- pop subject into x5/x6 -- - emitter.instruction("ldp x5, x6, [sp], #16"); // pop subject ptr/len into x5/x6 - - // -- call runtime: x1/x2=pattern, x3/x4=replacement, x5/x6=subject -- - emitter.instruction("bl __rt_preg_replace"); // regex replace → x1=result ptr, x2=result len - } - Arch::X86_64 => { - emit_expr(&args[2], emitter, ctx, data); - crate::codegen::abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push subject ptr and len - emit_expr(&args[1], emitter, ctx, data); - crate::codegen::abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push replacement ptr and len - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // pass the pattern pointer in the first SysV integer argument register - emitter.instruction("mov rsi, rdx"); // pass the pattern length in the second SysV integer argument register - crate::codegen::abi::emit_pop_reg_pair(emitter, "rdx", "rcx"); // pop replacement ptr/len into the next SysV integer argument registers - crate::codegen::abi::emit_pop_reg_pair(emitter, "r8", "r9"); // pop subject ptr/len into the remaining SysV integer argument registers - crate::codegen::abi::emit_call_label(emitter, "__rt_preg_replace"); // regex replace → rax=result ptr, rdx=result len - } - } - - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/preg_replace_callback.rs b/src/codegen/builtins/system/preg_replace_callback.rs deleted file mode 100644 index f6a8afaf27..0000000000 --- a/src/codegen/builtins/system/preg_replace_callback.rs +++ /dev/null @@ -1,751 +0,0 @@ -//! Purpose: -//! Emits PHP `preg_replace_callback` PCRE-style regex builtin calls. -//! Wires a statically known callback into the regex replacement runtime. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - The callback receives `array` matches so untyped closure params -//! must be specialized before deferred closure emission. -//! - Descriptor-valued callbacks keep receiver and capture environments in descriptor storage. - -use crate::codegen::abi; -use crate::codegen::builtins::arrays::{ - call_user_func_array, callback_env, runtime_callable_array_callback, -}; -use crate::codegen::callable_dispatch::{RuntimeCallableCase, RuntimeCallableSelector}; -use crate::codegen::context::{Context, DeferredCallbackWrapper}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; - -use super::super::callable_lookup::{lookup_function, FunctionLookup}; - -/// Emits the `preg_replace_callback` builtin call. -/// -/// Evaluates arguments in PHP source order (pattern, callback, subject), -/// materializes the callback address, and calls the `__rt_preg_replace_callback` -/// runtime helper. Returns `PhpType::Str` on success. -/// -/// # Arguments -/// * `_name` - Unused, follows dispatcher convention -/// * `args` - `[pattern, callback, subject]` -/// * `emitter` - Target assembly emitter -/// * `ctx` - Codegen context (variables, deferred closures) -/// * `data` - Data section for constants/symbols -/// -/// # Returns -/// `Some(PhpType::Str)` on success, `None` if the call was deferred -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("preg_replace_callback()"); - - // -- evaluate pattern first, matching PHP source order -- - emit_expr(&args[0], emitter, ctx, data); - let (string_ptr_reg, string_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, string_ptr_reg, string_len_reg); - - let call_reg = abi::nested_call_reg(emitter); - let result_reg = abi::int_result_reg(emitter); - - if let Some(array_callback) = - callback_env::resolve_callable_array_descriptor_callback(&args[1], ctx, data) - { - let receiver_ty = if let Some((receiver, receiver_ty)) = &array_callback.receiver_prefix { - emit_expr(receiver, emitter, ctx, data); - abi::emit_push_reg(emitter, result_reg); // preserve callable-array receiver across subject evaluation - Some(receiver_ty.clone()) - } else { - None - }; - - // -- evaluate subject last -- - emit_expr(&args[2], emitter, ctx, data); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x5, x1"); // pass subject pointer to the regex callback runtime - emitter.instruction("mov x6, x2"); // pass subject length to the regex callback runtime - if receiver_ty.is_some() { - abi::emit_pop_reg(emitter, call_reg); // recover callable-array receiver for descriptor prefix storage - } - abi::emit_pop_reg_pair(emitter, "x7", "x8"); - - let wrapper = static_callable_array_preg_callback_env( - &array_callback, - receiver_ty.as_ref(), - call_reg, - "x5", - emitter, - ctx, - ); - abi::emit_symbol_address(emitter, "x3", &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, "x4"); - emitter.instruction("mov x1, x7"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov x2, x8"); // pass pattern length to the regex callback runtime - emitter.instruction("bl __rt_preg_replace_callback"); // run regex replacement through the callable-array descriptor callback → x1=ptr, x2=len - release_descriptor_preg_callback_env(wrapper.env_bytes, emitter); - } - Arch::X86_64 => { - emitter.instruction("mov r8, rax"); // pass subject pointer to the regex callback runtime - emitter.instruction("mov r9, rdx"); // pass subject length to the regex callback runtime - if receiver_ty.is_some() { - abi::emit_pop_reg(emitter, call_reg); // recover callable-array receiver for descriptor prefix storage - } - abi::emit_pop_reg_pair(emitter, "r13", "r14"); - - let wrapper = static_callable_array_preg_callback_env( - &array_callback, - receiver_ty.as_ref(), - call_reg, - "r8", - emitter, - ctx, - ); - abi::emit_symbol_address(emitter, "rdx", &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, "rcx"); - emitter.instruction("mov rdi, r13"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov rsi, r14"); // pass pattern length to the regex callback runtime - abi::emit_call_label(emitter, "__rt_preg_replace_callback"); // run regex replacement through the callable-array descriptor callback → rax=ptr, rdx=len - release_descriptor_preg_callback_env(wrapper.env_bytes, emitter); - } - } - - return Some(PhpType::Str); - } - - if runtime_callable_array_callback::emit_without_saved_array( - &args[1], - emitter, - ctx, - data, - |case, receiver_ty, emitter, ctx, data| { - emit_runtime_callable_array_preg_case( - case, - receiver_ty, - &args[2], - emitter, - ctx, - data, - ); - }, - ) { - return Some(PhpType::Str); - } - - if call_user_func_array::callback_is_runtime_string(&args[1], ctx) { - emit_runtime_string_preg_callback(&args[1], &args[2], emitter, ctx, data); - return Some(PhpType::Str); - } - - if callback_env::expr_call_needs_descriptor_callback_env(&args[1], ctx) - && callback_env::descriptor_callback_env_supported(&args[1]) - { - // -- evaluate the selected descriptor before the subject, matching PHP source order -- - let (expected_installed, previous_expected) = - install_preg_callback_expected_sig(&args[1], ctx); - emit_expr(&args[1], emitter, ctx, data); - restore_preg_callback_expected_sig(expected_installed, previous_expected, ctx); - specialize_recent_inline_callback(&args[1], ctx); - let retained_borrowed = - callback_env::retain_borrowed_descriptor_callback_result(&args[1], emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the selected callable descriptor across subject evaluation - - // -- evaluate subject last -- - emit_expr(&args[2], emitter, ctx, data); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x5, x1"); // pass subject pointer to the regex callback runtime - emitter.instruction("mov x6, x2"); // pass subject length to the regex callback runtime - abi::emit_pop_reg(emitter, result_reg); - abi::emit_pop_reg_pair(emitter, "x7", "x8"); - - let wrapper = descriptor_preg_callback_env( - &args[1], - "x5", - retained_borrowed, - emitter, - ctx, - ); - abi::emit_symbol_address(emitter, "x3", &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, "x4"); - emitter.instruction("mov x1, x7"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov x2, x8"); // pass pattern length to the regex callback runtime - emitter.instruction("bl __rt_preg_replace_callback"); // run regex replacement through the descriptor callback → x1=ptr, x2=len - release_descriptor_preg_callback_env(wrapper.env_bytes, emitter); - } - Arch::X86_64 => { - emitter.instruction("mov r8, rax"); // pass subject pointer to the regex callback runtime - emitter.instruction("mov r9, rdx"); // pass subject length to the regex callback runtime - abi::emit_pop_reg(emitter, result_reg); - abi::emit_pop_reg_pair(emitter, "r13", "r14"); - - let wrapper = descriptor_preg_callback_env( - &args[1], - "r8", - retained_borrowed, - emitter, - ctx, - ); - abi::emit_symbol_address(emitter, "rdx", &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, "rcx"); - emitter.instruction("mov rdi, r13"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov rsi, r14"); // pass pattern length to the regex callback runtime - abi::emit_call_label(emitter, "__rt_preg_replace_callback"); // run regex replacement through the descriptor callback → rax=ptr, rdx=len - release_descriptor_preg_callback_env(wrapper.env_bytes, emitter); - } - } - - return Some(PhpType::Str); - } - - // -- evaluate callback second and remember its address -- - let (expected_installed, previous_expected) = - install_preg_callback_expected_sig(&args[1], ctx); - let captures = materialize_callback_address(&args[1], call_reg, emitter, ctx, data); - restore_preg_callback_expected_sig(expected_installed, previous_expected, ctx); - specialize_recent_inline_callback(&args[1], ctx); - abi::emit_push_reg(emitter, call_reg); - - // -- evaluate subject last -- - emit_expr(&args[2], emitter, ctx, data); - - match emitter.target.arch { - Arch::AArch64 => { - // -- stage runtime arguments away from helper scratch registers -- - abi::emit_push_reg_pair(emitter, "x1", "x2"); - abi::emit_pop_reg_pair(emitter, "x5", "x6"); - abi::emit_pop_reg(emitter, call_reg); - abi::emit_pop_reg_pair(emitter, "x7", "x8"); - - let env_bytes = materialize_capture_env(&captures, call_reg, "x3", "x4", emitter, ctx); - emitter.instruction("mov x1, x7"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov x2, x8"); // pass pattern length to the regex callback runtime - emitter.instruction("bl __rt_preg_replace_callback"); // run regex replacement through the callback → x1=ptr, x2=len - if env_bytes > 0 { - abi::emit_release_temporary_stack(emitter, env_bytes); - } - } - Arch::X86_64 => { - // -- stage runtime arguments away from helper scratch registers -- - abi::emit_push_reg_pair(emitter, "rax", "rdx"); - abi::emit_pop_reg_pair(emitter, "r8", "r9"); - abi::emit_pop_reg(emitter, call_reg); - abi::emit_pop_reg_pair(emitter, "r13", "r14"); - - let env_bytes = - materialize_capture_env(&captures, call_reg, "rdx", "rcx", emitter, ctx); - emitter.instruction("mov rdi, r13"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov rsi, r14"); // pass pattern length to the regex callback runtime - abi::emit_call_label(emitter, "__rt_preg_replace_callback"); // run regex replacement through the callback → rax=ptr, rdx=len - if env_bytes > 0 { - abi::emit_release_temporary_stack(emitter, env_bytes); - } - } - } - - Some(PhpType::Str) -} - -/// Emits one selected runtime callable-array descriptor case for `preg_replace_callback()`. -/// -/// Static-method cases enter with only the saved pattern on the temporary stack. -/// Instance-method cases enter with the selected receiver above the saved pattern, -/// and this helper consumes both after evaluating the subject in PHP source order. -fn emit_runtime_callable_array_preg_case( - case: &RuntimeCallableCase, - receiver_ty: Option<&PhpType>, - subject: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let call_reg = abi::nested_call_reg(emitter); - - // -- evaluate subject last -- - emit_expr(subject, emitter, ctx, data); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x5, x1"); // pass subject pointer to the regex callback runtime - emitter.instruction("mov x6, x2"); // pass subject length to the regex callback runtime - if receiver_ty.is_some() { - abi::emit_pop_reg(emitter, call_reg); // recover selected callable-array receiver for descriptor prefix storage - } - abi::emit_pop_reg_pair(emitter, "x7", "x8"); - - let wrapper = callable_array_descriptor_preg_callback_env( - &case.descriptor_label, - receiver_ty, - call_reg, - "x5", - emitter, - ctx, - ); - abi::emit_symbol_address(emitter, "x3", &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, "x4"); - emitter.instruction("mov x1, x7"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov x2, x8"); // pass pattern length to the regex callback runtime - emitter.instruction("bl __rt_preg_replace_callback"); // run regex replacement through the runtime callable-array descriptor callback → x1=ptr, x2=len - release_descriptor_preg_callback_env(wrapper.env_bytes, emitter); - } - Arch::X86_64 => { - emitter.instruction("mov r8, rax"); // pass subject pointer to the regex callback runtime - emitter.instruction("mov r9, rdx"); // pass subject length to the regex callback runtime - if receiver_ty.is_some() { - abi::emit_pop_reg(emitter, call_reg); // recover selected callable-array receiver for descriptor prefix storage - } - abi::emit_pop_reg_pair(emitter, "r13", "r14"); - - let wrapper = callable_array_descriptor_preg_callback_env( - &case.descriptor_label, - receiver_ty, - call_reg, - "r8", - emitter, - ctx, - ); - abi::emit_symbol_address(emitter, "rdx", &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, "rcx"); - emitter.instruction("mov rdi, r13"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov rsi, r14"); // pass pattern length to the regex callback runtime - abi::emit_call_label(emitter, "__rt_preg_replace_callback"); // run regex replacement through the runtime callable-array descriptor callback → rax=ptr, rdx=len - release_descriptor_preg_callback_env(wrapper.env_bytes, emitter); - } - } -} - -/// Emits descriptor selection for a runtime string regex callback. -/// -/// The callback expression is evaluated before the subject to preserve PHP -/// source order. Descriptor matching happens afterward, so all arguments have -/// already been evaluated before an unknown callback name can abort. -fn emit_runtime_string_preg_callback( - callback: &Expr, - subject: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let callback_ty = emit_expr(callback, emitter, ctx, data); - debug_assert!(matches!(callback_ty.codegen_repr(), PhpType::Str)); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the regex callback string name across subject evaluation - - // -- evaluate subject last -- - emit_expr(subject, emitter, ctx, data); - let (subject_ptr_reg, subject_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, subject_ptr_reg, subject_len_reg); // preserve subject while runtime callback string cases are matched - - let cases = crate::codegen::callable_dispatch::runtime_callable_cases( - ctx, - data, - &[], - Some(&preg_matches_type()), - ); - let call_reg = abi::nested_call_reg(emitter); - let done_label = ctx.next_label("preg_runtime_string_done"); - let selector = RuntimeCallableSelector::StringNameStack { - ptr_offset: 16, - len_offset: 24, - call_reg, - }; - - for case in &cases { - let next_case = ctx.next_label("preg_runtime_string_next"); - crate::codegen::callable_dispatch::emit_branch_if_callable_case_mismatch( - &selector, - case, - &next_case, - emitter, - ctx, - data, - ); - emit_runtime_string_preg_case(case, emitter, ctx); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - - call_user_func_array::emit_dynamic_string_callback_abort(emitter, data); - emitter.label(&done_label); -} - -/// Emits one matched runtime string callback case for `preg_replace_callback()`. -fn emit_runtime_string_preg_case( - case: &RuntimeCallableCase, - emitter: &mut Emitter, - ctx: &mut Context, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg_pair(emitter, "x5", "x6"); // recover subject pointer and length for the regex callback runtime - abi::emit_release_temporary_stack(emitter, 16); // discard the matched callback string name - abi::emit_pop_reg_pair(emitter, "x7", "x8"); - let wrapper = callable_array_descriptor_preg_callback_env( - &case.descriptor_label, - None, - abi::int_result_reg(emitter), - "x5", - emitter, - ctx, - ); - abi::emit_symbol_address(emitter, "x3", &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, "x4"); - emitter.instruction("mov x1, x7"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov x2, x8"); // pass pattern length to the regex callback runtime - emitter.instruction("bl __rt_preg_replace_callback"); // run regex replacement through the runtime string descriptor callback → x1=ptr, x2=len - release_descriptor_preg_callback_env(wrapper.env_bytes, emitter); - } - Arch::X86_64 => { - abi::emit_pop_reg_pair(emitter, "r8", "r9"); // recover subject pointer and length for the regex callback runtime - abi::emit_release_temporary_stack(emitter, 16); // discard the matched callback string name - abi::emit_pop_reg_pair(emitter, "r13", "r14"); - let wrapper = callable_array_descriptor_preg_callback_env( - &case.descriptor_label, - None, - abi::int_result_reg(emitter), - "r8", - emitter, - ctx, - ); - abi::emit_symbol_address(emitter, "rdx", &wrapper.wrapper_label); - callback_env::load_env_pointer_to_reg(emitter, "rcx"); - emitter.instruction("mov rdi, r13"); // pass pattern pointer to the regex callback runtime - emitter.instruction("mov rsi, r14"); // pass pattern length to the regex callback runtime - abi::emit_call_label(emitter, "__rt_preg_replace_callback"); // run regex replacement through the runtime string descriptor callback → rax=ptr, rdx=len - release_descriptor_preg_callback_env(wrapper.env_bytes, emitter); - } - } -} - -/// Builds the descriptor-backed wrapper environment for `preg_replace_callback()`. -/// -/// The regex runtime passes one visible `array` argument and expects a -/// string result. The dummy register fills the shared callback-env helper's -/// unused array slot; only env slot zero (the descriptor) is read by the wrapper. -fn descriptor_preg_callback_env( - callback: &Expr, - dummy_array_reg: &str, - retained_borrowed: bool, - emitter: &mut Emitter, - ctx: &mut Context, -) -> callback_env::DescriptorCallbackEnv { - let wrapper = if retained_borrowed { - callback_env::emit_descriptor_callback_env_from_retained_result( - callback, - dummy_array_reg, - vec![preg_matches_type()], - PhpType::Str, - emitter, - ctx, - ) - } else { - callback_env::emit_descriptor_callback_env_from_result( - callback, - dummy_array_reg, - vec![preg_matches_type()], - PhpType::Str, - emitter, - ctx, - ) - }; - wrapper.expect("descriptor callback env support checked before emitting preg_replace_callback") -} - -/// Builds a descriptor-backed regex callback environment for a tracked callable-array variable. -/// -/// Instance-method callable arrays store their receiver as a descriptor prefix so the -/// shared descriptor wrapper can prepend it to the regex matches array before invoking -/// the method descriptor. Static-method callable arrays have no prefix. -fn static_callable_array_preg_callback_env( - array_callback: &callback_env::CallableArrayDescriptorCallback, - receiver_ty: Option<&PhpType>, - receiver_reg: &str, - dummy_array_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) -> callback_env::DescriptorCallbackEnv { - callable_array_descriptor_preg_callback_env( - &array_callback.descriptor_label, - receiver_ty, - receiver_reg, - dummy_array_reg, - emitter, - ctx, - ) -} - -/// Builds a descriptor-backed regex callback environment from a descriptor label. -fn callable_array_descriptor_preg_callback_env( - descriptor_label: &str, - receiver_ty: Option<&PhpType>, - receiver_reg: &str, - dummy_array_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) -> callback_env::DescriptorCallbackEnv { - let descriptor_prefix_types = receiver_ty.iter().map(|ty| (*ty).clone()).collect(); - let wrapper = callback_env::emit_descriptor_callback_env_from_static_descriptor( - descriptor_label, - vec![preg_matches_type()], - descriptor_prefix_types, - PhpType::Str, - emitter, - ctx, - ); - if let Some(ty) = receiver_ty { - emitter.instruction(&format!("mov {}, {}", abi::int_result_reg(emitter), receiver_reg)); // restore callable-array receiver for regex descriptor prefix storage - callback_env::store_descriptor_callback_prefix_result(&wrapper, 0, ty, emitter); - } - callback_env::store_descriptor_callback_array_reg(&wrapper, dummy_array_reg, emitter); - wrapper -} - -/// Releases a descriptor-backed regex callback environment while preserving the string result. -fn release_descriptor_preg_callback_env(env_bytes: usize, emitter: &mut Emitter) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - crate::codegen::callable_descriptor::emit_release_current_descriptor(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - abi::emit_release_temporary_stack(emitter, env_bytes); -} - -/// Installs the contextual regex-callback signature before emitting inline closures. -/// -/// `emit_closure()` reads `expected_first_class_callable_sig` while building the -/// descriptor metadata. Setting it before emission keeps descriptor invokers and -/// deferred closure bodies aligned on the `array` `$matches` parameter. -fn install_preg_callback_expected_sig( - callback: &Expr, - ctx: &mut Context, -) -> (bool, Option) { - if !matches!(callback.kind, ExprKind::Closure { .. }) { - return (false, None); - } - let previous = ctx.expected_first_class_callable_sig.replace(FunctionSig { - params: vec![("matches".to_string(), preg_matches_type())], - defaults: vec![None], - return_type: PhpType::Str, - declared_return: false, - by_ref_return: false, - ref_params: vec![false], - declared_params: vec![false], - variadic: None, - deprecation: None, - }); - (true, previous) -} - -/// Restores the previous contextual callable signature after callback emission. -fn restore_preg_callback_expected_sig( - installed: bool, - previous: Option, - ctx: &mut Context, -) { - if installed { - ctx.expected_first_class_callable_sig = previous; - } -} - -/// Returns the PHP type for preg_replace_callback closure parameters. -/// -/// `preg_replace_callback` passes `array` (matches) to the callback, -/// so untyped closure params must be specialized to `array` before emission. -fn preg_matches_type() -> PhpType { - PhpType::Array(Box::new(PhpType::Str)) -} - -/// Specializes the most recently deferred inline closure's first parameter type. -/// -/// When `callback` is an inline `Closure` expression, this updates the closure's -/// signature so its first parameter is `preg_matches_type()` (`array`), -/// matching what `preg_replace_callback` passes at runtime. -/// -/// No-op for non-closure callbacks or when no deferred closure is pending. -fn specialize_recent_inline_callback(callback: &Expr, ctx: &mut Context) { - if !matches!(callback.kind, ExprKind::Closure { .. }) { - return; - } - let Some(deferred) = ctx.deferred_closures.last_mut() else { - return; - }; - if let Some((_, ty)) = deferred.sig.params.first_mut() { - *ty = preg_matches_type(); - } - if let Some(declared) = deferred.sig.declared_params.first_mut() { - *declared = false; - } -} - -/// Loads the callback address into `call_reg` and returns capture variables. -/// -/// Handles three callback forms: -/// - **String literal**: looks up the function name and emits its symbol address -/// - **Variable**: loads the callable value from the stack slot -/// - **Other expression**: emits the expression and moves the result address -/// -/// Returns capture metadata `(name, PhpType, by_ref)` from `callable_captures`. -fn materialize_callback_address( - callback: &Expr, - call_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Vec<(String, PhpType, bool)> { - match &callback.kind { - ExprKind::StringLiteral(name) => { - let resolved_name = match lookup_function(ctx, name) { - Some(FunctionLookup::UserFunction(name)) - | Some(FunctionLookup::IncludeVariant(name)) => name, - _ => name.clone(), - }; - abi::emit_symbol_address(emitter, call_reg, &function_symbol(&resolved_name)); - Vec::new() - } - ExprKind::Variable(name) => { - let var = ctx.variables.get(name).expect("undefined callback variable"); - abi::load_at_offset(emitter, call_reg, var.stack_offset); - if ctx.ref_params.contains(name) { - abi::emit_load_from_address(emitter, call_reg, call_reg, 0); - } - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - call_reg, - call_reg, - ); - crate::codegen::callables::callable_captures(callback, ctx) - } - _ => { - emit_expr(callback, emitter, ctx, data); - emitter.instruction(&format!("mov {}, {}", call_reg, abi::int_result_reg(emitter))); // keep the evaluated callback descriptor in the nested-call scratch register - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - call_reg, - call_reg, - ); - crate::codegen::callables::callable_captures(callback, ctx) - } - } -} - -/// Emits a capture environment on the temporary stack and registers a wrapper. -/// -/// If `captures` is empty, passes the direct callback address with no environment. -/// Otherwise: -/// - Reserves `env_bytes` on the temporary stack (slot 0 = callback, slots 1+ = captures) -/// - Stores each capture variable into the corresponding environment slot -/// - Registers a `DeferredCallbackWrapper` for later wrapper emission -/// - Returns the environment size in bytes -/// -/// # Returns -/// Bytes of reserved temporary stack, or 0 if no captures -fn materialize_capture_env( - captures: &[(String, PhpType, bool)], - callback_reg: &str, - runtime_callback_reg: &str, - runtime_env_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) -> usize { - if captures.is_empty() { - emitter.instruction(&format!("mov {}, {}", runtime_callback_reg, callback_reg)); // pass the direct callback address to the regex runtime - abi::emit_load_int_immediate(emitter, runtime_env_reg, 0); - return 0; - } - - let wrapper_label = ctx.next_label("callback_wrapper"); - ctx.deferred_callback_wrappers.push(DeferredCallbackWrapper { - label: wrapper_label.clone(), - visible_arg_types: vec![preg_matches_type()], - target_visible_arg_types: None, - capture_types: captures - .iter() - .map(|(_, ty, by_ref)| if *by_ref { PhpType::Int } else { ty.clone() }) - .collect(), - descriptor_prefix_types: Vec::new(), - descriptor_return_type: None, - }); - - let env_bytes = (captures.len() + 1) * 16; - abi::emit_reserve_temporary_stack(emitter, env_bytes); - store_reg_to_env_slot(emitter, callback_reg, 0); - for (idx, (capture_name, capture_ty, by_ref)) in captures.iter().enumerate() { - emitter.comment(&format!("store preg_replace_callback capture ${}", capture_name)); - if *by_ref { - if !crate::codegen::expr::calls::args::emit_ref_arg_variable_address( - capture_name, - "preg_replace_callback capture ref", - emitter, - ctx, - ) { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - continue; - } - store_current_result_to_env_slot(emitter, &PhpType::Int, (idx + 1) * 16); - } else { - let Some(capture_info) = ctx.variables.get(capture_name) else { - emitter.comment(&format!( - "WARNING: captured callback variable ${} not found", - capture_name - )); - continue; - }; - abi::emit_load(emitter, capture_ty, capture_info.stack_offset); - store_current_result_to_env_slot(emitter, capture_ty, (idx + 1) * 16); - } - } - - abi::emit_symbol_address(emitter, runtime_callback_reg, &wrapper_label); - abi::emit_temporary_stack_address(emitter, runtime_env_reg, 0); - env_bytes -} - -/// Stores a raw register value into an environment slot at `offset`. -/// -/// Uses `symbol_scratch_reg` to compute the slot address on the temporary stack, -/// then stores `reg` at that address. -fn store_reg_to_env_slot(emitter: &mut Emitter, reg: &str, offset: usize) { - let scratch = abi::symbol_scratch_reg(emitter); - abi::emit_temporary_stack_address(emitter, scratch, offset); - abi::emit_store_to_address(emitter, reg, scratch, 0); -} - -/// Stores the current expression result into an environment slot at `offset`. -/// -/// Reads the ABI result registers appropriate for `ty` (int, float, or string -/// pointer+length) and stores them into the environment slot. For `Float`, -/// uses `float_result_reg`; for `Str`, uses both pointer and length registers; -/// otherwise uses `int_result_reg`. No-op for `Void`/`Never`. -fn store_current_result_to_env_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - let scratch = abi::symbol_scratch_reg(emitter); - abi::emit_temporary_stack_address(emitter, scratch, offset); - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), scratch, 0); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_store_to_address(emitter, ptr_reg, scratch, 0); - abi::emit_store_to_address(emitter, len_reg, scratch, 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), scratch, 0); - } - } -} diff --git a/src/codegen/builtins/system/preg_split.rs b/src/codegen/builtins/system/preg_split.rs deleted file mode 100644 index c1648964d1..0000000000 --- a/src/codegen/builtins/system/preg_split.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Purpose: -//! Emits PHP `preg_split` PCRE-style regex builtin calls. -//! Connects pattern/subject arguments and optional match arrays to runtime regex helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Match arrays and false/error results must use PHP-compatible Mixed array payloads. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -const PREG_SPLIT_FORCE_MIXED_RESULT: i64 = 1 << 30; - -/// Emits the `preg_split` builtin call. -/// -/// # Arguments -/// - `args[0]`: pattern string -/// - `args[1]`: subject string -/// - `args[2]`: optional limit -/// - `args[3]`: optional flags -/// -/// # ABI Details -/// - ARM64: pattern in x1/x2, subject in x3/x4, limit in x5, flags in x6, result array pointer in x0 -/// - x86_64: pattern in rdi/rsi, subject in rdx/rcx, limit in r8, flags in r9, result array pointer in rax -/// -/// # Returns -/// `Array` when no flags argument is present, otherwise `Array` -/// so dynamic offset-capture flags cannot make the runtime layout disagree with -/// static codegen. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("preg_split()"); - - match emitter.target.arch { - Arch::AArch64 => { - // -- evaluate arguments in PHP source order -- - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push pattern ptr and len - emit_expr(&args[1], emitter, ctx, data); - emitter.instruction("stp x1, x2, [sp, #-16]!"); // push subject ptr and len - if let Some(limit) = args.get(2) { - emit_expr(limit, emitter, ctx, data); - } else { - abi::emit_load_int_immediate(emitter, "x0", -1); - } - abi::emit_push_reg(emitter, "x0"); - if let Some(flags) = args.get(3) { - emit_expr(flags, emitter, ctx, data); - abi::emit_load_int_immediate(emitter, "x9", PREG_SPLIT_FORCE_MIXED_RESULT); - emitter.instruction("orr x0, x0, x9"); // force boxed-Mixed result slots for dynamic split flags - } else { - abi::emit_load_int_immediate(emitter, "x0", 0); - } - abi::emit_push_reg(emitter, "x0"); - abi::emit_pop_reg(emitter, "x6"); - abi::emit_pop_reg(emitter, "x5"); - emitter.instruction("ldp x3, x4, [sp], #16"); // pop subject ptr/len into x3/x4 - emitter.instruction("ldp x1, x2, [sp], #16"); // pop pattern ptr/len into x1/x2 - emitter.instruction("bl __rt_preg_split"); // regex split → x0=array pointer - } - Arch::X86_64 => { - emit_expr(&args[0], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push pattern ptr and len - emit_expr(&args[1], emitter, ctx, data); - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push subject ptr and len - if let Some(limit) = args.get(2) { - emit_expr(limit, emitter, ctx, data); - } else { - abi::emit_load_int_immediate(emitter, "rax", -1); - } - abi::emit_push_reg(emitter, "rax"); - if let Some(flags) = args.get(3) { - emit_expr(flags, emitter, ctx, data); - abi::emit_load_int_immediate(emitter, "r10", PREG_SPLIT_FORCE_MIXED_RESULT); - emitter.instruction("or rax, r10"); // force boxed-Mixed result slots for dynamic split flags - } else { - abi::emit_load_int_immediate(emitter, "rax", 0); - } - abi::emit_push_reg(emitter, "rax"); - abi::emit_pop_reg(emitter, "r9"); - abi::emit_pop_reg(emitter, "r8"); - abi::emit_pop_reg_pair(emitter, "rdx", "rcx"); - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); - abi::emit_call_label(emitter, "__rt_preg_split"); // regex split → rax=array pointer - } - } - - let elem_ty = if args.len() >= 4 { - PhpType::Mixed - } else { - PhpType::Str - }; - Some(PhpType::Array(Box::new(elem_ty))) -} diff --git a/src/codegen/builtins/system/putenv.rs b/src/codegen/builtins/system/putenv.rs deleted file mode 100644 index 0cd51ef349..0000000000 --- a/src/codegen/builtins/system/putenv.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Purpose: -//! Emits PHP `putenv` environment/platform information builtin calls. -//! Delegates host environment lookup or platform string construction to runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Environment and platform state are observable and must not be folded as compile-time constants here. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP's `putenv(KEY=VALUE)` builtin. -/// Copies the KEY=VALUE string to the heap (putenv retains the pointer), -/// calls the C `putenv` function, and converts its integer return (0 = success) -/// into a PHP boolean. Falls back to the platform runtime for the actual syscall. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("putenv()"); - // -- evaluate the KEY=VALUE string -- - emit_expr(&args[0], emitter, ctx, data); - let copy_loop = ctx.next_label("putenv_copy"); - let copy_done = ctx.next_label("putenv_copy_done"); - match emitter.target.arch { - Arch::AArch64 => { - // -- copy string to heap so it persists (putenv keeps the pointer) -- - emitter.instruction("add x0, x2, #1"); // heap size = string len + 1 (null terminator) - emitter.instruction("stp x1, x2, [sp, #-16]!"); // save string ptr/len - emitter.instruction("bl __rt_heap_alloc"); // allocate persistent buffer → x0=heap_ptr - emitter.instruction("ldp x1, x2, [sp], #16"); // restore string ptr/len - emitter.instruction("mov x3, x0"); // save heap ptr in x3 - // -- copy bytes to heap -- - emitter.instruction("mov x4, #0"); // copy index = 0 - emitter.label(©_loop); - emitter.instruction("cmp x4, x2"); // compare index with length - emitter.instruction(&format!("b.ge {}", copy_done)); // done if index >= length - emitter.instruction("ldrb w5, [x1, x4]"); // load byte from source - emitter.instruction("strb w5, [x3, x4]"); // store byte to heap - emitter.instruction("add x4, x4, #1"); // increment index - emitter.instruction(&format!("b {}", copy_loop)); // continue copying - emitter.label(©_done); - emitter.instruction("strb wzr, [x3, x4]"); // null-terminate on heap - // -- call putenv with heap-allocated string -- - emitter.instruction("mov x0, x3"); // pass heap cstr to putenv - emitter.bl_c("putenv"); // set env var, returns 0 on success - // -- convert return value to bool (0=success → true=1) -- - emitter.instruction("cmp x0, #0"); // check if putenv returned 0 (success) - emitter.instruction("cset x0, eq"); // x0 = 1 if success, 0 if failure - } - Arch::X86_64 => { - // -- copy string to heap so it persists (putenv keeps the pointer) -- - emitter.instruction("sub rsp, 16"); // reserve an aligned spill area for the source ptr/len across heap allocation - emitter.instruction("mov QWORD PTR [rsp], rax"); // save the source string pointer before requesting a persistent buffer - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // save the source string length before requesting a persistent buffer - emitter.instruction("mov rax, rdx"); // seed the heap allocation size from the source string length - emitter.instruction("add rax, 1"); // heap size = string len + 1 (null terminator) - emitter.instruction("call __rt_heap_alloc"); // allocate persistent buffer → rax=heap_ptr - emitter.instruction("mov rcx, QWORD PTR [rsp]"); // restore the source string pointer after heap allocation - emitter.instruction("mov r8, QWORD PTR [rsp + 8]"); // restore the source string length after heap allocation - emitter.instruction("add rsp, 16"); // release the aligned source spill area after heap allocation - emitter.instruction("mov r9, rax"); // save the persistent destination buffer pointer for the copy loop and putenv call - // -- copy bytes to heap -- - emitter.instruction("mov r10, 0"); // copy index = 0 - emitter.label(©_loop); - emitter.instruction("cmp r10, r8"); // compare the current copy index against the source string length - emitter.instruction(&format!("jae {}", copy_done)); // stop once every source byte has been copied into the persistent buffer - emitter.instruction("mov r11b, BYTE PTR [rcx + r10]"); // load one byte from the source string payload - emitter.instruction("mov BYTE PTR [r9 + r10], r11b"); // store the copied byte into the persistent environment buffer - emitter.instruction("add r10, 1"); // advance the copy index to the next byte - emitter.instruction(&format!("jmp {}", copy_loop)); // continue copying until the full KEY=VALUE string has been persisted - emitter.label(©_done); - emitter.instruction("mov BYTE PTR [r9 + r10], 0"); // append the trailing C null terminator after the copied KEY=VALUE bytes - // -- call putenv with heap-allocated string -- - emitter.instruction("mov rdi, r9"); // pass the persistent KEY=VALUE buffer in the SysV first-argument register - emitter.bl_c("putenv"); // set env var, returns 0 on success - // -- convert return value to bool (0=success → true=1) -- - emitter.instruction("cmp rax, 0"); // check if putenv returned 0 (success) on Linux x86_64 - emitter.instruction("sete al"); // set AL when putenv succeeded so the result becomes a PHP true value - emitter.instruction("movzx rax, al"); // zero-extend the boolean result back into the native integer result register - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/system/shell_exec.rs b/src/codegen/builtins/system/shell_exec.rs deleted file mode 100644 index 21223e504f..0000000000 --- a/src/codegen/builtins/system/shell_exec.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `shell_exec` process-control or shell execution builtin calls. -//! Marshals command/status arguments into runtime helpers with PHP-visible output and exit behavior. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Process calls are effectful and may terminate or emit output, so lowering must preserve evaluation order. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `shell_exec` builtin calls. -/// -/// # Arguments -/// - `_name`: Unused; the builtin name is hardcoded as `shell_exec`. -/// - `args`: Single argument — the command string to execute. -/// -/// # Behavior -/// Evaluates the command string argument in source order, then calls the runtime -/// helper `__rt_shell_exec` to execute the command and capture stdout as a string. -/// Returns `PhpType::Str` as the captured output. -/// -/// # ABI -/// The runtime call uses target-aware ABI helpers to materialize arguments and -/// capture the ptr/len result in registers per the target convention. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("shell_exec()"); - // -- evaluate command string -- - emit_expr(&args[0], emitter, ctx, data); - // -- call runtime to execute command and capture output -- - abi::emit_call_label(emitter, "__rt_shell_exec"); // execute command via the target-aware shell helper → ptr/len result regs - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/sleep.rs b/src/codegen/builtins/system/sleep.rs deleted file mode 100644 index bd226e891b..0000000000 --- a/src/codegen/builtins/system/sleep.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Purpose: -//! Emits PHP `sleep` time/date builtin calls. -//! Marshals timestamp and format arguments into runtime helpers that consult wall-clock state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Time calls are effectful/non-deterministic and must preserve PHP scalar return conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `sleep($seconds)` builtin. -/// -/// Evaluates the seconds argument into `x0` then calls the libc `sleep` helper. -/// Returns an integer indicating whether sleep completed normally (0) or was -/// interrupted (non-zero), following PHP's `sleep` semantics. -/// -/// Inputs: -/// - `args[0]`: seconds to sleep, must evaluate to an integer -/// - `emitter`: assembly emitter for writing instructions -/// - `ctx`: current.codegen context (scope, locals, etc.) -/// - `data`: data section for relocations and constants -/// -/// ABI: `x0` holds the argument (seconds); return value in `x0` -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("sleep()"); - // -- evaluate seconds argument -- - let seconds_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, &seconds_ty); // unbox a Mixed/Union seconds argument into a raw integer - // -- call libc sleep (x0 = seconds) -- - emitter.bl_c("sleep"); // sleep for x0 seconds, returns 0 on success - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/system/strtotime.rs b/src/codegen/builtins/system/strtotime.rs deleted file mode 100644 index 06d25fff31..0000000000 --- a/src/codegen/builtins/system/strtotime.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Purpose: -//! Emits PHP `strtotime` time/date builtin calls. -//! Marshals timestamp and format arguments into runtime helpers that consult wall-clock state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Time calls are effectful/non-deterministic and must preserve PHP scalar return conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `strtotime(datetime[, baseTimestamp])` builtin call. -/// -/// Parses a date/time string and returns a Unix timestamp (seconds since epoch). The first -/// argument (date string) is passed as a runtime string to `__rt_strtotime`. When a second -/// `baseTimestamp` argument is supplied, relative/keyword/time-only forms are resolved -/// against it instead of the current time (this also backs `DateTime::modify()`). -/// -/// # Runtime ABI -/// - **AArch64**: `x1`=string ptr, `x2`=string len, `x0`=base timestamp, `x3`=has-base flag. -/// - **x86_64**: `rdi`=string ptr, `rsi`=string len, `rdx`=base timestamp, `rcx`=has-base flag. -/// -/// The base argument is evaluated first and parked on the stack (mirroring `date()`), so the -/// string-argument evaluation cannot clobber it. -/// -/// # Returns -/// For `strtotime`: `PhpType::Mixed` — the parsed timestamp boxed as an integer, or boxed -/// `false` on parse failure (matching PHP's `int|false`). The runtime reports failure with -/// an `i64::MIN` sentinel so `-1` stays usable as a real pre-epoch timestamp. -/// For the internal `__elephc_strtotime_raw` alias (used by the synthetic `DateTime` -/// constructor and `modify()`): `PhpType::Int` — the raw timestamp, with the failure -/// sentinel mapped to `-1` so internal callers keep plain integer storage. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("strtotime()"); - - let has_base = args.len() >= 2; - - match emitter.target.arch { - Arch::AArch64 => { - if has_base { - // -- evaluate base timestamp first, then park it across the string eval -- - let base_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &base_ty); // unbox a Mixed/Union base timestamp into a raw integer - emitter.instruction("str x0, [sp, #-16]!"); // push the base timestamp onto the stack - emit_expr(&args[0], emitter, ctx, data); - // x1=string ptr, x2=string len - emitter.instruction("ldr x0, [sp], #16"); // pop the base timestamp into x0 - emitter.instruction("mov x3, #1"); // signal that a base timestamp was provided - } else { - emit_expr(&args[0], emitter, ctx, data); - // x1=string ptr, x2=string len - emitter.instruction("mov x3, #0"); // no base timestamp → runtime uses the current time - } - } - Arch::X86_64 => { - if has_base { - // -- evaluate base timestamp first, then park it across the string eval -- - let base_ty = emit_expr(&args[1], emitter, ctx, data); - coerce_to_int(emitter, &base_ty); // unbox a Mixed/Union base timestamp into a raw integer - abi::emit_push_reg(emitter, "rax"); // save the base timestamp while the string expression is evaluated - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the input string pointer into the first SysV string-argument register - emitter.instruction("mov rsi, rdx"); // move the input string length into the paired SysV string-argument register - abi::emit_pop_reg(emitter, "rdx"); // restore the base timestamp into the base-argument register - emitter.instruction("mov rcx, 1"); // signal that a base timestamp was provided - } else { - emit_expr(&args[0], emitter, ctx, data); - emitter.instruction("mov rdi, rax"); // move the input string pointer into the first SysV string-argument register - emitter.instruction("mov rsi, rdx"); // move the input string length into the paired SysV string-argument register - emitter.instruction("xor ecx, ecx"); // no base timestamp → runtime uses the current time - } - } - } - - // -- call runtime to parse date string and return timestamp -- - abi::emit_call_label(emitter, "__rt_strtotime"); // parse the supported date/time string formats through the target-aware runtime helper - - if name == "__elephc_strtotime_raw" { - // Internal alias: keep a raw integer result, mapping the failure sentinel to -1 - // (the synthetic DateTime constructor and modify() store timestamps directly). - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("movz x13, #0x8000, lsl #48"); // load the i64::MIN parse-failure sentinel - emitter.instruction("cmp x0, x13"); // did the parse fail? - emitter.instruction("mov x13, #-1"); // legacy in-object failure value - emitter.instruction("csel x0, x13, x0, eq"); // sentinel → -1, otherwise keep the timestamp - } - Arch::X86_64 => { - emitter.instruction("movabs r10, -9223372036854775808"); // load the i64::MIN parse-failure sentinel - emitter.instruction("cmp rax, r10"); // did the parse fail? - emitter.instruction("mov r10, -1"); // legacy in-object failure value - emitter.instruction("cmove rax, r10"); // sentinel → -1, otherwise keep the timestamp - } - } - return Some(PhpType::Int); - } - - box_parse_result(emitter, ctx); - - Some(PhpType::Mixed) -} - -/// Box a raw `strtotime` result as a `Mixed` value. -/// -/// Reads the raw timestamp from `x0` (ARM64) or `rax` (x86_64). The `i64::MIN` -/// parse-failure sentinel boxes as boolean `false` (`tag = 3`); every other value — -/// including `-1`, a valid pre-epoch timestamp — boxes as an integer (`tag = 0`), -/// preserving PHP's `strtotime(...) === false` contract. -/// -/// Uses `ctx.next_label` to generate local branch labels unique to this invocation. -fn box_parse_result(emitter: &mut Emitter, ctx: &mut Context) { - let ok_label = ctx.next_label("strtotime_ok"); - let end_label = ctx.next_label("strtotime_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("movz x13, #0x8000, lsl #48"); // load the i64::MIN parse-failure sentinel - emitter.instruction("cmp x0, x13"); // distinguish a parsed timestamp from the failure sentinel - emitter.instruction(&format!("b.ne {}", ok_label)); // box a parsed timestamp as an integer result - emitter.instruction("mov x1, #0"); // false payload = 0 for the mixed bool box - emitter.instruction("mov x2, #0"); // bool mixed payloads do not use a high word - emitter.instruction("mov x0, #3"); // runtime tag 3 = bool false for a failed parse - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false so -1 remains a valid timestamp value - emitter.instruction(&format!("b {}", end_label)); // skip the integer boxing path after the failure result - emitter.label(&ok_label); - emitter.instruction("mov x1, x0"); // move the parsed timestamp into the mixed helper payload register - emitter.instruction("mov x2, #0"); // integer mixed payloads do not use a high word - emitter.instruction("mov x0, #0"); // runtime tag 0 = int for parsed timestamps - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the parsed timestamp as mixed - emitter.label(&end_label); - } - Arch::X86_64 => { - emitter.instruction("movabs r10, -9223372036854775808"); // load the i64::MIN parse-failure sentinel - emitter.instruction("cmp rax, r10"); // distinguish a parsed timestamp from the failure sentinel - emitter.instruction(&format!("jne {}", ok_label)); // box a parsed timestamp as an integer result - emitter.instruction("xor edi, edi"); // false payload = 0 for the mixed bool box - emitter.instruction("xor esi, esi"); // bool mixed payloads do not use a high word - emitter.instruction("mov eax, 3"); // runtime tag 3 = bool false for a failed parse - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box false so -1 remains a valid timestamp value - emitter.instruction(&format!("jmp {}", end_label)); // skip the integer boxing path after the failure result - emitter.label(&ok_label); - emitter.instruction("mov rdi, rax"); // move the parsed timestamp into the mixed helper payload register - emitter.instruction("xor esi, esi"); // integer mixed payloads do not use a high word - emitter.instruction("xor eax, eax"); // runtime tag 0 = int for parsed timestamps - abi::emit_call_label(emitter, "__rt_mixed_from_value"); // box the parsed timestamp as mixed - emitter.label(&end_label); - } - } -} diff --git a/src/codegen/builtins/system/system_fn.rs b/src/codegen/builtins/system/system_fn.rs deleted file mode 100644 index f225c451fb..0000000000 --- a/src/codegen/builtins/system/system_fn.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Emits PHP `system` process-control or shell execution builtin calls. -//! Marshals command/status arguments into runtime helpers with PHP-visible output and exit behavior. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Process calls are effectful and may terminate or emit output, so lowering must preserve evaluation order. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the PHP `system()` builtin call. -/// -/// Executes a command string via the C `system()` libc call. The command -/// output is written directly to stdout by the C library. This function -/// evaluates the command argument, null-terminates it via `__rt_cstr`, -/// calls `system()`, and returns an empty string since output is already -/// streamed to stdout. -/// -/// # Arguments -/// * `_name` — unused builtin name (the module dispatches by function name) -/// * `args` — must contain exactly one expression yielding the command string -/// -/// # Return -/// Always returns `PhpType::Str` (empty string), matching PHP `system()` semantics. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("system()"); - // -- evaluate command string -- - emit_expr(&args[0], emitter, ctx, data); - // -- null-terminate and call libc system() which outputs directly to stdout -- - abi::emit_call_label(emitter, "__rt_cstr"); // null-terminate the command string through the target-aware C-string helper - match emitter.target.arch { - Arch::AArch64 => {} - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // pass the null-terminated command pointer in the SysV first-argument register - } - } - emitter.bl_c("system"); // execute command, output goes to stdout - // -- return empty string (system() returns last line, but we let stdout handle it) -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, #0"); // return empty string ptr (null) after the direct stdout system() call - emitter.instruction("mov x2, #0"); // return empty string len = 0 after the direct stdout system() call - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // return empty string ptr (null) after the direct stdout system() call - emitter.instruction("mov rdx, 0"); // return empty string len = 0 after the direct stdout system() call - } - } - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/system/time.rs b/src/codegen/builtins/system/time.rs deleted file mode 100644 index 841183f19b..0000000000 --- a/src/codegen/builtins/system/time.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Purpose: -//! Emits PHP `time` time/date builtin calls. -//! Marshals timestamp and format arguments into runtime helpers that consult wall-clock state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Time calls are effectful/non-deterministic and must preserve PHP scalar return conventions. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `time()` builtin, which returns the current Unix timestamp. -/// -/// `name` and `args` are ignored—`time()` takes no arguments. Calls the `__rt_time` -/// runtime helper, which returns the current wall-clock Unix timestamp in the native -/// integer result register (`x0` on ARM64). Returns `PhpType::Int` to indicate the -/// result type is a signed integer. -pub fn emit( - _name: &str, - _args: &[Expr], - emitter: &mut Emitter, - _ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("time()"); - abi::emit_call_label(emitter, "__rt_time"); // call the target-aware runtime helper that returns the current Unix timestamp in the native integer result register - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/system/usleep.rs b/src/codegen/builtins/system/usleep.rs deleted file mode 100644 index 8489fc7649..0000000000 --- a/src/codegen/builtins/system/usleep.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Emits PHP `usleep` time/date builtin calls. -//! Marshals timestamp and format arguments into runtime helpers that consult wall-clock state. -//! -//! Called from: -//! - `crate::codegen::builtins::system::emit()`. -//! -//! Key details: -//! - Time calls are effectful/non-deterministic and must preserve PHP scalar return conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_int, emit_expr}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a call to the libc `usleep` function, suspending execution for the given number of microseconds. -/// -/// # Arguments -/// - `_name`: unused, always `"usleep"` -/// - `args[0]`: evaluated and placed in `x0` (ARM64 integer return register) as the sleep duration in microseconds -/// - `emitter`: controls output assembly -/// - `ctx`: carries codegen state -/// - `data`: data section for literals/constants -/// -/// # Returns -/// `Some(PhpType::Void)` — PHP `usleep` has no return value -/// -/// # Side effects -/// Invokes the `usleep` libc routine, which blocks the calling thread for the specified duration. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("usleep()"); - // -- evaluate microseconds argument -- - let micros_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_int(emitter, µs_ty); // unbox a Mixed/Union microseconds argument into a raw integer - // -- call libc usleep (x0 = microseconds) -- - emitter.bl_c("usleep"); // sleep for x0 microseconds - Some(PhpType::Void) -} diff --git a/src/codegen/builtins/types/boolval.rs b/src/codegen/builtins/types/boolval.rs deleted file mode 100644 index dec6d6f24a..0000000000 --- a/src/codegen/builtins/types/boolval.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `boolval` type conversion or type-name builtin calls. -//! Applies PHP scalar conversion rules or materializes runtime type names for values. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Conversion results must stay aligned with type-checker signatures and boxed Mixed handling. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{coerce_to_truthiness, emit_expr}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `boolval()` builtin, converting a value to boolean. -/// -/// Converts `args[0]` to PHP truthiness/falsiness using the shared -/// `coerce_to_truthiness` helper. The result is always `PhpType::Bool`. -/// -/// # Arguments -/// - `_name`: Unused; dispatch is already resolved. -/// - `args`: Single expression to convert. -/// - `emitter`: Target assembly emitter. -/// - `ctx`: Codegen context (variable layout, class metadata). -/// - `data`: Data section for literals and runtime symbols. -/// -/// # Returns -/// Always `Some(PhpType::Bool)`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("boolval()"); - // -- convert any value to boolean (truthy/falsy) -- - let src_ty = emit_expr(&args[0], emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &src_ty); // normalize the value to PHP truthiness through the shared target-aware coercion helper - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/class_alias.rs b/src/codegen/builtins/types/class_alias.rs deleted file mode 100644 index 42ba006571..0000000000 --- a/src/codegen/builtins/types/class_alias.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Provides a defensive codegen fallback for unsupported `class_alias` calls. -//! Keeps the builtin dispatcher total even though valid AOT alias calls are consumed earlier. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()` -//! -//! Key details: -//! - Top-level literal alias calls are compiled into synthetic subclass declarations by autoload. -//! - Any call reaching this file should already have been rejected by the checker. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a defensive codegen fallback for unsupported `class_alias` calls. -/// -/// Evaluates all arguments for side effects, then returns `false` (0) to indicate -/// the alias operation failed. This fallback should never be reached for valid -/// programs since autoload handles AOT alias resolution before codegen. -/// -/// Inputs: -/// - `name`: the builtin name (unused, always `"class_alias"`) -/// - `args`: the call arguments, evaluated for side effects -/// - `emitter`, `ctx`, `data`: codegen state -/// -/// Returns: -/// - Always `Some(PhpType::Bool)` indicating `false` / failed alias -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("class_alias() unsupported fallback"); - for arg in args { - emit_expr(arg, emitter, ctx, data); - } - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/class_exists.rs b/src/codegen/builtins/types/class_exists.rs deleted file mode 100644 index ac27675001..0000000000 --- a/src/codegen/builtins/types/class_exists.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Purpose: -//! Emits AOT results for `class_exists`, `interface_exists`, `trait_exists`, and `enum_exists`. -//! Evaluates arguments for side effects, then lowers literal lookups to an integer bool. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()` -//! -//! Key details: -//! - The autoload pass has already resolved literal autoload demands before codegen. -//! - Non-literal arguments are checker errors; codegen falls back to `false` defensively. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::names::php_symbol_key; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits code for `class_exists`, `interface_exists`, `trait_exists`, or `enum_exists`. -/// -/// Evaluates all arguments for side effects, then resolves literal string class names -/// against the folded symbol tables. Non-literal arguments are checker errors, but -/// codegen defensively returns `false` if encountered. -/// -/// Returns `PhpType::Bool` in `abi::int_result_reg()`. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}()", name)); - // Always evaluate every argument for side-effects (the user may have - // passed an expression with observable behavior). - for arg in args { - emit_expr(arg, emitter, ctx, data); - } - let value = literal_lookup_result(name, args, ctx).unwrap_or(0); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), value); - Some(PhpType::Bool) -} - -/// Resolves a literal class name argument to a folded symbol table lookup result. -/// -/// Extracts the string literal from the first argument, normalizes it with a leading -/// backslash trim, and checks the appropriate symbol table based on `name`. -/// Returns `Some(1)` if the class/interface/enum/trait exists, `Some(0)` if not, -/// or `None` if the first argument is not a string literal. -fn literal_lookup_result(name: &str, args: &[Expr], ctx: &Context) -> Option { - let first = args.first()?; - let ExprKind::StringLiteral(class) = &first.kind else { - return None; - }; - let cleaned = class.trim_start_matches('\\'); - let present = match name { - "class_exists" => contains_folded( - ctx.classes - .keys() - .filter(|name| !is_internal_synthetic_class_name(name)), - cleaned, - ), - "interface_exists" => contains_folded(ctx.interfaces.keys(), cleaned), - "enum_exists" => contains_folded(ctx.enums.keys(), cleaned), - "trait_exists" => contains_folded(ctx.traits.iter(), cleaned), - _ => return None, - }; - Some(if present { 1 } else { 0 }) -} - -/// Checks whether `needle` (a PHP-style symbol name) exists in `names` using PHP symbol key comparison. -/// -/// PHP symbol names are case-insensitive; this normalizes both the needle and each -/// name via `php_symbol_key` before comparing. -fn contains_folded<'a>( - mut names: impl Iterator, - needle: &str, -) -> bool { - let needle_key = php_symbol_key(needle); - names.any(|name| php_symbol_key(name) == needle_key) -} - -/// Returns true when internal synthetic class name. -fn is_internal_synthetic_class_name(name: &str) -> bool { - php_symbol_key(name).starts_with("__elephc") -} diff --git a/src/codegen/builtins/types/class_relations.rs b/src/codegen/builtins/types/class_relations.rs deleted file mode 100644 index a18ec2c560..0000000000 --- a/src/codegen/builtins/types/class_relations.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Purpose: -//! Emits AOT metadata arrays for `class_implements`, `class_parents`, and `class_uses`. -//! Resolves class-like names from static type information and declaration tables. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()` -//! -//! Key details: -//! - Arguments are evaluated for side effects before the folded metadata array is materialized. -//! - Results use PHP's associative shape: each key is the same string as its value. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::arrays::{ - emit_assoc_array_literal, emit_empty_assoc_array_literal, -}; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, emit_box_current_value_as_mixed}; -use crate::names::php_symbol_key; -use crate::parser::ast::{Expr, ExprKind}; -use crate::span::Span; -use crate::types::{ClassInfo, InterfaceInfo, PhpType}; - -enum ClassLikeTarget { - Class(String), - Interface(String), - Trait(String), - Unknown, -} - -/// Emits the class relations entry point for this module. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}() — AOT class metadata snapshot", name)); - - let first_ty = args.first().map(|arg| emit_expr(arg, emitter, ctx, data)); - for arg in args.iter().skip(1) { - emit_expr(arg, emitter, ctx, data); - } - - let target = resolve_target(args.first(), first_ty.as_ref(), ctx); - if matches!(target, ClassLikeTarget::Unknown) { - emit_false_result(emitter); - return Some(class_relation_return_type()); - } - - let names = relation_names(name, &target, ctx)?; - let array_ty = class_relation_array_type(); - emit_assoc_string_set(&names, args.first().map(|arg| arg.span), emitter, ctx, data); - emit_box_current_value_as_mixed(emitter, &array_ty); - Some(class_relation_return_type()) -} - -/// Computes relation array type for the PHP class-introspection builtin. -fn class_relation_array_type() -> PhpType { - PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Str), - } -} - -/// Computes relation return type for the PHP class-introspection builtin. -fn class_relation_return_type() -> PhpType { - PhpType::Union(vec![class_relation_array_type(), PhpType::Bool]) -} - -/// Resolves target using the available compile-time metadata. -fn resolve_target(arg: Option<&Expr>, arg_ty: Option<&PhpType>, ctx: &Context) -> ClassLikeTarget { - if let Some(Expr { - kind: ExprKind::StringLiteral(raw), - .. - }) = arg - { - if let Some(name) = lookup_class_name(ctx, raw) { - return ClassLikeTarget::Class(name); - } - if let Some(name) = lookup_interface_name(ctx, raw) { - return ClassLikeTarget::Interface(name); - } - if let Some(name) = lookup_trait_name(ctx, raw) { - return ClassLikeTarget::Trait(name); - } - return ClassLikeTarget::Unknown; - } - - if let Some(PhpType::Object(class_name)) = arg_ty { - if let Some(name) = lookup_class_name(ctx, class_name) { - return ClassLikeTarget::Class(name); - } - } - - ClassLikeTarget::Unknown -} - -/// Provides the Relation names helper used by the class relations module. -fn relation_names(name: &str, target: &ClassLikeTarget, ctx: &Context) -> Option> { - match name { - "class_implements" => Some(class_implements(target, ctx)), - "class_parents" => Some(class_parents(target, ctx)), - "class_uses" => Some(class_uses(target, ctx)), - _ => None, - } -} - -/// Computes implements for the PHP class-introspection builtin. -fn class_implements(target: &ClassLikeTarget, ctx: &Context) -> Vec { - match target { - ClassLikeTarget::Class(class_name) => lookup_class(ctx, class_name) - .map(|info| info.interfaces.clone()) - .unwrap_or_default(), - ClassLikeTarget::Interface(interface_name) => { - let mut names = Vec::new(); - collect_interface_parents(ctx, interface_name, &mut names); - names - } - ClassLikeTarget::Trait(_) | ClassLikeTarget::Unknown => Vec::new(), - } -} - -/// Computes parents for the PHP class-introspection builtin. -fn class_parents(target: &ClassLikeTarget, ctx: &Context) -> Vec { - let ClassLikeTarget::Class(class_name) = target else { - return Vec::new(); - }; - - let mut names = Vec::new(); - let mut current = class_name.clone(); - while let Some(info) = lookup_class(ctx, ¤t) { - let Some(parent) = &info.parent else { - break; - }; - let parent_name = lookup_class_name(ctx, parent).unwrap_or_else(|| parent.clone()); - names.push(parent_name.clone()); - current = parent_name; - } - names -} - -/// Computes uses for the PHP class-introspection builtin. -fn class_uses(target: &ClassLikeTarget, ctx: &Context) -> Vec { - match target { - ClassLikeTarget::Class(class_name) => lookup_class(ctx, class_name) - .map(|info| info.used_traits.clone()) - .unwrap_or_default(), - ClassLikeTarget::Trait(trait_name) => crate::codegen::declared_trait_uses(trait_name), - ClassLikeTarget::Interface(_) | ClassLikeTarget::Unknown => Vec::new(), - } -} - -/// Collects interface parents for the surrounding analysis or metadata result. -fn collect_interface_parents(ctx: &Context, interface_name: &str, names: &mut Vec) { - let Some(interface) = lookup_interface(ctx, interface_name) else { - return; - }; - for parent in &interface.parents { - let parent_name = lookup_interface_name(ctx, parent).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_interface_parents(ctx, &parent_name, names); - } - } -} - -/// Emits assembly for assoc string set. -fn emit_assoc_string_set( - names: &[String], - span: Option, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if names.is_empty() { - emit_empty_assoc_array_literal(PhpType::Str, PhpType::Str, emitter); - return; - } - - let span = span.unwrap_or_else(Span::dummy); - let pairs: Vec<(Expr, Expr)> = names - .iter() - .map(|name| { - let key = Expr::new(ExprKind::StringLiteral(name.clone()), span); - let value = Expr::new(ExprKind::StringLiteral(name.clone()), span); - (key, value) - }) - .collect(); - emit_assoc_array_literal(&pairs, emitter, ctx, data); -} - -/// Emits assembly for false result. -fn emit_false_result(emitter: &mut Emitter) { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - emit_box_current_value_as_mixed(emitter, &PhpType::Bool); -} - -/// Looks up class name and returns the matching metadata when present. -fn lookup_class_name(ctx: &Context, raw: &str) -> Option { - lookup_folded(ctx.classes.keys(), raw) -} - -/// Looks up interface name and returns the matching metadata when present. -fn lookup_interface_name(ctx: &Context, raw: &str) -> Option { - lookup_folded(ctx.interfaces.keys(), raw) -} - -/// Looks up trait name and returns the matching metadata when present. -fn lookup_trait_name(ctx: &Context, raw: &str) -> Option { - lookup_folded(ctx.traits.iter(), raw) -} - -/// Looks up folded and returns the matching metadata when present. -fn lookup_folded<'a>(names: impl Iterator, raw: &str) -> Option { - let clean = raw.trim_start_matches('\\'); - let key = php_symbol_key(clean); - names - .into_iter() - .find(|name| php_symbol_key(name.trim_start_matches('\\')) == key) - .cloned() -} - -/// Looks up class and returns the matching metadata when present. -fn lookup_class<'a>(ctx: &'a Context, raw: &str) -> Option<&'a ClassInfo> { - let name = lookup_class_name(ctx, raw)?; - ctx.classes.get(&name) -} - -/// Looks up interface and returns the matching metadata when present. -fn lookup_interface<'a>(ctx: &'a Context, raw: &str) -> Option<&'a InterfaceInfo> { - let name = lookup_interface_name(ctx, raw)?; - ctx.interfaces.get(&name) -} diff --git a/src/codegen/builtins/types/empty.rs b/src/codegen/builtins/types/empty.rs deleted file mode 100644 index 4b052517e3..0000000000 --- a/src/codegen/builtins/types/empty.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Purpose: -//! Emits PHP `empty` checks without reducing them to ordinary boolean casts. -//! Handles unset/null/zero/empty string and array cases according to PHP truthiness rules. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Must distinguish undefined storage probes from evaluated expressions where PHP suppresses notices. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP's `empty(expr)` builtin. -/// -/// Evaluates whether `args[0]` is "empty" according to PHP truthiness rules, -/// then writes a boolean result to the canonical integer result register. -/// Handles all PHP types: int/float/bool compare against zero, null returns true, -/// strings compare length, arrays inspect element count, objects/resources/callables -/// return false, pointers check for null, and Mixed delegates to `__rt_mixed_is_empty`. -/// -/// # Arguments -/// * `name` - Unused; present to match the builtin emitter signature -/// * `args` - The expression to evaluate (exactly one) -/// * `emitter` - Target-aware instruction emitter -/// * `ctx` - Codegen context (labels, frame layout, types) -/// * `data` - Data section for relocations -/// -/// # Returns -/// `Some(PhpType::Bool)` as `empty()` always produces a boolean. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("empty()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - match &ty { - PhpType::Int | PhpType::TaggedScalar => { - // -- int is empty if it equals zero; a null tagged scalar narrows to zero -- - crate::codegen::expr::coerce_null_to_zero(emitter, &ty); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // compare the integer value against zero using the native AArch64 integer result register - emitter.instruction("cset x0, eq"); // normalize the AArch64 comparison result to 1 when the integer is zero and 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // compare the integer value against zero using the native x86_64 integer result register - emitter.instruction("sete al"); // materialize the x86_64 comparison result in the low byte when the integer is zero - emitter.instruction("movzx eax, al"); // widen the x86_64 boolean byte back into the canonical integer result register - } - } - } - PhpType::Float => { - // -- float is empty if it equals 0.0 -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fcmp d0, #0.0"); // compare the float value against 0.0 using the native AArch64 floating-point compare instruction - emitter.instruction("cset x0, eq"); // normalize the AArch64 floating-point comparison to 1 when the value is 0.0 and 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("xorpd xmm1, xmm1"); // materialize a canonical 0.0 comparison operand in a scratch SIMD register for the x86_64 compare - emitter.instruction("ucomisd xmm0, xmm1"); // compare the float result against 0.0 using the native x86_64 scalar-double compare - emitter.instruction("sete al"); // materialize the x86_64 floating-point comparison result in the low byte when the value is 0.0 - emitter.instruction("movzx eax, al"); // widen the x86_64 boolean byte back into the canonical integer result register - } - } - } - PhpType::Bool => { - // -- bool is empty if false (0) -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // compare the boolean payload against false using the native AArch64 integer result register - emitter.instruction("cset x0, eq"); // normalize the AArch64 comparison result to 1 when the boolean is false and 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // compare the boolean payload against false using the native x86_64 integer result register - emitter.instruction("sete al"); // materialize the x86_64 comparison result in the low byte when the boolean is false - emitter.instruction("movzx eax, al"); // widen the x86_64 boolean byte back into the canonical integer result register - } - } - } - PhpType::Void | PhpType::Never => { - // -- null is always empty -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // null is always empty, so return true in the native AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("mov eax, 1"); // null is always empty, so return true in the native x86_64 integer result register - } - } - } - PhpType::Iterable => { - // -- iterable values are raw heap pointers, so inspect the heap kind before applying empty() -- - let array_case = ctx.next_label("empty_iterable_array"); - let false_case = ctx.next_label("empty_iterable_false"); - let true_case = ctx.next_label("empty_iterable_true"); - let done = ctx.next_label("empty_iterable_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the iterable pointer while checking its heap kind - emitter.instruction("bl __rt_heap_kind"); // classify the raw iterable pointer by heap kind - emitter.instruction("cmp x0, #2"); // is this iterable backed by an indexed array? - emitter.instruction(&format!("b.eq {}", array_case)); // indexed arrays are empty only when their length is zero - emitter.instruction("cmp x0, #3"); // is this iterable backed by an associative array? - emitter.instruction(&format!("b.eq {}", array_case)); // associative arrays are empty only when their length is zero - emitter.instruction("cmp x0, #4"); // is this iterable backed by an object? - emitter.instruction(&format!("b.eq {}", false_case)); // objects are never empty in PHP - emitter.instruction(&format!("b {}", true_case)); // null/unknown iterable payloads are treated as empty - - emitter.label(&array_case); - emitter.instruction("ldr x9, [sp], #16"); // restore the array/hash pointer from the temporary stack slot - emitter.instruction("ldr x0, [x9]"); // load the container element count from the shared header layout - emitter.instruction("cmp x0, #0"); // compare the iterable container length against zero - emitter.instruction("cset x0, eq"); // return true only when the iterable container has no elements - emitter.instruction(&format!("b {}", done)); // finish after the array/hash empty result is materialized - - emitter.label(&false_case); - emitter.instruction("add sp, sp, #16"); // discard the preserved iterable pointer before returning false - emitter.instruction("mov x0, #0"); // object-backed iterables are not empty - emitter.instruction(&format!("b {}", done)); // finish after the false result is materialized - - emitter.label(&true_case); - emitter.instruction("add sp, sp, #16"); // discard the preserved iterable pointer before returning true - emitter.instruction("mov x0, #1"); // null or unknown iterable payloads are empty - emitter.label(&done); - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the iterable pointer while checking its heap kind - emitter.instruction("call __rt_heap_kind"); // classify the raw iterable pointer by heap kind - emitter.instruction("cmp rax, 2"); // is this iterable backed by an indexed array? - emitter.instruction(&format!("je {}", array_case)); // indexed arrays are empty only when their length is zero - emitter.instruction("cmp rax, 3"); // is this iterable backed by an associative array? - emitter.instruction(&format!("je {}", array_case)); // associative arrays are empty only when their length is zero - emitter.instruction("cmp rax, 4"); // is this iterable backed by an object? - emitter.instruction(&format!("je {}", false_case)); // objects are never empty in PHP - emitter.instruction(&format!("jmp {}", true_case)); // null/unknown iterable payloads are treated as empty - - emitter.label(&array_case); - abi::emit_pop_reg(emitter, "r10"); // restore the array/hash pointer from the temporary stack slot - emitter.instruction("mov rax, QWORD PTR [r10]"); // load the container element count from the shared header layout - emitter.instruction("cmp rax, 0"); // compare the iterable container length against zero - emitter.instruction("sete al"); // return true only when the iterable container has no elements - emitter.instruction("movzx rax, al"); // widen the boolean byte into the canonical integer result - emitter.instruction(&format!("jmp {}", done)); // finish after the array/hash empty result is materialized - - emitter.label(&false_case); - abi::emit_pop_reg(emitter, "r10"); // discard the preserved iterable pointer before returning false - emitter.instruction("xor eax, eax"); // object-backed iterables are not empty - emitter.instruction(&format!("jmp {}", done)); // finish after the false result is materialized - - emitter.label(&true_case); - abi::emit_pop_reg(emitter, "r10"); // discard the preserved iterable pointer before returning true - emitter.instruction("mov eax, 1"); // null or unknown iterable payloads are empty - emitter.label(&done); - } - } - } - PhpType::Mixed | PhpType::Union(_) => { - // -- mixed values use PHP empty() semantics for the boxed payload -- - abi::emit_call_label(emitter, "__rt_mixed_is_empty"); // inspect the boxed payload instead of the mixed box pointer through the target-aware runtime helper - } - PhpType::Str => { - // -- string is empty if length is zero -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x2, #0"); // compare the string length against zero using the native AArch64 string-length result register - emitter.instruction("cset x0, eq"); // normalize the AArch64 comparison result to 1 when the string length is zero and 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rdx, 0"); // compare the string length against zero using the native x86_64 string-length result register - emitter.instruction("sete al"); // materialize the x86_64 comparison result in the low byte when the string length is zero - emitter.instruction("movzx eax, al"); // widen the x86_64 boolean byte back into the canonical integer result register - } - } - } - PhpType::Array(_) | PhpType::AssocArray { .. } => { - // -- array is empty if element count is zero -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [x0]"); // load the container element count from the header into the AArch64 integer result register - emitter.instruction("cmp x0, #0"); // compare the container element count against zero on AArch64 - emitter.instruction("cset x0, eq"); // normalize the AArch64 comparison result to 1 when the container is empty and 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rax]"); // load the container element count from the header into the x86_64 integer result register - emitter.instruction("cmp rax, 0"); // compare the container element count against zero on x86_64 - emitter.instruction("sete al"); // materialize the x86_64 comparison result in the low byte when the container is empty - emitter.instruction("movzx eax, al"); // widen the x86_64 boolean byte back into the canonical integer result register - } - } - } - PhpType::Callable | PhpType::Object(_) => { - // -- callable/object is never empty -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // callable/object values are never empty, so return false in the native AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // callable/object values are never empty, so return false in the native x86_64 integer result register - } - } - } - PhpType::Resource(_) => { - // -- resources are never empty in PHP -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // resource values are never empty, so return false in the native AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // resource values are never empty, so return false in the native x86_64 integer result register - } - } - } - PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) => { - // -- pointer is empty only when it is the null pointer -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // compare the pointer-like value against null using the native AArch64 integer result register - emitter.instruction("cset x0, eq"); // normalize the AArch64 comparison result to 1 when the pointer-like value is null and 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // compare the pointer-like value against null using the native x86_64 integer result register - emitter.instruction("sete al"); // materialize the x86_64 comparison result in the low byte when the pointer-like value is null - emitter.instruction("movzx eax, al"); // widen the x86_64 boolean byte back into the canonical integer result register - } - } - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/floatval.rs b/src/codegen/builtins/types/floatval.rs deleted file mode 100644 index 4cea15e9f1..0000000000 --- a/src/codegen/builtins/types/floatval.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Emits PHP `floatval` type conversion or type-name builtin calls. -//! Applies PHP scalar conversion rules or materializes runtime type names for values. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Conversion results must stay aligned with type-checker signatures and boxed Mixed handling. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::abi; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for the PHP `floatval()` builtin, which converts a value to a float. -/// -/// Converts the first argument to a double-precision floating-point value. -/// If the argument is already a `Float`, no conversion occurs; otherwise an integer -/// result is converted to the target float register via the ABI conversion routine. -/// -/// - `args[0]`: the expression to convert -/// - `emitter`: used to emit instructions and comments -/// - `ctx`: carries variable layout and compilation context -/// - `data`: data section for literals and runtime symbols -/// - Returns `Some(PhpType::Float)` unconditionally; callers can ignore the result. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("floatval()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if ty != PhpType::Float { - // -- convert integer to double-precision float -- - abi::emit_int_result_to_float_result(emitter); // convert signed int result to the target float result register - } - Some(PhpType::Float) -} diff --git a/src/codegen/builtins/types/get_class.rs b/src/codegen/builtins/types/get_class.rs deleted file mode 100644 index efdcc9fbd8..0000000000 --- a/src/codegen/builtins/types/get_class.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Purpose: -//! Emits `get_class()` and `get_parent_class()` through runtime object class-id lookup. -//! Materializes no-argument scope lookups statically and object arguments dynamically. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()` -//! -//! Key details: -//! - Arguments are still evaluated for side effects before class-name results are loaded. -//! - Object arguments use dense class-name metadata so caught base-type variables keep their concrete class. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits `get_class()` or `get_parent_class()`. -/// -/// `get_class()` with no arguments returns the current class name from `ctx.current_class`. -/// With an object argument, evaluates the argument and reads the runtime class id from the -/// object header, preserving concrete subclasses even when the static type is a parent or interface. -/// Non-object arguments currently return an empty string after evaluation. -/// -/// `get_parent_class()` with no arguments resolves the current class parent through `ctx.classes`. -/// With an object argument, it resolves the runtime object's parent id through emitted metadata. -/// -/// The resolved class name is emitted as a string literal into the data section, and its -/// address/length are published via ABI string-result registers (`x1`/`x2` on ARM64). -/// -/// # Arguments -/// * `name` — `"get_class"` or `"get_parent_class"` -/// * `args` — call arguments (empty for no-arg variant, one argument otherwise) -/// * `emitter` — code emitter -/// * `ctx` — codegen context (provides `current_class` and `classes` map) -/// * `data` — data section for string literal emission -/// -/// # Returns -/// `Some(PhpType::Str)` — the result type is always a string -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}() — class-name lookup", name)); - - let resolved_class = if args.is_empty() { - ctx.current_class.clone().unwrap_or_default() - } else { - let arg_ty = emit_expr(&args[0], emitter, ctx, data); - match arg_ty { - PhpType::Object(_) => { - emit_dynamic_object_class_name(name, emitter, ctx); - return Some(PhpType::Str); - } - _ => String::new(), - } - }; - - let final_name = match name { - "get_class" => resolved_class, - "get_parent_class" => parent_of(&resolved_class, ctx), - _ => String::new(), - }; - - let bytes = final_name.as_bytes(); - let (label, len) = data.add_string(bytes); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_symbol_address(emitter, ptr_reg, &label); // expose the resolved class name in the string-pointer result register - abi::emit_load_int_immediate(emitter, len_reg, len as i64); // publish the resolved class name length in the paired length result register - Some(PhpType::Str) -} - -/// Emits dynamic class-name lookup for an object pointer in the integer result register. -/// -/// `get_class()` indexes `_class_name_entries` by the object's runtime class id. -/// `get_parent_class()` first maps the runtime class id through `_class_parent_ids`. -/// Invalid or parentless class ids return the shared zero-length `_class_name_missing` string. -fn emit_dynamic_object_class_name(name: &str, emitter: &mut Emitter, ctx: &mut Context) { - let empty_label = ctx.next_label("get_class_empty"); - let done_label = ctx.next_label("get_class_done"); - match emitter.target.arch { - Arch::AArch64 => emit_dynamic_object_class_name_arm64(name, &empty_label, &done_label, emitter), - Arch::X86_64 => emit_dynamic_object_class_name_x86_64(name, &empty_label, &done_label, emitter), - } -} - -/// Emits ARM64 runtime object class-name lookup for `get_class()` and `get_parent_class()`. -fn emit_dynamic_object_class_name_arm64( - name: &str, - empty_label: &str, - done_label: &str, - emitter: &mut Emitter, -) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - emitter.instruction(&format!("cbz x0, {}", empty_label)); // null object pointers produce an empty class name - emitter.instruction("ldr x9, [x0]"); // load the object's concrete runtime class id - abi::emit_symbol_address(emitter, "x10", "_class_name_count"); - emitter.instruction("ldr x10, [x10]"); // x10 = number of dense class-name lookup rows - if name == "get_parent_class" { - emitter.instruction("cmp x9, x10"); // validate the object class id before reading its parent id - emitter.instruction(&format!("b.hs {}", empty_label)); // unknown object class ids have no reportable parent class - abi::emit_symbol_address(emitter, "x11", "_class_parent_ids"); - emitter.instruction("lsl x12, x9, #3"); // scale the class id to a parent-id table byte offset - emitter.instruction("ldr x9, [x11, x12]"); // replace the object class id with its parent class id - emitter.instruction("mov x13, #-1"); // x13 = parentless class sentinel - emitter.instruction("cmp x9, x13"); // check whether the runtime class has no parent - emitter.instruction(&format!("b.eq {}", empty_label)); // parentless runtime classes produce an empty string - } - emitter.instruction("cmp x9, x10"); // validate the class id before indexing class-name metadata - emitter.instruction(&format!("b.hs {}", empty_label)); // invalid class ids produce an empty class name - abi::emit_symbol_address(emitter, "x11", "_class_name_entries"); - emitter.instruction("lsl x12, x9, #4"); // scale the class id by the 16-byte class-name row size - emitter.instruction("add x11, x11, x12"); // x11 = selected class-name metadata row - emitter.instruction(&format!("ldr {}, [x11]", ptr_reg)); // load the concrete class-name string pointer - emitter.instruction(&format!("ldr {}, [x11, #8]", len_reg)); // load the concrete class-name string length - emitter.instruction(&format!("b {}", done_label)); // skip the empty-string fallback after a successful lookup - - emitter.label(empty_label); - abi::emit_symbol_address(emitter, ptr_reg, "_class_name_missing"); - abi::emit_load_int_immediate(emitter, len_reg, 0); - - emitter.label(done_label); -} - -/// Emits x86_64 runtime object class-name lookup for `get_class()` and `get_parent_class()`. -fn emit_dynamic_object_class_name_x86_64( - name: &str, - empty_label: &str, - done_label: &str, - emitter: &mut Emitter, -) { - emitter.instruction("test rax, rax"); // null object pointers produce an empty class name - emitter.instruction(&format!("je {}", empty_label)); // branch to the empty-string fallback for null object pointers - emitter.instruction("mov r8, QWORD PTR [rax]"); // load the object's concrete runtime class id - abi::emit_load_symbol_to_reg(emitter, "r9", "_class_name_count", 0); // r9 = number of dense class-name lookup rows - if name == "get_parent_class" { - emitter.instruction("cmp r8, r9"); // validate the object class id before reading its parent id - emitter.instruction(&format!("jae {}", empty_label)); // unknown object class ids have no reportable parent class - abi::emit_symbol_address(emitter, "r10", "_class_parent_ids"); // materialize the runtime parent-id table base pointer - emitter.instruction("mov r8, QWORD PTR [r10 + r8 * 8]"); // replace the object class id with its parent class id - emitter.instruction("cmp r8, -1"); // check whether the runtime class has no parent - emitter.instruction(&format!("je {}", empty_label)); // parentless runtime classes produce an empty string - } - emitter.instruction("cmp r8, r9"); // validate the class id before indexing class-name metadata - emitter.instruction(&format!("jae {}", empty_label)); // invalid class ids produce an empty class name - abi::emit_symbol_address(emitter, "r10", "_class_name_entries"); // materialize the class-name metadata table base pointer - emitter.instruction("shl r8, 4"); // scale the class id by the 16-byte class-name row size - emitter.instruction("mov rax, QWORD PTR [r10 + r8]"); // load the concrete class-name string pointer - emitter.instruction("mov rdx, QWORD PTR [r10 + r8 + 8]"); // load the concrete class-name string length - emitter.instruction(&format!("jmp {}", done_label)); // skip the empty-string fallback after a successful lookup - - emitter.label(empty_label); - abi::emit_symbol_address(emitter, "rax", "_class_name_missing"); // return the shared empty class-name string pointer - emitter.instruction("xor edx, edx"); // return zero bytes for the empty class name - - emitter.label(done_label); -} - -/// Returns the parent class name for `class_name`, consulting `ctx.classes`. -/// -/// Returns an empty string if `class_name` is empty or the class has no parent entry. -/// -/// # Arguments -/// * `class_name` — fully or partially qualified class name -/// * `ctx` — codegen context providing the class metadata map -/// -/// # Returns -/// Parent class name as a `String`, or empty string if unavailable -fn parent_of(class_name: &str, ctx: &Context) -> String { - if class_name.is_empty() { - return String::new(); - } - ctx.classes - .get(class_name.trim_start_matches('\\')) - .and_then(|info| info.parent.clone()) - .unwrap_or_default() -} diff --git a/src/codegen/builtins/types/get_declared.rs b/src/codegen/builtins/types/get_declared.rs deleted file mode 100644 index dd32a3e107..0000000000 --- a/src/codegen/builtins/types/get_declared.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Purpose: -//! Emits `get_declared_classes()`, `get_declared_interfaces()`, and `get_declared_traits()`. -//! Materializes compile-time declaration registries as indexed string arrays. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()` -//! -//! Key details: -//! - Internal names are emitted first in deterministic order, then user declarations in source order. -//! - The fallback path sorts map keys for tests or callers that bypass normal codegen setup. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits `get_declared_classes()`, `get_declared_interfaces()`, and `get_declared_traits()`. -/// Uses the compile-time declaration registry (from codegen pass) when available; falls back to -/// `ctx.classes`/`ctx.interfaces`/`ctx.traits` sorted by key when the registry is empty (e.g., certain -/// test harnesses or unusual codegen paths). Allocates an array via `__rt_array_new`, then populates -/// it by pushing each name string through `emit_push_names`. Returns `Some(Array(Str))` on success -/// or `None` if `name` does not match a known declaration-bucket builtin. -/// -/// Arguments: -/// - `name`: one of `"get_declared_classes"`, `"get_declared_interfaces"`, `"get_declared_traits"` -/// - `_args`: not used by these builtins (they take no arguments) -/// - `emitter`: target-aware instruction emission -/// - `ctx`: declaration maps used for fallback path -/// - `data`: data section for string literal allocation -pub fn emit( - name: &str, - _args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let mut names: Vec = match name { - "get_declared_classes" => crate::codegen::declared_class_names(), - "get_declared_interfaces" => crate::codegen::declared_interface_names(), - "get_declared_traits" => crate::codegen::declared_trait_names(), - _ => return None, - }; - if names.is_empty() { - names = match name { - "get_declared_classes" => ctx - .classes - .keys() - .filter(|name| !is_internal_synthetic_class_name(name)) - .cloned() - .collect(), - "get_declared_interfaces" => ctx.interfaces.keys().cloned().collect(), - "get_declared_traits" => ctx.traits.iter().cloned().collect(), - _ => unreachable!(), - }; - names.sort(); - } - - emitter.comment(&format!("{}() — AOT introspection snapshot", name)); - - // -- allocate the result array with capacity = N, elem_size = 16 (str) -- - let cap = names.len().max(1); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", cap)); // request capacity for one entry per declared name - emitter.instruction("mov x1, #16"); // request 16-byte string slots so the array can store ptr+len pairs - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, {}", cap)); // request capacity for one entry per declared name - emitter.instruction("mov rsi, 16"); // request 16-byte string slots so the array can store ptr+len pairs - } - } - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the introspection array through the shared array constructor - - if !names.is_empty() { - emit_push_names(&names, emitter, data); - } - - Some(PhpType::Array(Box::new(PhpType::Str))) -} - -/// Returns true when internal synthetic class name. -fn is_internal_synthetic_class_name(name: &str) -> bool { - crate::names::php_symbol_key(name).starts_with("__elephc") -} - -/// Push each name onto the array via `__rt_array_push_str`. The array -/// pointer is parked on the stack between iterations because -/// `__rt_array_push_str` may grow the storage and return a new pointer. -/// Emits the per-name push sequence for the declared-names array. Parks the array pointer on the stack -/// while iterating `names` so that `__rt_array_push_str` can grow the vector and return a new pointer. -/// Each iteration: (1) reloads the current array pointer, (2) adds the name string to `data`, (3) calls -/// `__rt_array_push_str` to append it, and (4) saves the returned pointer back to the park slot. -/// On exit the final array pointer is restored to the register used for the call result. -/// -/// Arguments: -/// - `names`: ordered list of class/interface/trait names to push onto the array -/// - `emitter`: target-aware instruction emission -/// - `data`: data section for string literal allocation (each name is emitted as a literal) -fn emit_push_names(names: &[String], emitter: &mut Emitter, data: &mut DataSection) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // park the indexed-array pointer while we push the declared-name entries - for name in names { - let (label, len) = data.add_string(name.as_bytes()); - emitter.instruction("ldr x0, [sp]"); // reload the array pointer for this push call - abi::emit_symbol_address(emitter, "x1", &label); // load the address of this name's string literal - emitter.instruction(&format!("mov x2, #{}", len)); // load the length of this name's string literal - emitter.instruction("bl __rt_array_push_str"); // append the name and may grow the storage - emitter.instruction("str x0, [sp]"); // refresh the saved array pointer if __rt_array_push_str grew it - } - emitter.instruction("ldr x0, [sp], #16"); // restore the final array pointer as the builtin result - } - Arch::X86_64 => { - emitter.instruction("push rax"); // park the indexed-array pointer while we push the declared-name entries - emitter.instruction("sub rsp, 8"); // keep the stack 16-byte aligned for the call sequence - for name in names { - let (label, len) = data.add_string(name.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the array pointer for this push call - abi::emit_symbol_address(emitter, "rsi", &label); // load the address of this name's string literal - emitter.instruction(&format!("mov rdx, {}", len)); // load the length of this name's string literal - emitter.instruction("call __rt_array_push_str"); // append the name and may grow the storage - emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // refresh the saved array pointer if __rt_array_push_str grew it - } - emitter.instruction("add rsp, 8"); // pop the alignment padding before restoring the array pointer - emitter.instruction("pop rax"); // restore the final array pointer as the builtin result - } - } -} diff --git a/src/codegen/builtins/types/get_resource_id.rs b/src/codegen/builtins/types/get_resource_id.rs deleted file mode 100644 index ac3daabedb..0000000000 --- a/src/codegen/builtins/types/get_resource_id.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Emits PHP `get_resource_id` calls. -//! Returns the 1-based resource id — the descriptor plus one. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Reuses the shared `emit_stream_fd_arg` helper, then adds one so the id -//! matches elephc's 1-based `Resource id #N` display. - -use crate::codegen::builtins::io::stream_arg::emit_stream_fd_arg; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `get_resource_id()` resource/type builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("get_resource_id()"); - // The helper validates the argument and leaves the descriptor in the - // integer result register; elephc's resource id is the descriptor plus - // one, matching the 1-based "Resource id #N" display. - emit_stream_fd_arg("get_resource_id", &args[0], emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("add x0, x0, #1"); // descriptor -> 1-based resource id - } - Arch::X86_64 => { - emitter.instruction("add rax, 1"); // descriptor -> 1-based resource id - } - } - Some(PhpType::Int) -} diff --git a/src/codegen/builtins/types/get_resource_type.rs b/src/codegen/builtins/types/get_resource_type.rs deleted file mode 100644 index c5de08d1e3..0000000000 --- a/src/codegen/builtins/types/get_resource_type.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Emits PHP `get_resource_type` calls. -//! Returns the resource's type-name string after evaluating the argument. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Every resource elephc currently produces is a stream, so the result is the -//! constant `"stream"`; the argument is still evaluated for its side effects. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `get_resource_type()` resource/type builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("get_resource_type()"); - emit_expr(&args[0], emitter, ctx, data); - let (label, len) = data.add_string(b"stream"); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_symbol_address(emitter, ptr_reg, &label); // materialize the "stream" resource type-name literal - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, len)); // load the type-name byte length into the AArch64 string-length result register - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, len)); // load the type-name byte length into the x86_64 string-length result register - } - } - Some(PhpType::Str) -} diff --git a/src/codegen/builtins/types/gettype.rs b/src/codegen/builtins/types/gettype.rs deleted file mode 100644 index dc0f4daf66..0000000000 --- a/src/codegen/builtins/types/gettype.rs +++ /dev/null @@ -1,338 +0,0 @@ -//! Purpose: -//! Emits PHP `gettype` type conversion or type-name builtin calls. -//! Applies PHP scalar conversion rules or materializes runtime type names for values. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Conversion results must stay aligned with type-checker signatures and boxed Mixed handling. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP type-name string into the string-result registers and records it in -/// the data section. -/// -/// Adds `type_name` as a null-terminated string to the data section, then loads its -/// address into `ptr_reg` and its byte length into `len_reg` per the target ABI. -/// Always returns `Some(PhpType::Str)`. -fn emit_type_name_result( - emitter: &mut Emitter, - data: &mut DataSection, - type_name: &[u8], -) -> Option { - let (label, len) = data.add_string(type_name); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_symbol_address(emitter, ptr_reg, &label); // materialize the selected PHP type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, len)); // load the PHP type-name byte length into the active AArch64 string-length result register - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, len)); // load the PHP type-name byte length into the active x86_64 string-length result register - } - } - Some(PhpType::Str) -} - -/// Emits code for the `gettype()` builtin, which returns the PHP type name of a -/// value as a string. -/// -/// Handles three cases: -/// - `PhpType::Iterable`: probes the runtime heap kind tag to distinguish array, -/// object, and unknown heap representations and emits the corresponding PHP type name. -/// - `PhpType::Mixed` or `PhpType::Union`: unboxes the mixed payload and dispatches on -/// its runtime tag to emit one of: integer, double, string, boolean, NULL, array, -/// object, or resource. -/// - All other types: directly emits the known PHP type name string. -/// -/// Returns `Some(PhpType::Str)` with the type-name bytes materialized in the ABI -/// string-result registers. The caller is responsible for releasing any temporary -/// Mixed box owned by the expression before entry. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("gettype()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if matches!(ty, PhpType::Iterable) { - let (array_label, array_len) = data.add_string(b"array"); - let (object_label, object_len) = data.add_string(b"object"); - let (unknown_label, unknown_len) = data.add_string(b"unknown type"); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - let array_case = ctx.next_label("builtin_gettype_iter_array"); - let object_case = ctx.next_label("builtin_gettype_iter_object"); - let unknown_case = ctx.next_label("builtin_gettype_iter_unknown"); - let done = ctx.next_label("builtin_gettype_iter_done"); - - // -- iterable values are raw heap pointers; resolve their PHP type by reading - // the runtime heap kind and mapping array/hash kinds to "array" -- - abi::emit_call_label(emitter, "__rt_heap_kind"); // probe the runtime heap kind tag for the iterable operand - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #2"); // is the iterable backed by an indexed array? - emitter.instruction(&format!("b.eq {}", array_case)); // indexed arrays report PHP type \"array\" - emitter.instruction("cmp x0, #3"); // is the iterable backed by a hash table? - emitter.instruction(&format!("b.eq {}", array_case)); // hash tables also report PHP type \"array\" - emitter.instruction("cmp x0, #4"); // is the iterable backed by an object instance? - emitter.instruction(&format!("b.eq {}", object_case)); // object instances report PHP type \"object\" - emitter.instruction(&format!("b {}", unknown_case)); // any other heap kind falls back to \"unknown type\" - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 2"); // is the iterable backed by an indexed array? - emitter.instruction(&format!("je {}", array_case)); // indexed arrays report PHP type \"array\" - emitter.instruction("cmp rax, 3"); // is the iterable backed by a hash table? - emitter.instruction(&format!("je {}", array_case)); // hash tables also report PHP type \"array\" - emitter.instruction("cmp rax, 4"); // is the iterable backed by an object instance? - emitter.instruction(&format!("je {}", object_case)); // object instances report PHP type \"object\" - emitter.instruction(&format!("jmp {}", unknown_case)); // any other heap kind falls back to \"unknown type\" - } - } - - emitter.label(&array_case); - abi::emit_symbol_address(emitter, ptr_reg, &array_label); // materialize the array type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, array_len)); // load the array type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the array type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, array_len)); // load the array type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the array type string on x86_64 - } - } - - emitter.label(&object_case); - abi::emit_symbol_address(emitter, ptr_reg, &object_label); // materialize the object type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, object_len)); // load the object type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the object type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, object_len)); // load the object type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the object type string on x86_64 - } - } - - emitter.label(&unknown_case); - abi::emit_symbol_address(emitter, ptr_reg, &unknown_label); // materialize the unknown type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, unknown_len)); // load the unknown type-name byte length into the active AArch64 string-length result register - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, unknown_len)); // load the unknown type-name byte length into the active x86_64 string-length result register - } - } - emitter.label(&done); - return Some(PhpType::Str); - } - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - let (integer_label, integer_len) = data.add_string(b"integer"); - let (double_label, double_len) = data.add_string(b"double"); - let (string_label, string_len) = data.add_string(b"string"); - let (boolean_label, boolean_len) = data.add_string(b"boolean"); - let (null_label, null_len) = data.add_string(b"NULL"); - let (array_label, array_len) = data.add_string(b"array"); - let (object_label, object_len) = data.add_string(b"object"); - let (resource_label, resource_len) = data.add_string(b"resource"); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - let integer_case = ctx.next_label("builtin_gettype_mixed_integer"); - let double_case = ctx.next_label("builtin_gettype_mixed_double"); - let string_case = ctx.next_label("builtin_gettype_mixed_string"); - let boolean_case = ctx.next_label("builtin_gettype_mixed_boolean"); - let null_case = ctx.next_label("builtin_gettype_mixed_null"); - let array_case = ctx.next_label("builtin_gettype_mixed_array"); - let object_case = ctx.next_label("builtin_gettype_mixed_object"); - let resource_case = ctx.next_label("builtin_gettype_mixed_resource"); - let done = ctx.next_label("builtin_gettype_mixed_done"); - - // -- mixed gettype() unwraps the payload and dispatches on its concrete runtime tag -- - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // resolve the boxed payload tag before selecting the PHP type string - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // check whether the unboxed mixed tag denotes an integer payload - emitter.instruction(&format!("b.eq {}", integer_case)); // integers map to PHP's integer type name - emitter.instruction("cmp x0, #1"); // check whether the unboxed mixed tag denotes a string payload - emitter.instruction(&format!("b.eq {}", string_case)); // strings map to PHP's string type name - emitter.instruction("cmp x0, #2"); // check whether the unboxed mixed tag denotes a float payload - emitter.instruction(&format!("b.eq {}", double_case)); // floats map to PHP's double type name - emitter.instruction("cmp x0, #3"); // check whether the unboxed mixed tag denotes a boolean payload - emitter.instruction(&format!("b.eq {}", boolean_case)); // booleans map to PHP's boolean type name - emitter.instruction("cmp x0, #4"); // check whether the unboxed mixed tag denotes an indexed-array payload - emitter.instruction(&format!("b.eq {}", array_case)); // indexed arrays map to PHP's array type name - emitter.instruction("cmp x0, #5"); // check whether the unboxed mixed tag denotes an associative-array payload - emitter.instruction(&format!("b.eq {}", array_case)); // associative arrays also map to PHP's array type name - emitter.instruction("cmp x0, #6"); // check whether the unboxed mixed tag denotes an object payload - emitter.instruction(&format!("b.eq {}", object_case)); // objects map to PHP's object type name - emitter.instruction("cmp x0, #9"); // check whether the unboxed mixed tag denotes a resource payload - emitter.instruction(&format!("b.eq {}", resource_case)); // resources map to PHP's resource type name - emitter.instruction(&format!("b {}", null_case)); // null and unknown tags fall back to PHP's NULL type name - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // check whether the unboxed mixed tag denotes an integer payload - emitter.instruction(&format!("je {}", integer_case)); // integers map to PHP's integer type name - emitter.instruction("cmp rax, 1"); // check whether the unboxed mixed tag denotes a string payload - emitter.instruction(&format!("je {}", string_case)); // strings map to PHP's string type name - emitter.instruction("cmp rax, 2"); // check whether the unboxed mixed tag denotes a float payload - emitter.instruction(&format!("je {}", double_case)); // floats map to PHP's double type name - emitter.instruction("cmp rax, 3"); // check whether the unboxed mixed tag denotes a boolean payload - emitter.instruction(&format!("je {}", boolean_case)); // booleans map to PHP's boolean type name - emitter.instruction("cmp rax, 4"); // check whether the unboxed mixed tag denotes an indexed-array payload - emitter.instruction(&format!("je {}", array_case)); // indexed arrays map to PHP's array type name - emitter.instruction("cmp rax, 5"); // check whether the unboxed mixed tag denotes an associative-array payload - emitter.instruction(&format!("je {}", array_case)); // associative arrays also map to PHP's array type name - emitter.instruction("cmp rax, 6"); // check whether the unboxed mixed tag denotes an object payload - emitter.instruction(&format!("je {}", object_case)); // objects map to PHP's object type name - emitter.instruction("cmp rax, 9"); // check whether the unboxed mixed tag denotes a resource payload - emitter.instruction(&format!("je {}", resource_case)); // resources map to PHP's resource type name - emitter.instruction(&format!("jmp {}", null_case)); // null and unknown tags fall back to PHP's NULL type name - } - } - - emitter.label(&integer_case); - abi::emit_symbol_address(emitter, ptr_reg, &integer_label); // materialize the integer type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, integer_len)); // load the integer type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the integer type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, integer_len)); // load the integer type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the integer type string on x86_64 - } - } - - emitter.label(&double_case); - abi::emit_symbol_address(emitter, ptr_reg, &double_label); // materialize the double type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, double_len)); // load the double type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the double type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, double_len)); // load the double type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the double type string on x86_64 - } - } - - emitter.label(&string_case); - abi::emit_symbol_address(emitter, ptr_reg, &string_label); // materialize the string type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, string_len)); // load the string type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the string type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, string_len)); // load the string type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the string type string on x86_64 - } - } - - emitter.label(&boolean_case); - abi::emit_symbol_address(emitter, ptr_reg, &boolean_label); // materialize the boolean type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, boolean_len)); // load the boolean type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the boolean type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, boolean_len)); // load the boolean type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the boolean type string on x86_64 - } - } - - emitter.label(&null_case); - abi::emit_symbol_address(emitter, ptr_reg, &null_label); // materialize the NULL type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, null_len)); // load the NULL type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the NULL type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, null_len)); // load the NULL type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the NULL type string on x86_64 - } - } - - emitter.label(&array_case); - abi::emit_symbol_address(emitter, ptr_reg, &array_label); // materialize the array type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, array_len)); // load the array type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the array type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, array_len)); // load the array type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the array type string on x86_64 - } - } - - emitter.label(&object_case); - abi::emit_symbol_address(emitter, ptr_reg, &object_label); // materialize the object type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, object_len)); // load the object type-name byte length into the active AArch64 string-length result register - emitter.instruction(&format!("b {}", done)); // finish after selecting the object type string on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, object_len)); // load the object type-name byte length into the active x86_64 string-length result register - emitter.instruction(&format!("jmp {}", done)); // finish after selecting the object type string on x86_64 - } - } - - emitter.label(&resource_case); - abi::emit_symbol_address(emitter, ptr_reg, &resource_label); // materialize the resource type-name literal in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, resource_len)); // load the resource type-name byte length into the active AArch64 string-length result register - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, resource_len)); // load the resource type-name byte length into the active x86_64 string-length result register - } - } - emitter.label(&done); - return Some(PhpType::Str); - } - - if matches!(&ty, PhpType::TaggedScalar) { - let null_case = ctx.next_label("gettype_tagged_null"); - let done = ctx.next_label("gettype_tagged_done"); - crate::codegen::sentinels::emit_branch_if_tagged_scalar_null(emitter, &null_case); - emit_type_name_result(emitter, data, b"integer"); - abi::emit_jump(emitter, &done); // skip the NULL literal after selecting the integer type name - emitter.label(&null_case); - emit_type_name_result(emitter, data, b"NULL"); - emitter.label(&done); - return Some(PhpType::Str); - } - - let type_str = match &ty { - PhpType::Int => b"integer".as_slice(), - PhpType::Float => b"double".as_slice(), - PhpType::Str => b"string".as_slice(), - PhpType::Bool => b"boolean".as_slice(), - PhpType::Void | PhpType::Never => b"NULL".as_slice(), - PhpType::Array(_) | PhpType::AssocArray { .. } => b"array".as_slice(), - PhpType::Callable => b"callable".as_slice(), - PhpType::Object(_) => b"object".as_slice(), - PhpType::Pointer(_) => b"pointer".as_slice(), - PhpType::Buffer(_) => b"buffer".as_slice(), - PhpType::Packed(_) => b"packed".as_slice(), - PhpType::Resource(_) => b"resource".as_slice(), - PhpType::Iterable => unreachable!("iterable handled above via runtime heap-kind dispatch"), - PhpType::Mixed | PhpType::Union(_) => unreachable!("mixed handled above"), - PhpType::TaggedScalar => unreachable!("tagged scalar handled above via runtime tag dispatch"), - }; - emit_type_name_result(emitter, data, type_str) -} diff --git a/src/codegen/builtins/types/is_a.rs b/src/codegen/builtins/types/is_a.rs deleted file mode 100644 index d444cc7e9c..0000000000 --- a/src/codegen/builtins/types/is_a.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Purpose: -//! Emits folded `is_a()` and `is_subclass_of()` checks for literal targets. -//! Walks class parent/interface metadata using PHP-style case-insensitive names. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()` -//! -//! Key details: -//! - Both arguments are evaluated for side effects before the folded boolean is loaded. -//! - `is_subclass_of()` uses the same relation check as `is_a()` but excludes an exact self match. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::names::php_symbol_key; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{ClassInfo, PhpType}; - -/// Emits AOT `is_a()` and `is_subclass_of()` checks when the target class is a string literal. -/// The first argument is evaluated for its static type (used for the relation check); all -/// remaining arguments are evaluated purely for side effects. Returns a `PhpType::Bool` result -/// in the ABI integer register. `is_subclass_of()` differs by excluding an exact self match. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment(&format!("{}() — AOT static-type check", name)); - - // Eval first arg, capture static type, eval rest for side effects. - let arg_ty = emit_expr(&args[0], emitter, ctx, data); - for arg in args.iter().skip(1) { - emit_expr(arg, emitter, ctx, data); - } - - let exclude_self = name == "is_subclass_of"; - let result = static_relation_holds(&arg_ty, &args[1], ctx, exclude_self); - - abi::emit_load_int_immediate( - emitter, - abi::int_result_reg(emitter), - if result { 1 } else { 0 }, - ); - Some(PhpType::Bool) -} - -/// Statically evaluates whether `arg_ty` (an `PhpType::Object`) satisfies the class relation -/// described by `target_arg` (a string literal class name). -/// -/// Returns `true` if `arg_ty`'s class is the same as (or is a subclass/implements) `target_arg, -/// depending on the `exclude_self` flag. Walks the parent chain first, then the implemented -/// interface list. All comparisons are case-insensitive via `php_symbol_key`. -fn static_relation_holds( - arg_ty: &PhpType, - target_arg: &Expr, - ctx: &Context, - exclude_self: bool, -) -> bool { - let PhpType::Object(obj_class) = arg_ty else { - return false; - }; - let ExprKind::StringLiteral(target) = &target_arg.kind else { - return false; - }; - let obj_class = obj_class.trim_start_matches('\\'); - let target = target.trim_start_matches('\\'); - let target_key = php_symbol_key(target); - - if !exclude_self && php_symbol_key(obj_class) == target_key { - return true; - } - - // Walk the parent chain. - let mut current = obj_class.to_string(); - while let Some(info) = lookup_class(ctx, ¤t) { - if let Some(parent) = &info.parent { - let parent_clean = parent.trim_start_matches('\\'); - if php_symbol_key(parent_clean) == target_key { - return true; - } - current = parent_clean.to_string(); - } else { - break; - } - } - - // Walk implemented (and transitively-inherited) interfaces. - if let Some(info) = lookup_class(ctx, obj_class) { - for iface in &info.interfaces { - if php_symbol_key(iface.trim_start_matches('\\')) == target_key { - return true; - } - } - } - - false -} - -/// Looks up a class by name in `ctx.classes` using PHP-style case-insensitive lookup. -/// Tries an exact match first (with leading backslash stripped), then falls back to a -/// linear search via `php_symbol_key`. Returns the `ClassInfo` if found. -fn lookup_class<'a>(ctx: &'a Context, name: &str) -> Option<&'a ClassInfo> { - let clean = name.trim_start_matches('\\'); - if let Some(info) = ctx.classes.get(clean) { - return Some(info); - } - let key = php_symbol_key(clean); - ctx.classes - .iter() - .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == key) - .map(|(_, info)| info) -} diff --git a/src/codegen/builtins/types/is_bool.rs b/src/codegen/builtins/types/is_bool.rs deleted file mode 100644 index 946eba032a..0000000000 --- a/src/codegen/builtins/types/is_bool.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Purpose: -//! Emits PHP `is_bool` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `is_bool` type predicate. -/// -/// Evaluates the first argument's type at runtime or compile time: -/// - **Mixed/Union**: unboxes the runtime tag and compares it against `3` (boolean). -/// - **Concrete Bool**: returns `1`. -/// - **All other types**: returns `0`. -/// -/// Result is always returned as an integer (`x0` on ARM64, `rax` on x86_64). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_bool()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // normalize boxed mixed payloads to their concrete runtime tag - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #3"); // runtime tag 3 = boolean payload - emitter.instruction("cset x0, eq"); // x0 = 1 if the unboxed payload is a bool, 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 3"); // runtime tag 3 = boolean payload - emitter.instruction("sete al"); // set al when the unboxed payload is a bool - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } - } else { - let val = if matches!(ty, PhpType::Bool) { 1 } else { 0 }; - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val); // return the compile-time type predicate result - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/is_callable.rs b/src/codegen/builtins/types/is_callable.rs deleted file mode 100644 index fd64498779..0000000000 --- a/src/codegen/builtins/types/is_callable.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Purpose: -//! Emits codegen for `is_callable()`. -//! Handles compile-time callable shapes and delegates dynamic PHP callable forms to runtime helpers. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()` when lowering type/introspection builtins. -//! -//! Key details: -//! - Runtime fallback covers non-literal strings, callable arrays, invokable objects, Mixed, and erased iterables. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -use super::super::callable_lookup::lookup_function; - -/// Emits code for `is_callable(value): bool`. -/// -/// Static evaluation when the argument's compile-time type is `Callable` -/// (closures, first-class callables) or a string literal that resolves -/// to a known builtin or user function. Dynamic strings, callable arrays, -/// objects, and type-erased payloads route to runtime metadata lookup. -/// -/// # Arguments -/// - `args[0]`: the value to check -/// -/// # Returns -/// Always `PhpType::Bool` — the result is in `int_result_reg`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_callable()"); - - // Compile-time string literal: defer to the same lookup as - // function_exists() — known catalog builtin or user-declared - // function ⇒ true, else false. Evaluating the literal expression - // has no side effects, so we skip emit_expr. - if let ExprKind::StringLiteral(name) = &args[0].kind { - if !name.contains("::") { - let known = lookup_function(ctx, name).is_some(); - let val: i64 = if known { 1 } else { 0 }; - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val); - return Some(PhpType::Bool); - } - } - - let ty = emit_expr(&args[0], emitter, ctx, data); - match ty.codegen_repr() { - PhpType::Callable => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1); - } - PhpType::Str => emit_dynamic_string_lookup(emitter), - PhpType::Array(_) => { - emit_pointer_lookup(emitter, "__rt_is_callable_array"); // inspect indexed array shape for callable arrays - } - PhpType::AssocArray { .. } => { - emit_pointer_lookup(emitter, "__rt_is_callable_assoc"); // inspect hash shape for numeric 0/1 callable-array entries - } - PhpType::Object(_) => { - emit_pointer_lookup(emitter, "__rt_is_callable_object"); // check whether the object's runtime class exposes public __invoke - } - PhpType::Mixed | PhpType::Union(_) => { - emit_pointer_lookup(emitter, "__rt_is_callable_mixed"); // unwrap Mixed and dispatch to the dynamic callable checks - } - PhpType::Iterable => { - emit_pointer_lookup(emitter, "__rt_is_callable_heap"); // inspect erased iterable heap kind before choosing array/object fallback - } - _ => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - } - Some(PhpType::Bool) -} - -/// Emits code to call a runtime `__rt_is_callable_*` helper for non-literal types. -/// -/// Sets up the pointer argument in the correct ABI register for the target -/// and dispatches to the selected label. Used for arrays, objects, Mixed, -/// and erased iterables where compile-time resolution is not possible. -fn emit_pointer_lookup(emitter: &mut Emitter, label: &str) { - if emitter.target.arch == crate::codegen::platform::Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move pointer-shaped result into SysV helper argument 0 - } - abi::emit_call_label(emitter, label); // call the selected pointer-shaped runtime callable fallback -} - -/// Emits code to resolve a dynamic (non-literal) string as a callable name. -/// -/// Loads the string pointer and length from the expression result registers -/// into the correct ABI argument registers for the target architecture, then -/// calls `__rt_is_callable_string` to perform runtime lookup against builtin -/// and user function metadata. -fn emit_dynamic_string_lookup(emitter: &mut Emitter) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // move dynamic string pointer into runtime helper argument 0 - emitter.instruction("mov x1, x2"); // move dynamic string length into runtime helper argument 1 - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move dynamic string pointer into SysV helper argument 0 - emitter.instruction("mov rsi, rdx"); // move dynamic string length into SysV helper argument 1 - } - } - abi::emit_call_label(emitter, "__rt_is_callable_string"); // resolve dynamic function-name string against builtin and user metadata -} diff --git a/src/codegen/builtins/types/is_finite.rs b/src/codegen/builtins/types/is_finite.rs deleted file mode 100644 index 3a3fdc0926..0000000000 --- a/src/codegen/builtins/types/is_finite.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Purpose: -//! Emits PHP `is_finite` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `is_finite` PHP builtin call. -/// -/// Takes a single float argument (integers are normalized to float before the check). -/// Returns a PHP boolean indicating whether the value is finite (not NaN, not ±Inf). -/// -/// - ARM64: computes |value|, compares against +∞ constant, materializes result in `x0`. -/// - x86_64: checks NaN via self-comparison, then compares against +∞ and −∞ constants, -/// materializes result in `rax`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_finite()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // unbox a boxed Mixed payload to a double before the finite check (avoids treating the cell pointer as a value) - } else if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer inputs into the active floating-point result register before the finite check - } - match emitter.target.arch { - Arch::AArch64 => { - // -- check if |value| is strictly less than infinity (not NaN, not Inf) -- - emitter.instruction("fabs d0, d0"); // take the absolute value so both +INF and -INF compare against the same constant - let inf_label = data.add_float(f64::INFINITY); - abi::emit_symbol_address(emitter, "x9", &inf_label); // resolve the address of the infinity constant - emitter.instruction("ldr d1, [x9]"); // load the infinity constant into the comparison register - emitter.instruction("fcmp d0, d1"); // compare the absolute value against positive infinity - emitter.instruction("cset x0, mi"); // materialize the strict-less-than-infinity result as a boolean integer - } - Arch::X86_64 => { - let pos_inf_label = data.add_float(f64::INFINITY); - let neg_inf_label = data.add_float(f64::NEG_INFINITY); - let not_finite_label = ctx.next_label("is_finite_false"); - let done_label = ctx.next_label("is_finite_done"); - emitter.instruction("ucomisd xmm0, xmm0"); // compare the value against itself so NaN sets the parity flag - emitter.instruction(&format!("jp {}", not_finite_label)); // NaN is not finite - abi::emit_load_symbol_to_reg(emitter, "xmm1", &pos_inf_label, 0); // load the positive infinity constant into the comparison register - emitter.instruction("ucomisd xmm0, xmm1"); // compare the value against positive infinity - emitter.instruction(&format!("je {}", not_finite_label)); // +INF is not finite - abi::emit_load_symbol_to_reg(emitter, "xmm1", &neg_inf_label, 0); // load the negative infinity constant into the comparison register - emitter.instruction("ucomisd xmm0, xmm1"); // compare the value against negative infinity - emitter.instruction(&format!("je {}", not_finite_label)); // -INF is not finite - emitter.instruction("mov rax, 1"); // any remaining non-NaN and non-infinite value is finite - emitter.instruction(&format!("jmp {}", done_label)); // skip the false materialization path after confirming finiteness - emitter.label(¬_finite_label); - emitter.instruction("mov rax, 0"); // NaN and +/-INF are not finite - emitter.label(&done_label); - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/is_float.rs b/src/codegen/builtins/types/is_float.rs deleted file mode 100644 index 4b97416fbb..0000000000 --- a/src/codegen/builtins/types/is_float.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Purpose: -//! Emits PHP `is_float` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `is_float` type predicate call. -/// -/// For `PhpType::Mixed` or `PhpType::Union`, unpacks the boxed mixed payload via -/// `__rt_mixed_unbox` and tests the runtime tag (2 = float). For all other types, -/// returns the compile-time predicate result directly. -/// -/// Arguments: -/// args[0] — the expression to inspect -/// -/// Outputs: -/// - Result register: 1 if the value is a float at runtime, 0 otherwise -/// - Return type: `PhpType::Bool` -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_float()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // normalize boxed mixed payloads to their concrete runtime tag - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #2"); // runtime tag 2 = float payload - emitter.instruction("cset x0, eq"); // x0 = 1 if the unboxed payload is a float, 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 2"); // runtime tag 2 = float payload - emitter.instruction("sete al"); // set al when the unboxed payload is a float - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } - } else { - let val = if matches!(ty, PhpType::Float) { 1 } else { 0 }; - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val); // return the compile-time type predicate result - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/is_infinite.rs b/src/codegen/builtins/types/is_infinite.rs deleted file mode 100644 index 7f9b8bdf4e..0000000000 --- a/src/codegen/builtins/types/is_infinite.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Purpose: -//! Emits PHP `is_infinite` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP's `is_infinite()` builtin. -/// -/// Writes the result as a boolean integer in `x0`/`rax`. Non-float inputs are -/// normalized into the float register before the infinity check. The predicate -/// is true when the absolute value equals positive infinity (covers both `+INF` -/// and `-INF` on AArch64) or when the value equals either `+INF` or `-INF` on -/// x86_64. -/// -/// # Arguments -/// * `_name` — unused, follows the builtin emitter convention -/// * `args` — the expression to test for infinity; must have at least one element -/// * `emitter` — target-specific instruction emission -/// * `ctx` — variable layout, ownership state, class/FFI metadata -/// * `data` — runtime data section for floating-point constants -/// -/// # Returns -/// `Some(PhpType::Bool)` on success; never returns `None` for this builtin. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_infinite()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // unbox a boxed Mixed payload to a double before the infinity check (avoids treating the cell pointer as a value) - } else if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer inputs into the active floating-point result register before the infinity check - } - match emitter.target.arch { - Arch::AArch64 => { - // -- check if |value| equals infinity -- - emitter.instruction("fabs d0, d0"); // take the absolute value so both +INF and -INF compare against the same constant - let inf_label = data.add_float(f64::INFINITY); - abi::emit_symbol_address(emitter, "x9", &inf_label); // resolve the address of the infinity constant - emitter.instruction("ldr d1, [x9]"); // load the infinity constant into the comparison register - emitter.instruction("fcmp d0, d1"); // compare the absolute value against positive infinity - emitter.instruction("cset x0, eq"); // materialize the infinity comparison result as a boolean integer - } - Arch::X86_64 => { - let pos_inf_label = data.add_float(f64::INFINITY); - let neg_inf_label = data.add_float(f64::NEG_INFINITY); - let not_inf_label = ctx.next_label("is_infinite_false"); - let done_label = ctx.next_label("is_infinite_done"); - emitter.instruction("ucomisd xmm0, xmm0"); // compare the value against itself so NaN sets the parity flag - emitter.instruction(&format!("jp {}", not_inf_label)); // NaN is unordered against everything, so it is not infinite (ucomisd would otherwise set ZF and look equal) - abi::emit_load_symbol_to_reg(emitter, "xmm1", &pos_inf_label, 0); // load the positive infinity constant into the comparison register - emitter.instruction("ucomisd xmm0, xmm1"); // compare the value against positive infinity - emitter.instruction("sete al"); // remember whether the value equals positive infinity - abi::emit_load_symbol_to_reg(emitter, "xmm1", &neg_inf_label, 0); // load the negative infinity constant into the comparison register - emitter.instruction("ucomisd xmm0, xmm1"); // compare the value against negative infinity - emitter.instruction("sete cl"); // remember whether the value equals negative infinity - emitter.instruction("or al, cl"); // combine the +/- infinity comparisons into one boolean byte - emitter.instruction("movzx rax, al"); // widen the infinity boolean byte into the canonical integer result register - emitter.instruction(&format!("jmp {}", done_label)); // skip the NaN false path after a real infinity check - emitter.label(¬_inf_label); - emitter.instruction("mov rax, 0"); // NaN is not infinite - emitter.label(&done_label); - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/is_int.rs b/src/codegen/builtins/types/is_int.rs deleted file mode 100644 index 79ecef7944..0000000000 --- a/src/codegen/builtins/types/is_int.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Purpose: -//! Emits PHP `is_int` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `is_int` type predicate call. -/// -/// For `PhpType::Mixed` or `PhpType::Union`, unpacks the boxed mixed payload via -/// `__rt_mixed_unbox` and tests the runtime tag (0 = integer). For all other types, -/// returns the compile-time predicate result directly. -/// -/// Arguments: -/// args[0] — the expression to inspect -/// -/// Outputs: -/// - Result register: 1 if the value is an integer at runtime, 0 otherwise -/// - Return type: `PhpType::Bool` -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_int()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // normalize boxed mixed payloads to their concrete runtime tag - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // runtime tag 0 = integer payload - emitter.instruction("cset x0, eq"); // x0 = 1 if the unboxed payload is an int, 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // runtime tag 0 = integer payload - emitter.instruction("sete al"); // set al when the unboxed payload is an int - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } - } else { - let val = if matches!(ty, PhpType::Int) { 1 } else { 0 }; - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val); // return the compile-time type predicate result - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/is_iterable.rs b/src/codegen/builtins/types/is_iterable.rs deleted file mode 100644 index c813c2bac4..0000000000 --- a/src/codegen/builtins/types/is_iterable.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! Purpose: -//! Emits PHP `is_iterable` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `is_iterable` builtin call. -/// -/// dispatches based on the resolved type of `args[0]`: -/// - For `PhpType::Mixed` or `PhpType::Union`: unboxes the runtime value at runtime and -/// checks the payload tag (indexed array, assoc hash, or object implementing -/// Iterator/IteratorAggregate). Returns true or false via the `true_case`/`done` control flow. -/// - For `PhpType::Array`, `PhpType::AssocArray`, `PhpType::Iterable`, or a known object -/// implementing Iterator/IteratorAggregate: folds to a compile-time `1` or `0`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_iterable()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - // Mixed/Union values are boxed cells. Unwrap to the concrete runtime tag and - // report true for arrays and objects implementing Iterator/IteratorAggregate. - let true_case = ctx.next_label("builtin_is_iterable_true"); - let object_case = ctx.next_label("builtin_is_iterable_object"); - let done = ctx.next_label("builtin_is_iterable_done"); - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // resolve the boxed mixed payload tag for the iterable predicate - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #4"); // runtime tag 4 = indexed array - emitter.instruction(&format!("b.eq {}", true_case)); // indexed arrays satisfy is_iterable - emitter.instruction("cmp x0, #5"); // runtime tag 5 = associative hash - emitter.instruction(&format!("b.eq {}", true_case)); // hash tables satisfy is_iterable - emitter.instruction("cmp x0, #6"); // runtime tag 6 = object - emitter.instruction(&format!("b.eq {}", object_case)); // Traversable objects satisfy is_iterable - emitter.instruction("mov x0, #0"); // every other concrete payload reports false - emitter.instruction(&format!("b {}", done)); // skip the truthy assignment - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 4"); // runtime tag 4 = indexed array - emitter.instruction(&format!("je {}", true_case)); // indexed arrays satisfy is_iterable - emitter.instruction("cmp rax, 5"); // runtime tag 5 = associative hash - emitter.instruction(&format!("je {}", true_case)); // hash tables satisfy is_iterable - emitter.instruction("cmp rax, 6"); // runtime tag 6 = object - emitter.instruction(&format!("je {}", object_case)); // Traversable objects satisfy is_iterable - emitter.instruction("mov rax, 0"); // every other concrete payload reports false - emitter.instruction(&format!("jmp {}", done)); // skip the truthy assignment - } - } - - emitter.label(&object_case); - emit_runtime_object_iterable_check(emitter, ctx, &true_case, &done); - - emitter.label(&true_case); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // record the truthy is_iterable result on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov rax, 1"); // record the truthy is_iterable result on x86_64 - } - } - emitter.label(&done); - return Some(PhpType::Bool); - } - - let val = matches!( - ty, - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Iterable - ) || matches!(&ty, PhpType::Object(name) if object_type_implements_iterable(ctx, name)); - abi::emit_load_int_immediate( - emitter, - abi::int_result_reg(emitter), - if val { 1 } else { 0 }, - ); // record the compile-time is_iterable predicate result - Some(PhpType::Bool) -} - -/// Emits the runtime check for whether a boxed object payload implements Iterator or IteratorAggregate. -/// -/// Saves the object pointer from `x1`/`rdi` onto the stack, then tests it against both interface IDs -/// via `__rt_exception_matches`. Jumps to `true_case` on either match, otherwise falls through to -/// load `0` and jump to `done`. Preserves stack balance on both paths. -fn emit_runtime_object_iterable_check( - emitter: &mut Emitter, - ctx: &mut Context, - true_case: &str, - done: &str, -) { - let object_true = ctx.next_label("builtin_is_iterable_object_true"); - let Some(iterator_id) = ctx.interfaces.get("Iterator").map(|info| info.interface_id) else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_jump(emitter, done); // no Iterator metadata means object payloads cannot satisfy is_iterable - return; - }; - let Some(aggregate_id) = ctx - .interfaces - .get("IteratorAggregate") - .map(|info| info.interface_id) - else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_jump(emitter, done); // no IteratorAggregate metadata means object payloads cannot satisfy is_iterable - return; - }; - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x1, [sp, #-16]!"); // preserve the unboxed object pointer across interface checks - emit_saved_object_interface_check(iterator_id, &object_true, emitter); - emit_saved_object_interface_check(aggregate_id, &object_true, emitter); - emitter.instruction("add sp, sp, #16"); // discard the saved object pointer after failed interface checks - emitter.instruction("mov x0, #0"); // non-Traversable objects do not satisfy is_iterable - emitter.instruction(&format!("b {}", done)); // skip the truthy assignment - emitter.label(&object_true); - emitter.instruction("add sp, sp, #16"); // discard the saved object pointer before returning true - emitter.instruction(&format!("b {}", true_case)); // continue through the shared truthy result path - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rdi"); // preserve the unboxed object pointer across interface checks - emit_saved_object_interface_check(iterator_id, &object_true, emitter); - emit_saved_object_interface_check(aggregate_id, &object_true, emitter); - abi::emit_pop_reg(emitter, "r10"); // discard the saved object pointer after failed interface checks - emitter.instruction("xor eax, eax"); // non-Traversable objects do not satisfy is_iterable - emitter.instruction(&format!("jmp {}", done)); // skip the truthy assignment - emitter.label(&object_true); - abi::emit_pop_reg(emitter, "r10"); // discard the saved object pointer before returning true - emitter.instruction(&format!("jmp {}", true_case)); // continue through the shared truthy result path - } - } -} - -/// Emits a single interface-implements check for a previously saved object pointer. -/// -/// Reloads the saved object from the stack and calls `__rt_exception_matches` with the given -/// `interface_id`. On success (non-zero result), jumps to `true_case`. This function does not -/// modify the stack pointer; the caller manages push/pop around the two checks. -fn emit_saved_object_interface_check(interface_id: u64, true_case: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - 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"); // test whether the object implements this Traversable interface - emitter.instruction("cmp x0, #0"); // did the runtime interface matcher succeed? - emitter.instruction(&format!("b.ne {}", true_case)); // matching Iterator/IteratorAggregate means is_iterable is true - } - Arch::X86_64 => { - 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"); // test whether the object implements this Traversable interface - emitter.instruction("test rax, rax"); // did the runtime interface matcher succeed? - emitter.instruction(&format!("jne {}", true_case)); // matching Iterator/IteratorAggregate means is_iterable is true - } - } -} - -/// Statically checks whether a named class or interface implements Iterator or IteratorAggregate. -/// -/// For classes, checks the `interfaces` list directly. For interfaces, performs a DFS up the -/// parent hierarchy. Returns `false` if the type is unknown or implements neither interface. -fn object_type_implements_iterable(ctx: &Context, type_name: &str) -> bool { - if ctx.classes.contains_key(type_name) { - return ctx.classes.get(type_name).is_some_and(|class_info| { - class_info - .interfaces - .iter() - .any(|name| name == "Iterator" || name == "IteratorAggregate") - }); - } - if ctx.interfaces.contains_key(type_name) { - return interface_extends_interface(ctx, type_name, "Iterator") - || interface_extends_interface(ctx, type_name, "IteratorAggregate"); - } - false -} - -/// Returns `true` if `interface_name` is or transitively extends `ancestor_name`. -/// -/// Uses an iterative DFS with a visited set to avoid cycles. The `interface_name == ancestor_name` -/// check handles the direct-match case before the search loop. -fn interface_extends_interface(ctx: &Context, interface_name: &str, ancestor_name: &str) -> bool { - if interface_name == ancestor_name { - return true; - } - let mut stack = vec![interface_name.to_string()]; - let mut seen = std::collections::HashSet::new(); - while let Some(current_name) = stack.pop() { - if !seen.insert(current_name.clone()) { - continue; - } - let Some(interface_info) = ctx.interfaces.get(¤t_name) else { - continue; - }; - for parent_name in &interface_info.parents { - if parent_name == ancestor_name { - return true; - } - stack.push(parent_name.clone()); - } - } - false -} diff --git a/src/codegen/builtins/types/is_nan.rs b/src/codegen/builtins/types/is_nan.rs deleted file mode 100644 index 0a78a6da25..0000000000 --- a/src/codegen/builtins/types/is_nan.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Purpose: -//! Emits PHP `is_nan` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits a PHP `is_nan()` type predicate call. -/// -/// Compares the first argument against itself using an unordered floating-point -/// comparison. NaN is the only value that does not equal itself, so the comparison -/// result directly indicates whether the value is NaN. -/// -/// # Arguments -/// * `_name` — builtin name (unused, dispatch is by caller) -/// * `args` — argument expressions; `args[0]` is the value to test -/// * `emitter` — target-specific assembly emitter -/// * `ctx` — codegen context (types, locals, etc.) -/// * `data` — data section for literals and jump tables -/// -/// # Returns -/// `Some(PhpType::Bool)` — the predicate result type -/// -/// # ABI / Runtime behavior -/// - Non-float inputs are first normalized into the float register via `emit_int_result_to_float_result`. -/// - AArch64: `fcmp d0, d0` sets the unordered flag for NaN; `cset x0, vs` materializes the bool. -/// - x86_64: `ucomisd xmm0, xmm0` sets the parity flag for NaN; `setp al` / `movzx` materializes the bool. -/// - Result is returned in the canonical integer register (`x0` on AArch64, `rax` on x86_64). -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_nan()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - // -- NaN is the only value that does not equal itself -- - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // unbox a boxed Mixed payload to a double before the NaN check (avoids treating the cell pointer as a value) - } else if ty != PhpType::Float { - abi::emit_int_result_to_float_result(emitter); // normalize integer inputs into the active floating-point result register before the NaN check - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fcmp d0, d0"); // compare the floating-point value against itself so NaN sets the unordered flag - emitter.instruction("cset x0, vs"); // materialize the unordered NaN comparison result as a boolean integer - } - Arch::X86_64 => { - emitter.instruction("ucomisd xmm0, xmm0"); // compare the floating-point value against itself so NaN sets the parity flag - emitter.instruction("setp al"); // materialize the unordered NaN comparison result into the low boolean byte - emitter.instruction("movzx rax, al"); // widen the NaN boolean byte into the canonical integer result register - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/is_null.rs b/src/codegen/builtins/types/is_null.rs deleted file mode 100644 index d683badb49..0000000000 --- a/src/codegen/builtins/types/is_null.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Purpose: -//! Emits PHP `is_null` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `is_null($value)` builtin call. -/// -/// Inspects the runtime value of `args[0]` and sets the integer result register -/// to 1 (true) if the value is null, or 0 (false) otherwise. -/// -/// For `Mixed` or `Union` types, peels nested mixed wrappers via `__rt_mixed_unbox` -/// before testing the null sentinel (runtime tag 8). For scalar types, directly -/// compares against the null sentinel (all-bits-set except LSB). -/// -/// # Arguments -/// - `args[0]`: the expression to check for null -/// - `emitter`: assembly emitter -/// - `ctx`: codegen context (variable layout, ownership state) -/// - `data`: data section for relocations -/// -/// # Returns -/// `Some(PhpType::Bool)` since the result is always a PHP boolean. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_null()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - // Mixed/Union values are boxed cells — peel nested mixed wrappers first - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // normalize boxed mixed payloads to their concrete runtime tag - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #8"); // runtime tag 8 = null - emitter.instruction("cset x0, eq"); // x0 = 1 if the unboxed payload is null, 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 8"); // runtime tag 8 = null - emitter.instruction("sete al"); // set al when the unboxed payload is null - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } - } else if matches!(ty, PhpType::TaggedScalar) { - // Tagged scalars carry a runtime tag word — null means tag 8 - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x1, #8"); // runtime tag 8 means the tagged scalar is PHP null - emitter.instruction("cset x0, eq"); // x0 = 1 if the tagged scalar is null, 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rdx, 8"); // runtime tag 8 means the tagged scalar is PHP null - emitter.instruction("sete al"); // set al when the tagged scalar is null - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } - } else if matches!(ty, PhpType::Int) && crate::codegen::sentinels::null_repr_is_tagged() { - // Under the tagged representation a plain Int can never hold null - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } else { - // Scalar types — check directly against the null sentinel - match emitter.target.arch { - Arch::AArch64 => { - let sentinel = NULL_SENTINEL as u64; - emitter.instruction(&format!("movz x9, #0x{:X}", sentinel & 0xFFFF)); // load null sentinel bits [15:0] - emitter.instruction(&format!("movk x9, #0x{:X}, lsl #16", (sentinel >> 16) & 0xFFFF)); // load null sentinel bits [31:16] - emitter.instruction(&format!("movk x9, #0x{:X}, lsl #32", (sentinel >> 32) & 0xFFFF)); // load null sentinel bits [47:32] - emitter.instruction(&format!("movk x9, #0x{:X}, lsl #48", (sentinel >> 48) & 0xFFFF)); // load null sentinel bits [63:48] - emitter.instruction("cmp x0, x9"); // compare value against null sentinel - emitter.instruction("cset x0, eq"); // x0 = 1 if value is null, 0 otherwise - } - Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "r10", NULL_SENTINEL); - emitter.instruction("cmp rax, r10"); // compare value against the runtime null sentinel - emitter.instruction("sete al"); // set al when the value is null - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } - } - - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/is_numeric.rs b/src/codegen/builtins/types/is_numeric.rs deleted file mode 100644 index 28504b336f..0000000000 --- a/src/codegen/builtins/types/is_numeric.rs +++ /dev/null @@ -1,244 +0,0 @@ -//! Purpose: -//! Emits PHP `is_numeric` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `is_numeric()` for the given expression. -/// -/// Dispatches on the known static type of `args[0]`: -/// - `Int` / `Float`: returns `true` immediately. -/// - `Str`: scans the string for an optional leading `-`, then digits, then an -/// optional `.` followed by more digits (at least one digit required). -/// - `Mixed` / `Union`: unboxes the runtime tag and tests it — int/float are numeric, -/// a string is run through the same scan, everything else is not numeric. -/// - All other static types: returns `false`. -/// -/// # Returns -/// Always `Some(PhpType::Bool)`. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_numeric()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - - match ty { - PhpType::Int | PhpType::Float => { - // -- int and float are always numeric -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // return true for int/float types - } - Arch::X86_64 => { - emitter.instruction("mov rax, 1"); // return true for int/float types - } - } - } - PhpType::Str => { - // -- scan the string operand directly (ptr/len already in the string regs) -- - emit_numeric_string_scan(emitter, ctx); - } - PhpType::Mixed | PhpType::Union(_) => { - // -- a boxed Mixed payload: unbox and dispatch on the runtime tag -- - let pass_label = ctx.next_label("isnum_mx_pass"); - let scan_label = ctx.next_label("isnum_mx_scan"); - let fail_label = ctx.next_label("isnum_mx_fail"); - let end_label = ctx.next_label("isnum_mx_end"); - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // x0/rax = tag, x1/rdi = value_lo, x2/rdx = value_hi - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // runtime tag 0 = integer - emitter.instruction(&format!("b.eq {}", pass_label)); // integers are numeric - emitter.instruction("cmp x0, #2"); // runtime tag 2 = float - emitter.instruction(&format!("b.eq {}", pass_label)); // floats are numeric - emitter.instruction("cmp x0, #1"); // runtime tag 1 = string - emitter.instruction(&format!("b.eq {}", scan_label)); // strings need the numeric-string scan - emitter.instruction(&format!("b {}", fail_label)); // every other payload is not numeric - emitter.label(&pass_label); - emitter.instruction("mov x0, #1"); // return true for int/float payloads - emitter.instruction(&format!("b {}", end_label)); // skip the scan and failure paths - emitter.label(&scan_label); - // mixed_unbox already left the string pointer in x1 and length in x2 - emit_numeric_string_scan(emitter, ctx); // result in x0 - emitter.instruction(&format!("b {}", end_label)); // skip the failure path after scanning - emitter.label(&fail_label); - emitter.instruction("mov x0, #0"); // return false for non-numeric payloads - emitter.label(&end_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // runtime tag 0 = integer - emitter.instruction(&format!("je {}", pass_label)); // integers are numeric - emitter.instruction("cmp rax, 2"); // runtime tag 2 = float - emitter.instruction(&format!("je {}", pass_label)); // floats are numeric - emitter.instruction("cmp rax, 1"); // runtime tag 1 = string - emitter.instruction(&format!("je {}", scan_label)); // strings need the numeric-string scan - emitter.instruction(&format!("jmp {}", fail_label)); // every other payload is not numeric - emitter.label(&pass_label); - emitter.instruction("mov rax, 1"); // return true for int/float payloads - emitter.instruction(&format!("jmp {}", end_label)); // skip the scan and failure paths - emitter.label(&scan_label); - emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the scan's pointer register (length already in rdx) - emit_numeric_string_scan(emitter, ctx); // result in rax - emitter.instruction(&format!("jmp {}", end_label)); // skip the failure path after scanning - emitter.label(&fail_label); - emitter.instruction("mov rax, 0"); // return false for non-numeric payloads - emitter.label(&end_label); - } - } - } - _ => { - // -- all other types are not numeric -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // return false for non-numeric types - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // return false for non-numeric types - } - } - } - } - - Some(PhpType::Bool) -} - -/// Emits the numeric-string scan: optional leading `-`, digits, an optional `.` with more -/// digits, requiring at least one digit. Expects the string pointer/length in the canonical -/// string registers (ARM64: `x1`/`x2`; x86_64: `rax`/`rdx`) and returns 1/0 in `x0`/`rax`. -/// Shared by the `Str` path and the Mixed string-tag path. -fn emit_numeric_string_scan(emitter: &mut Emitter, ctx: &mut Context) { - let loop_label = ctx.next_label("isnum_loop"); - let dot_label = ctx.next_label("isnum_dot"); - let frac_loop = ctx.next_label("isnum_frac"); - let fail_label = ctx.next_label("isnum_fail"); - let pass_label = ctx.next_label("isnum_pass"); - let end_label = ctx.next_label("isnum_end"); - - match emitter.target.arch { - Arch::AArch64 => { - // -- return false for empty string -- - emitter.instruction(&format!("cbz x2, {}", fail_label)); // empty string is not numeric - emitter.instruction("mov x3, #0"); // x3 = loop index - emitter.instruction("mov x5, #0"); // x5 = digit count - - // -- check for optional leading minus sign -- - emitter.instruction("ldrb w4, [x1]"); // load first byte - emitter.instruction("cmp w4, #45"); // check if '-' - emitter.instruction(&format!("b.ne {}", loop_label)); // not minus, start digit loop - emitter.instruction("add x3, x3, #1"); // skip the minus sign - emitter.instruction("cmp x3, x2"); // check if string is just "-" - emitter.instruction(&format!("b.ge {}", fail_label)); // just "-" is not numeric - - // -- scan integer part: digits before optional dot -- - emitter.label(&loop_label); - emitter.instruction("cmp x3, x2"); // check if index reached length - emitter.instruction(&format!("b.ge {}", pass_label)); // end of string, check if we had digits - emitter.instruction("ldrb w4, [x1, x3]"); // load byte at index - emitter.instruction("cmp w4, #46"); // check if '.' - emitter.instruction(&format!("b.eq {}", dot_label)); // found dot, switch to fractional part - emitter.instruction("sub w6, w4, #48"); // w6 = byte - '0' - emitter.instruction("cmp w6, #9"); // check if in range 0-9 - emitter.instruction(&format!("b.hi {}", fail_label)); // not a digit, fail - emitter.instruction("add x5, x5, #1"); // increment digit count - emitter.instruction("add x3, x3, #1"); // increment index - emitter.instruction(&format!("b {}", loop_label)); // continue loop - - // -- found a dot, scan fractional digits -- - emitter.label(&dot_label); - emitter.instruction("add x3, x3, #1"); // skip the dot - emitter.label(&frac_loop); - emitter.instruction("cmp x3, x2"); // check if index reached length - emitter.instruction(&format!("b.ge {}", pass_label)); // end of string after dot - emitter.instruction("ldrb w4, [x1, x3]"); // load byte at index - emitter.instruction("sub w6, w4, #48"); // w6 = byte - '0' - emitter.instruction("cmp w6, #9"); // check if in range 0-9 - emitter.instruction(&format!("b.hi {}", fail_label)); // not a digit after dot, fail - emitter.instruction("add x5, x5, #1"); // increment digit count - emitter.instruction("add x3, x3, #1"); // increment index - emitter.instruction(&format!("b {}", frac_loop)); // continue fractional loop - - // -- must have at least one digit to be numeric -- - emitter.label(&pass_label); - emitter.instruction("cmp x5, #0"); // check if we found any digits - emitter.instruction(&format!("b.eq {}", fail_label)); // no digits found, not numeric - emitter.instruction("mov x0, #1"); // return true - emitter.instruction(&format!("b {}", end_label)); // jump to end - - emitter.label(&fail_label); - emitter.instruction("mov x0, #0"); // return false - - emitter.label(&end_label); - } - Arch::X86_64 => { - // -- return false for empty string -- - emitter.instruction("test rdx, rdx"); // empty string is not numeric - emitter.instruction(&format!("je {}", fail_label)); // branch to failure when the string length is zero - emitter.instruction("mov rcx, 0"); // rcx = loop index - emitter.instruction("mov r8, 0"); // r8 = digit count - - // -- check for optional leading minus sign -- - emitter.instruction("movzx r9d, BYTE PTR [rax]"); // load the first byte of the string - emitter.instruction("cmp r9d, 45"); // check whether the string starts with '-' - emitter.instruction(&format!("jne {}", loop_label)); // skip the sign handling when the first byte is not '-' - emitter.instruction("add rcx, 1"); // skip the minus sign - emitter.instruction("cmp rcx, rdx"); // check if the string was just "-" - emitter.instruction(&format!("jae {}", fail_label)); // just "-" is not numeric - - // -- scan integer part: digits before optional dot -- - emitter.label(&loop_label); - emitter.instruction("cmp rcx, rdx"); // check if the scan index reached the string length - emitter.instruction(&format!("jae {}", pass_label)); // end of string, check whether we saw any digits - emitter.instruction("movzx r9d, BYTE PTR [rax + rcx]"); // load the current byte - emitter.instruction("cmp r9d, 46"); // check whether the current byte is '.' - emitter.instruction(&format!("je {}", dot_label)); // switch to fractional scanning when a dot is found - emitter.instruction("sub r9d, 48"); // normalize the byte into a candidate digit value - emitter.instruction("cmp r9d, 9"); // check whether the normalized digit is in the range 0-9 - emitter.instruction(&format!("ja {}", fail_label)); // any other byte makes the string non-numeric - emitter.instruction("add r8, 1"); // record that we consumed one more digit - emitter.instruction("add rcx, 1"); // advance to the next byte - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning the integer part - - // -- found a dot, scan fractional digits -- - emitter.label(&dot_label); - emitter.instruction("add rcx, 1"); // skip the dot itself - emitter.label(&frac_loop); - emitter.instruction("cmp rcx, rdx"); // check if the fractional scan reached the end of the string - emitter.instruction(&format!("jae {}", pass_label)); // end of string after the dot still needs at least one digit overall - emitter.instruction("movzx r9d, BYTE PTR [rax + rcx]"); // load the current fractional byte - emitter.instruction("sub r9d, 48"); // normalize the byte into a candidate digit value - emitter.instruction("cmp r9d, 9"); // check whether the normalized digit is in the range 0-9 - emitter.instruction(&format!("ja {}", fail_label)); // any non-digit after the dot makes the string non-numeric - emitter.instruction("add r8, 1"); // record that we consumed one more digit - emitter.instruction("add rcx, 1"); // advance to the next byte - emitter.instruction(&format!("jmp {}", frac_loop)); // continue scanning the fractional part - - // -- must have at least one digit to be numeric -- - emitter.label(&pass_label); - emitter.instruction("test r8, r8"); // check whether any digits were consumed in either scan phase - emitter.instruction(&format!("je {}", fail_label)); // reject strings like "." or "-." - emitter.instruction("mov rax, 1"); // return true for a numeric-looking string - emitter.instruction(&format!("jmp {}", end_label)); // skip the failure path after choosing the true result - - emitter.label(&fail_label); - emitter.instruction("mov rax, 0"); // return false for a non-numeric string - - emitter.label(&end_label); - } - } -} diff --git a/src/codegen/builtins/types/is_resource.rs b/src/codegen/builtins/types/is_resource.rs deleted file mode 100644 index 17e5627b0a..0000000000 --- a/src/codegen/builtins/types/is_resource.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Purpose: -//! Emits PHP `is_resource` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - A boxed Mixed payload carries runtime tag 9 for resources; statically-typed -//! `Resource` values fold the predicate to a compile-time constant. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits codegen for PHP `is_resource()` resource/type builtin calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_resource()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // normalize boxed mixed payloads to their concrete runtime tag - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #9"); // runtime tag 9 = resource payload - emitter.instruction("cset x0, eq"); // x0 = 1 if the unboxed payload is a resource, 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 9"); // runtime tag 9 = resource payload - emitter.instruction("sete al"); // set al when the unboxed payload is a resource - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } - } else { - let val = if matches!(ty, PhpType::Resource(_)) { 1 } else { 0 }; - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), val); // return the compile-time type predicate result - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/is_string.rs b/src/codegen/builtins/types/is_string.rs deleted file mode 100644 index 8cbbb17f52..0000000000 --- a/src/codegen/builtins/types/is_string.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Purpose: -//! Emits PHP `is_string` type predicate calls. -//! Inspects static or boxed runtime value representation and returns a PHP boolean. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Predicate behavior must match PHP sentinel, Mixed tag, and object/interface layout conventions. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `is_string` type predicate as a runtime check. -/// -/// For `PhpType::Mixed` or `PhpType::Union` arguments, unboxes the value via -/// `__rt_mixed_unbox` and compares the resulting runtime tag against the -/// string-payload sentinel (tag value 1). For types known at compile time, -/// returns a constant 1 (string) or 0 (not string). -/// -/// Returns `Some(PhpType::Bool)` unconditionally. -/// -/// Arguments: -/// - `args[0]`: the expression to test -/// -/// Input type `ty`: -/// - `Mixed` / `Union`: runtime unbox + tag comparison -/// - `Str`: constant 1 -/// - all other types: constant 0 -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("is_string()"); - let ty = emit_expr(&args[0], emitter, ctx, data); - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - // Mixed/Union values are boxed cells — peel nested mixed wrappers - // and compare the runtime tag against the string-payload tag. - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // normalize boxed mixed payloads to their concrete runtime tag - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #1"); // runtime tag 1 = string payload - emitter.instruction("cset x0, eq"); // x0 = 1 if the unboxed payload is a string, 0 otherwise - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 1"); // runtime tag 1 = string payload - emitter.instruction("sete al"); // set al when the unboxed payload is a string - emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register - } - } - } else { - // Compile-time type fully determines the answer for non-mixed types. - let val = if matches!(ty, PhpType::Str) { 1 } else { 0 }; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", val)); // set result: 1 if string, 0 otherwise - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", val)); // set result: 1 if string, 0 otherwise - } - } - } - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/mod.rs b/src/codegen/builtins/types/mod.rs deleted file mode 100644 index d0108cb5a7..0000000000 --- a/src/codegen/builtins/types/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Purpose: -//! Dispatches type predicate, conversion, and variable-state PHP builtins to their focused codegen emitters. -//! Keeps the public builtin category surface small while leaf files own lowering details. -//! -//! Called from: -//! - `crate::codegen::builtins::emit_builtin_call()`. -//! -//! Key details: -//! - Dispatcher names must stay aligned with the builtin catalog and signature normalization layer. - -mod boolval; -mod class_alias; -mod class_exists; -mod class_relations; -mod empty; -mod floatval; -mod get_class; -mod get_declared; -mod get_resource_id; -mod get_resource_type; -mod gettype; -mod is_a; -mod is_bool; -mod is_callable; -mod is_finite; -mod is_float; -mod is_infinite; -mod is_int; -mod is_iterable; -mod is_nan; -mod is_null; -mod is_numeric; -mod is_resource; -mod is_string; -mod settype; -mod unset; - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Dispatches a typePredicate/conversion/variableState builtin call to its leaf emitter. -/// -/// Looks up `name` in the builtin dispatch table and delegates to the corresponding -/// leaf emitter. Returns `None` if `name` is not a recognized type builtin, allowing -/// callers to try other dispatch paths. -/// -/// # Arguments -/// - `name` - lowercase builtin name (e.g. `"is_bool"`, `"floatval"`); -/// - `args` - call arguments as AST expressions; -/// - `emitter` - code emitter accumulating assembly; -/// - `ctx` - codegen context (variable layout, class metadata, target); -/// - `data` - data section for read-only constants and runtime symbols. -/// -/// # Returns -/// `Some(PhpType)` with the result type if the builtin is handled here, `None` if unknown. -pub fn emit( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - match name { - "is_bool" => is_bool::emit(name, args, emitter, ctx, data), - "is_callable" => is_callable::emit(name, args, emitter, ctx, data), - "boolval" => boolval::emit(name, args, emitter, ctx, data), - "is_null" => is_null::emit(name, args, emitter, ctx, data), - "floatval" => floatval::emit(name, args, emitter, ctx, data), - "is_float" => is_float::emit(name, args, emitter, ctx, data), - "is_int" => is_int::emit(name, args, emitter, ctx, data), - "is_iterable" => is_iterable::emit(name, args, emitter, ctx, data), - "is_string" => is_string::emit(name, args, emitter, ctx, data), - "is_numeric" => is_numeric::emit(name, args, emitter, ctx, data), - "is_nan" => is_nan::emit(name, args, emitter, ctx, data), - "is_infinite" => is_infinite::emit(name, args, emitter, ctx, data), - "is_finite" => is_finite::emit(name, args, emitter, ctx, data), - "gettype" => gettype::emit(name, args, emitter, ctx, data), - "is_resource" => is_resource::emit(name, args, emitter, ctx, data), - "get_resource_type" => get_resource_type::emit(name, args, emitter, ctx, data), - "get_resource_id" => get_resource_id::emit(name, args, emitter, ctx, data), - "empty" => empty::emit(name, args, emitter, ctx, data), - "unset" => unset::emit(name, args, emitter, ctx, data), - "settype" => settype::emit(name, args, emitter, ctx, data), - "class_alias" => class_alias::emit(name, args, emitter, ctx, data), - "class_exists" | "interface_exists" | "trait_exists" | "enum_exists" => { - class_exists::emit(name, args, emitter, ctx, data) - } - "class_implements" | "class_parents" | "class_uses" => { - class_relations::emit(name, args, emitter, ctx, data) - } - "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { - get_declared::emit(name, args, emitter, ctx, data) - } - "get_class" | "get_parent_class" => get_class::emit(name, args, emitter, ctx, data), - "is_a" | "is_subclass_of" => is_a::emit(name, args, emitter, ctx, data), - _ => None, - } -} diff --git a/src/codegen/builtins/types/settype.rs b/src/codegen/builtins/types/settype.rs deleted file mode 100644 index e64bd9a288..0000000000 --- a/src/codegen/builtins/types/settype.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Purpose: -//! Emits PHP `settype` type conversion or type-name builtin calls. -//! Applies PHP scalar conversion rules or materializes runtime type names for values. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Conversion results must stay aligned with type-checker signatures and boxed Mixed handling. - -use crate::codegen::abi; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits code for PHP's `settype($var, $type)` builtin. -/// -/// Converts the variable named in `args[0]` to the type specified by the string literal in -/// `args[1]`. Supports `"int"`/`"integer"`, `"float"`/`"double"`, `"string"`, and `"bool"`/`"boolean"`. -/// Updates the variable's type in the context and always returns `true` (bool). -/// -/// # Arguments -/// - `args[0]` must be a `Variable` expression naming the target variable. -/// - `args[1]` must be a `StringLiteral` giving the target type name. -/// - `emitter` drives assembly emission with target-aware ABI helpers. -/// - `ctx` provides variable layout (stack offset) and is updated with the new type. -/// - `_data` is used for string coercion runtime calls. -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - _data: &mut DataSection, -) -> Option { - emitter.comment("settype()"); - if let crate::parser::ast::ExprKind::Variable(vname) = &args[0].kind { - if let crate::parser::ast::ExprKind::StringLiteral(type_name) = &args[1].kind { - let var = ctx.variables.get(vname).expect("undefined variable"); - let offset = var.stack_offset; - let old_ty = var.ty.clone(); - crate::codegen::abi::emit_load(emitter, &old_ty, offset); - let new_ty = match type_name.as_str() { - "int" | "integer" => { - // -- convert value to integer -- - match &old_ty { - PhpType::Float => { - abi::emit_float_result_to_int_result(emitter); // truncate the floating-point source value into the active integer result register for the current target ABI - } - PhpType::Bool | PhpType::Int => {} - _ => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); // coerce unsupported settype(..., \"integer\") sources to zero in the active integer result register - } - } - PhpType::Int - } - "float" | "double" => { - // -- convert value to float -- - match &old_ty { - PhpType::Float => {} - _ => { - abi::emit_int_result_to_float_result(emitter); // convert the scalar settype(..., \"float\") source into the active floating-point result register - } - } - PhpType::Float - } - "string" => { - crate::codegen::expr::coerce_to_string(emitter, ctx, _data, &old_ty); - PhpType::Str - } - "bool" | "boolean" => { - // -- convert value to boolean -- - crate::codegen::expr::coerce_null_to_zero(emitter, &old_ty); - match emitter.target.arch { - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // compare the coerced scalar source against zero before normalizing it into a boolean on x86_64 - emitter.instruction("setne al"); // set the low byte when the coerced scalar source is truthy on x86_64 - emitter.instruction("movzx eax, al"); // widen the normalized boolean result back into the full x86_64 integer result register - } - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // compare the coerced scalar source against zero before normalizing it into a boolean on AArch64 - emitter.instruction("cset x0, ne"); // set the integer result register to 1 when the coerced scalar source is truthy on AArch64 - } - } - PhpType::Bool - } - _ => old_ty.clone(), - }; - crate::codegen::abi::emit_store(emitter, &new_ty, offset); - ctx.update_var_type_and_ownership( - vname, - new_ty.clone(), - HeapOwnership::local_owner_for_type(&new_ty), - ); - } - } - // -- settype() always returns true -- - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1); // return true in the active target integer result register because settype() reports success - Some(PhpType::Bool) -} diff --git a/src/codegen/builtins/types/unset.rs b/src/codegen/builtins/types/unset.rs deleted file mode 100644 index 697d1ef769..0000000000 --- a/src/codegen/builtins/types/unset.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Purpose: -//! Emits PHP `unset` calls that clear variables or array elements. -//! Coordinates ownership cleanup with caller storage updates for removed values. -//! -//! Called from: -//! - `crate::codegen::builtins::types::emit()`. -//! -//! Key details: -//! - Unset is mutating and must release owned refcounted values without touching unrelated aliases. - -use crate::codegen::abi; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits PHP `unset($var)` and `unset($arr[$key])` calls. -/// -/// For each argument, dispatches to array-element unset if the argument is an -/// array-access expression; otherwise treats it as a variable unset. Releases -/// any owned heap-backed value before writing the null sentinel into the -/// variable slot. -/// -/// Arguments: -/// - `_name`: unused, matches the builtin dispatcher signature -/// - `args`: one or more expressions to unset -/// -/// Output: -/// - Always returns `Some(PhpType::Void)` to satisfy the builtin caller -pub fn emit( - _name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("unset()"); - for arg in args { - emit_unset_arg(arg, emitter, ctx, data); - } - Some(PhpType::Void) -} - -/// Emits the runtime cleanup and null-sentinel write for a single `unset` argument. -/// -/// For array-access targets, delegates to `emit_array_access_offset_unset`. For -/// simple variables, loads the old heap pointer, calls the appropriate refcount -/// helper (`__rt_heap_free_safe`, `__rt_decref_array`, `__rt_decref_hash`, or -/// `__rt_decref_object`), then writes the null sentinel (0x7FFFFFFFFFFFFFFFE) -/// into the variable slot and marks it `Void`/non-heap. -/// -/// Arguments: -/// - `arg`: the expression to unset (must be `ArrayAccess` or `Variable`) -/// - `emitter`: target assembly emitter -/// - `ctx`: current codegen context (provides variable layout and type info) -/// - `data`: mutable data section for relocations -/// -/// ABI/side effects: -/// - Clobbers `int_result_reg` for loading the old heap pointer and materializing -/// the null sentinel -/// - Updates `ctx` variable entry to `PhpType::Void` with `HeapOwnership::NonHeap` -fn emit_unset_arg( - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if let crate::parser::ast::ExprKind::ArrayAccess { array, index } = &arg.kind { - if crate::codegen::expr::arrays::type_is_array_access_object( - &crate::codegen::functions::infer_contextual_type(array, ctx), - ctx, - ) { - crate::codegen::expr::arrays::emit_array_access_offset_unset( - array, index, emitter, ctx, data, - ); - return; - } - } - - if let crate::parser::ast::ExprKind::Variable(name) = &arg.kind { - let var = ctx.variables.get(name).expect("undefined variable"); - let offset = var.stack_offset; - let old_ty = var.ty.clone(); - - // -- free old heap value before unsetting -- - if matches!(&old_ty, PhpType::Str) { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), offset); // load the previous heap pointer from the variable slot - abi::emit_call_label(emitter, "__rt_heap_free_safe"); // free old string storage when the previous value is heap-backed - } else if matches!(&old_ty, PhpType::Array(_)) { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), offset); // load the previous heap pointer from the variable slot - abi::emit_call_label(emitter, "__rt_decref_array"); // decrement the array refcount and deep-free when it reaches zero - } else if matches!(&old_ty, PhpType::AssocArray { .. }) { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), offset); // load the previous heap pointer from the variable slot - abi::emit_call_label(emitter, "__rt_decref_hash"); // decrement the hash refcount and deep-free when it reaches zero - } else if matches!(&old_ty, PhpType::Object(_)) { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), offset); // load the previous heap pointer from the variable slot - abi::emit_call_label(emitter, "__rt_decref_object"); // decrement the object refcount and deep-free when it reaches zero - } - - // -- set variable to null sentinel value (0x7FFFFFFFFFFFFFFFE) -- - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), NULL_SENTINEL); // materialize the shared null sentinel in the target integer result register - abi::store_at_offset(emitter, abi::int_result_reg(emitter), offset); // store the null sentinel back into the variable slot - ctx.update_var_type_and_ownership(name, PhpType::Void, HeapOwnership::NonHeap); - } -} diff --git a/src/codegen/callable_dispatch.rs b/src/codegen/callable_dispatch.rs deleted file mode 100644 index 6709b9ff6c..0000000000 --- a/src/codegen/callable_dispatch.rs +++ /dev/null @@ -1,1001 +0,0 @@ -//! Purpose: -//! Defines runtime callable dispatch metadata shared by indirect callback emitters. -//! Bridges AOT function signatures with runtime-selected callable values or names. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::call_user_func_array` -//! -//! Key details: -//! - Cases carry the ABI entry label, optional PHP-visible name, signature metadata, and hidden captures. -//! - String-name dispatch compares against userland callable names before loading the matched descriptor. - -use crate::codegen::abi; -use crate::codegen::callable_descriptor::{ - self, CallableDescriptorInvocation, CallableDescriptorShape, -}; -use crate::codegen::context::{Context, DeferredClosure, DeferredRuntimeCallableInvoker}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::names::{function_symbol, php_symbol_key, Name}; -use crate::parser::ast::{Expr, ExprKind, StaticReceiver, Stmt, StmtKind, Visibility}; -use crate::span::Span; -use crate::types::{ - callable_wrapper_sig, first_class_callable_builtin_sig, ExternFunctionSig, FunctionSig, PhpType, -}; -use crate::types::checker::builtins::supported_builtin_function_names; - -const RUNTIME_RECEIVER_PARAM: &str = "__elephc_callable_receiver"; - -#[derive(Clone)] -pub(crate) struct RuntimeCallableCase { - pub(crate) label: String, - pub(crate) descriptor_label: String, - pub(crate) php_name: Option, - pub(crate) sig: FunctionSig, - pub(crate) captures: Vec<(String, PhpType, bool)>, - pub(crate) has_invoker: bool, - pub(crate) invoker_label: Option, -} - -pub(crate) enum RuntimeCallableSelector<'a> { - Address(&'a str), - StringNameStack { - ptr_offset: usize, - len_offset: usize, - call_reg: &'a str, - }, -} - -#[derive(Clone, Copy)] -pub(crate) enum RuntimeInstanceCallableShape { - ObjectInvoke, - InstanceMethod, -} - -#[derive(Clone)] -pub(crate) struct RuntimeInstanceMethodCallableCase { - pub(crate) class_id: u64, - pub(crate) method_name: String, - pub(crate) case: RuntimeCallableCase, -} - -#[derive(Clone)] -pub(crate) struct RuntimeStaticMethodCallableCase { - pub(crate) class_name: String, - pub(crate) method_name: String, - pub(crate) case: RuntimeCallableCase, -} - -/// Provides the Runtime callable cases helper used by the callable dispatch module. -pub(crate) fn runtime_callable_cases( - ctx: &mut Context, - data: &mut DataSection, - captures: &[(String, PhpType, bool)], - source_arg_ty: Option<&PhpType>, -) -> Vec { - let mut cases = Vec::new(); - let source_elem_ty = source_arg_ty.map(runtime_case_source_elem_ty); - if captures.is_empty() { - for (name, sig) in runtime_extern_wrappers(ctx) { - let case_sig = callable_wrapper_sig(&sig); - let label = ensure_runtime_extern_wrapper(ctx, &name, &case_sig); - let invoker_label = ensure_runtime_descriptor_invoker(ctx, captures, &case_sig); - let descriptor_label = runtime_case_descriptor( - data, - &label, - Some(&name), - callable_descriptor::CALLABLE_DESC_KIND_EXTERN, - &case_sig, - &[], - &[], - CallableDescriptorInvocation::named(CallableDescriptorShape::Extern, &name), - invoker_label.as_deref(), - ); - cases.push(RuntimeCallableCase { - label, - descriptor_label, - php_name: Some(name), - sig: case_sig, - captures: Vec::new(), - has_invoker: invoker_label.is_some(), - invoker_label, - }); - } - for name in supported_builtin_function_names() { - if runtime_builtin_wrapper_excluded(name) || runtime_extern_named(ctx, name) { - continue; - } - let Some(sig) = first_class_callable_builtin_sig(name) else { - continue; - }; - let case_sig = callable_wrapper_sig(&sig); - let label = ensure_runtime_builtin_wrapper(ctx, name, &case_sig); - let invoker_label = ensure_runtime_descriptor_invoker(ctx, captures, &case_sig); - let descriptor_label = runtime_case_descriptor( - data, - &label, - Some(name), - callable_descriptor::CALLABLE_DESC_KIND_BUILTIN, - &case_sig, - &[], - &[], - CallableDescriptorInvocation::named(CallableDescriptorShape::Builtin, *name), - invoker_label.as_deref(), - ); - cases.push(RuntimeCallableCase { - label, - descriptor_label, - php_name: Some((*name).to_string()), - sig: case_sig, - captures: Vec::new(), - has_invoker: invoker_label.is_some(), - invoker_label, - }); - } - for (class_name, method_name, sig) in runtime_static_method_wrappers(ctx) { - let case_sig = static_method_runtime_wrapper_sig(&sig); - let label = - ensure_runtime_static_method_wrapper(ctx, &class_name, &method_name, &case_sig); - let php_name = format!("{}::{}", class_name, method_name); - let invoker_label = ensure_runtime_descriptor_invoker(ctx, captures, &case_sig); - let descriptor_label = runtime_case_descriptor( - data, - &label, - Some(&php_name), - callable_descriptor::CALLABLE_DESC_KIND_STATIC_METHOD, - &case_sig, - &[], - &[], - CallableDescriptorInvocation::method( - CallableDescriptorShape::StaticMethod, - Some(class_name.clone()), - method_name.as_str(), - ), - invoker_label.as_deref(), - ); - cases.push(RuntimeCallableCase { - label, - descriptor_label, - php_name: Some(php_name), - sig: case_sig, - captures: Vec::new(), - has_invoker: invoker_label.is_some(), - invoker_label, - }); - } - } - let user_functions: Vec<(String, FunctionSig)> = ctx - .functions - .iter() - .filter(|(name, _)| !ctx.extern_functions.contains_key(*name)) - .map(|(name, sig)| (name.clone(), sig.clone())) - .collect(); - for (name, sig) in user_functions { - let case_sig = callable_wrapper_sig(&sig); - let invoker_label = ensure_runtime_descriptor_invoker(ctx, captures, &case_sig); - let descriptor_label = runtime_case_descriptor( - data, - &function_symbol(&name), - Some(&name), - callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, - &case_sig, - &[], - &[], - CallableDescriptorInvocation::named(CallableDescriptorShape::Function, &name), - invoker_label.as_deref(), - ); - cases.push(RuntimeCallableCase { - label: function_symbol(&name), - descriptor_label, - php_name: Some(name), - sig: case_sig, - captures: Vec::new(), - has_invoker: invoker_label.is_some(), - invoker_label, - }); - } - let mut deferred_closure_cases = Vec::new(); - for deferred in &mut ctx.deferred_closures { - if !captures.is_empty() && deferred.hidden_params.as_slice() != captures { - continue; - } - let sig = specialized_runtime_case_sig(&deferred.sig, source_elem_ty.as_ref()); - deferred.sig = sig.clone(); - deferred_closure_cases.push(( - deferred.label.clone(), - sig, - deferred.captures.clone(), - deferred.hidden_params.clone(), - )); - } - for (label, sig, closure_captures, hidden_params) in deferred_closure_cases { - let invoker_label = ensure_runtime_descriptor_invoker(ctx, &hidden_params, &sig); - let descriptor_label = runtime_case_descriptor( - data, - &label, - None, - callable_descriptor::CALLABLE_DESC_KIND_CLOSURE, - &sig, - &closure_captures, - &hidden_params, - CallableDescriptorInvocation::new(CallableDescriptorShape::Closure), - invoker_label.as_deref(), - ); - cases.push(RuntimeCallableCase { - label, - descriptor_label, - php_name: None, - sig, - captures: hidden_params, - has_invoker: invoker_label.is_some(), - invoker_label, - }); - } - cases.sort_by(|left, right| left.label.cmp(&right.label)); - cases.dedup_by(|left, right| left.label == right.label); - cases -} - -/// Emits a runtime-callable case descriptor and returns its data label. -fn runtime_case_descriptor( - data: &mut DataSection, - label: &str, - php_name: Option<&str>, - kind: u64, - sig: &FunctionSig, - captures: &[(String, PhpType, bool)], - hidden_params: &[(String, PhpType, bool)], - invocation: CallableDescriptorInvocation, - invoker_label: Option<&str>, -) -> String { - callable_descriptor::static_descriptor_with_optional_invoker_meta( - data, - label, - php_name, - kind, - Some(sig), - captures, - hidden_params, - invocation, - invoker_label, - ) -} - -/// Returns the element/value type visible to dynamic argument specialization. -fn runtime_case_source_elem_ty(source_arg_ty: &PhpType) -> PhpType { - match source_arg_ty { - PhpType::Array(elem_ty) => *elem_ty.clone(), - PhpType::AssocArray { value, .. } => *value.clone(), - other => other.clone(), - } -} - -/// Ensures a descriptor-compatible runtime invoker exists for the callable signature. -pub(crate) fn ensure_runtime_descriptor_invoker( - ctx: &mut Context, - captures: &[(String, PhpType, bool)], - sig: &FunctionSig, -) -> Option { - if let Some(existing) = ctx - .deferred_runtime_callable_invokers - .iter() - .find(|invoker| invoker.sig == *sig && invoker.captures == captures) - { - return Some(existing.label.clone()); - } - let label = ctx.next_label("callable_invoker"); - ctx.deferred_runtime_callable_invokers - .push(DeferredRuntimeCallableInvoker { - label: label.clone(), - sig: sig.clone(), - captures: captures.to_vec(), - }); - Some(label) -} - -/// Provides runtime extern wrapper metadata in deterministic declaration-name order. -fn runtime_extern_wrappers(ctx: &Context) -> Vec<(String, FunctionSig)> { - let mut wrappers: Vec<(String, FunctionSig)> = ctx - .extern_functions - .iter() - .map(|(name, extern_sig)| { - let sig = ctx - .functions - .get(name) - .cloned() - .unwrap_or_else(|| function_sig_from_extern(extern_sig)); - (name.clone(), sig) - }) - .collect(); - wrappers.sort_by(|left, right| left.0.cmp(&right.0)); - wrappers -} - -/// Converts extern metadata to the PHP-facing wrapper signature used by descriptor dispatch. -fn function_sig_from_extern(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 whether an extern declaration shadows a builtin callback name. -fn runtime_extern_named(ctx: &Context, name: &str) -> bool { - let name_key = php_symbol_key(name); - ctx.extern_functions - .keys() - .any(|extern_name| php_symbol_key(extern_name) == name_key) -} - -/// Provides the Runtime static method wrappers helper used by the callable dispatch module. -fn runtime_static_method_wrappers(ctx: &Context) -> Vec<(String, String, FunctionSig)> { - let mut wrappers = Vec::new(); - for (class_name, class_info) in &ctx.classes { - // Synthetic builtin classes (e.g. DateTime::createFromFormat) are emitted on demand, so - // their static-method symbols may not exist in a program that never uses the class. Keep - // them out of the dynamic-callable descriptor to avoid referencing an unemitted symbol, - // mirroring how they are excluded from dynamic `new $x()`. - if crate::codegen::expr::objects::known_dynamic_new_builtin_class_names() - .contains(&class_name.as_str()) - { - continue; - } - 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; - } - wrappers.push((class_name.clone(), method_name.clone(), sig.clone())); - } - } - wrappers.sort_by(|left, right| (&left.0, &left.1).cmp(&(&right.0, &right.1))); - wrappers -} - -/// Builds descriptor cases for every public instance method visible to runtime callable arrays. -pub(crate) fn runtime_public_instance_method_cases( - ctx: &mut Context, - data: &mut DataSection, -) -> Vec { - let mut methods = Vec::new(); - for (class_name, class_info) in &ctx.classes { - for method_name in class_info.methods.keys() { - if !class_info - .method_visibilities - .get(method_name) - .is_some_and(|visibility| matches!(visibility, Visibility::Public)) - { - continue; - } - methods.push((class_name.clone(), class_info.class_id, method_name.clone())); - } - } - methods.sort_by(|left, right| (&left.0, &left.2).cmp(&(&right.0, &right.2))); - - let mut cases = Vec::new(); - for (class_name, class_id, method_name) in methods { - if let Some(case) = runtime_instance_method_case( - ctx, - data, - &class_name, - &method_name, - RuntimeInstanceCallableShape::InstanceMethod, - ) { - cases.push(RuntimeInstanceMethodCallableCase { - class_id, - method_name, - case, - }); - } - } - cases -} - -/// Builds descriptor cases for every public static method visible to runtime callable arrays. -pub(crate) fn runtime_public_static_method_cases( - ctx: &mut Context, - data: &mut DataSection, -) -> Vec { - let wrappers = runtime_static_method_wrappers(ctx); - let mut cases = Vec::new(); - for (class_name, method_name, _) in wrappers { - if let Some(case) = runtime_static_method_case(ctx, data, &class_name, &method_name) { - cases.push(RuntimeStaticMethodCallableCase { - class_name, - method_name, - case, - }); - } - } - cases -} - -/// Builds the runtime descriptor case for one public static method callable. -pub(crate) fn runtime_static_method_case( - ctx: &mut Context, - data: &mut DataSection, - class_name: &str, - method_name: &str, -) -> Option { - let (resolved_method_name, sig) = { - let class_info = ctx.classes.get(class_name)?; - let method_key = php_symbol_key(method_name); - let (resolved_method_name, sig) = class_info - .static_methods - .iter() - .find(|(candidate, _)| php_symbol_key(candidate) == method_key)?; - if !class_info - .static_method_visibilities - .get(resolved_method_name) - .is_some_and(|visibility| matches!(visibility, Visibility::Public)) - { - return None; - } - (resolved_method_name.clone(), sig.clone()) - }; - - let case_sig = static_method_runtime_wrapper_sig(&sig); - let label = ensure_runtime_static_method_wrapper( - ctx, - class_name, - &resolved_method_name, - &case_sig, - ); - let php_name = format!("{}::{}", class_name, resolved_method_name); - let invoker_label = ensure_runtime_descriptor_invoker(ctx, &[], &case_sig); - let descriptor_label = runtime_case_descriptor( - data, - &label, - Some(&php_name), - callable_descriptor::CALLABLE_DESC_KIND_STATIC_METHOD, - &case_sig, - &[], - &[], - CallableDescriptorInvocation::method( - CallableDescriptorShape::StaticMethod, - Some(class_name.to_string()), - resolved_method_name.as_str(), - ), - invoker_label.as_deref(), - ); - - Some(RuntimeCallableCase { - label, - descriptor_label, - php_name: Some(php_name), - sig: case_sig, - captures: Vec::new(), - has_invoker: invoker_label.is_some(), - invoker_label, - }) -} - -/// Builds the runtime descriptor case for one public instance-method or `__invoke` callable. -pub(crate) fn runtime_instance_method_case( - ctx: &mut Context, - data: &mut DataSection, - class_name: &str, - method_name: &str, - shape: RuntimeInstanceCallableShape, -) -> Option { - let (resolved_method_name, sig) = { - let class_info = ctx.classes.get(class_name)?; - let method_key = php_symbol_key(method_name); - let (resolved_method_name, sig) = class_info - .methods - .iter() - .find(|(candidate, _)| php_symbol_key(candidate) == method_key)?; - if !class_info - .method_visibilities - .get(resolved_method_name) - .is_some_and(|visibility| matches!(visibility, Visibility::Public)) - { - return None; - } - (resolved_method_name.clone(), sig.clone()) - }; - - let case_sig = instance_method_runtime_wrapper_sig(class_name, &sig); - let label = - ensure_runtime_instance_method_wrapper(ctx, class_name, &resolved_method_name, &case_sig); - let php_name = format!("{}::{}", class_name, resolved_method_name); - let invoker_label = ensure_runtime_descriptor_invoker(ctx, &[], &case_sig); - let (kind, invocation_shape) = match shape { - RuntimeInstanceCallableShape::ObjectInvoke => ( - callable_descriptor::CALLABLE_DESC_KIND_OBJECT_INVOKE, - CallableDescriptorShape::ObjectInvoke, - ), - RuntimeInstanceCallableShape::InstanceMethod => ( - callable_descriptor::CALLABLE_DESC_KIND_INSTANCE_METHOD, - CallableDescriptorShape::InstanceMethod, - ), - }; - let descriptor_label = runtime_case_descriptor( - data, - &label, - Some(&php_name), - kind, - &case_sig, - &[], - &[], - CallableDescriptorInvocation::method( - invocation_shape, - Some(class_name.to_string()), - resolved_method_name.as_str(), - ), - invoker_label.as_deref(), - ); - - Some(RuntimeCallableCase { - label, - descriptor_label, - php_name: Some(php_name), - sig: case_sig, - captures: Vec::new(), - has_invoker: invoker_label.is_some(), - invoker_label, - }) -} - -/// Provides the Runtime builtin wrapper excluded helper used by the callable dispatch module. -/// -/// `__elephc_mktime_raw` / `__elephc_gmmktime_raw` are internal escape hatches that the -/// `mktime`/`gmmktime` procedural-alias rewriter and synthetic DateTime bodies call directly. -/// They are lowered inline by the active EIR backend (`__rt_mktime` / `__rt_gmmktime`) and have no -/// standalone `fn_` symbol, but the deferred-closure wrapper body emitted here is lowered by the -/// frozen legacy direct backend, which does not know these names and would emit an unresolved -/// `bl _fn_` reference. They are never invoked dynamically, so excluding them from the -/// dynamic-call descriptor table is both safe and semantically correct. -fn runtime_builtin_wrapper_excluded(name: &str) -> bool { - matches!( - name, - "iterator_apply" | "preg_replace_callback" - | "__elephc_mktime_raw" | "__elephc_gmmktime_raw" - ) -} - -/// Ensures runtime builtin wrapper is available before the caller continues. -pub(crate) fn ensure_runtime_builtin_wrapper( - ctx: &mut Context, - name: &str, - sig: &FunctionSig, -) -> String { - if let Some(label) = ctx.runtime_callable_builtin_wrappers.get(name) { - return label.clone(); - } - - let label = ctx.next_label("callable_builtin"); - let params: Vec = sig.params.iter().map(|(name, _)| name.clone()).collect(); - ctx.deferred_closures.push(DeferredClosure { - label: label.clone(), - params, - body: builtin_wrapper_body(name, sig), - sig: sig.clone(), - captures: Vec::new(), - hidden_params: Vec::new(), - current_class: None, - needed: true, - }); - ctx.runtime_callable_builtin_wrappers - .insert(name.to_string(), label.clone()); - label -} - -/// Ensures a PHP-ABI extern wrapper is available before runtime descriptor dispatch uses it. -fn ensure_runtime_extern_wrapper( - ctx: &mut Context, - name: &str, - sig: &FunctionSig, -) -> String { - if let Some(label) = ctx.runtime_callable_extern_wrappers.get(name) { - return label.clone(); - } - - let label = ctx.next_label("callable_extern"); - let params: Vec = sig.params.iter().map(|(name, _)| name.clone()).collect(); - ctx.deferred_closures.push(DeferredClosure { - label: label.clone(), - params, - body: extern_wrapper_body(name, sig), - sig: sig.clone(), - captures: Vec::new(), - hidden_params: Vec::new(), - current_class: None, - needed: true, - }); - ctx.runtime_callable_extern_wrappers - .insert(name.to_string(), label.clone()); - label -} - -/// Ensures runtime static method wrapper is available before the caller continues. -pub(crate) fn ensure_runtime_static_method_wrapper( - ctx: &mut Context, - class_name: &str, - method_name: &str, - sig: &FunctionSig, -) -> String { - let key = format!("{}::{}", class_name, method_name); - if let Some(label) = ctx.runtime_callable_static_method_wrappers.get(&key) { - return label.clone(); - } - - let label = ctx.next_label("callable_static_method"); - let params: Vec = sig.params.iter().map(|(name, _)| name.clone()).collect(); - ctx.deferred_closures.push(DeferredClosure { - label: label.clone(), - params, - body: static_method_wrapper_body(class_name, method_name, sig), - sig: sig.clone(), - captures: Vec::new(), - hidden_params: Vec::new(), - current_class: None, - needed: true, - }); - ctx.runtime_callable_static_method_wrappers - .insert(key, label.clone()); - label -} - -/// Ensures runtime instance-method wrapper is available before descriptor dispatch uses it. -fn ensure_runtime_instance_method_wrapper( - ctx: &mut Context, - class_name: &str, - method_name: &str, - sig: &FunctionSig, -) -> String { - let key = format!("{}::{}", class_name, method_name); - if let Some(label) = ctx.runtime_callable_instance_method_wrappers.get(&key) { - return label.clone(); - } - - let label = ctx.next_label("callable_instance_method"); - let params: Vec = sig.params.iter().map(|(name, _)| name.clone()).collect(); - ctx.deferred_closures.push(DeferredClosure { - label: label.clone(), - params, - body: instance_method_wrapper_body(method_name, sig), - sig: sig.clone(), - captures: Vec::new(), - hidden_params: Vec::new(), - current_class: Some(class_name.to_string()), - needed: true, - }); - ctx.runtime_callable_instance_method_wrappers - .insert(key, label.clone()); - label -} - -/// Builds a static-method runtime wrapper signature that can receive keyed variadic tails. -pub(crate) fn static_method_runtime_wrapper_sig(sig: &FunctionSig) -> FunctionSig { - let mut wrapper_sig = callable_wrapper_sig(sig); - if wrapper_sig.variadic.is_some() { - if let Some((_, ty)) = wrapper_sig.params.last_mut() { - *ty = PhpType::Iterable; - } - } - wrapper_sig -} - -/// Builds an instance-method runtime wrapper signature with receiver in slot zero. -fn instance_method_runtime_wrapper_sig(class_name: &str, sig: &FunctionSig) -> FunctionSig { - let mut wrapper_sig = callable_wrapper_sig(sig); - wrapper_sig.params.insert( - 0, - ( - RUNTIME_RECEIVER_PARAM.to_string(), - PhpType::Object(class_name.to_string()), - ), - ); - wrapper_sig.defaults.insert(0, None); - wrapper_sig.ref_params.insert(0, false); - wrapper_sig.declared_params.insert(0, true); - if wrapper_sig.variadic.is_some() { - if let Some((_, ty)) = wrapper_sig.params.last_mut() { - *ty = PhpType::Iterable; - } - } - wrapper_sig -} - -/// Builds the synthetic method body for static method wrapper. -fn static_method_wrapper_body(class_name: &str, method_name: &str, sig: &FunctionSig) -> Vec { - let last_param_idx = sig.params.len().saturating_sub(1); - let args: Vec = sig - .params - .iter() - .enumerate() - .map(|(idx, (param_name, _))| { - let var = Expr::new(ExprKind::Variable(param_name.clone()), Span::dummy()); - if sig.variadic.is_some() && idx == last_param_idx { - Expr::new(ExprKind::Spread(Box::new(var)), Span::dummy()) - } else { - var - } - }) - .collect(); - let call = Expr::new( - ExprKind::StaticMethodCall { - receiver: StaticReceiver::Named(Name::from(class_name.to_string())), - method: method_name.to_string(), - args, - }, - Span::dummy(), - ); - - if sig.return_type == PhpType::Void { - vec![ - Stmt::new(StmtKind::ExprStmt(call), Span::dummy()), - Stmt::new(StmtKind::Return(None), Span::dummy()), - ] - } else { - vec![Stmt::new(StmtKind::Return(Some(call)), Span::dummy())] - } -} - -/// Builds the synthetic method body for an instance-method wrapper. -fn instance_method_wrapper_body(method_name: &str, sig: &FunctionSig) -> Vec { - let last_param_idx = sig.params.len().saturating_sub(1); - let args: Vec = sig - .params - .iter() - .enumerate() - .skip(1) - .map(|(idx, (param_name, _))| { - let var = Expr::new(ExprKind::Variable(param_name.clone()), Span::dummy()); - if sig.variadic.is_some() && idx == last_param_idx { - Expr::new(ExprKind::Spread(Box::new(var)), Span::dummy()) - } else { - var - } - }) - .collect(); - let receiver = Expr::new( - ExprKind::Variable(RUNTIME_RECEIVER_PARAM.to_string()), - Span::dummy(), - ); - let call = Expr::new( - ExprKind::MethodCall { - object: Box::new(receiver), - method: method_name.to_string(), - args, - }, - Span::dummy(), - ); - - if sig.return_type == PhpType::Void { - vec![ - Stmt::new(StmtKind::ExprStmt(call), Span::dummy()), - Stmt::new(StmtKind::Return(None), Span::dummy()), - ] - } else { - vec![Stmt::new(StmtKind::Return(Some(call)), Span::dummy())] - } -} - -/// Builds the synthetic function body for an extern wrapper. -fn extern_wrapper_body(name: &str, sig: &FunctionSig) -> Vec { - function_wrapper_body(name, sig) -} - -/// Builds the synthetic method body for builtin wrapper. -fn builtin_wrapper_body(name: &str, sig: &FunctionSig) -> Vec { - function_wrapper_body(name, sig) -} - -/// Builds the synthetic body that forwards visible wrapper parameters to a function call. -fn function_wrapper_body(name: &str, sig: &FunctionSig) -> Vec { - let last_param_idx = sig.params.len().saturating_sub(1); - let args: Vec = sig - .params - .iter() - .enumerate() - .map(|(idx, (param_name, _))| { - let var = Expr::new(ExprKind::Variable(param_name.clone()), Span::dummy()); - if sig.variadic.is_some() && idx == last_param_idx { - Expr::new(ExprKind::Spread(Box::new(var)), Span::dummy()) - } else { - var - } - }) - .collect(); - let call = Expr::new( - ExprKind::FunctionCall { - name: Name::unqualified(name), - args, - }, - Span::dummy(), - ); - - if sig.return_type == PhpType::Void { - vec![ - Stmt::new(StmtKind::ExprStmt(call), Span::dummy()), - Stmt::new(StmtKind::Return(None), Span::dummy()), - ] - } else { - vec![Stmt::new(StmtKind::Return(Some(call)), Span::dummy())] - } -} - -/// Emits assembly for branch if callable case mismatch. -pub(crate) fn emit_branch_if_callable_case_mismatch( - selector: &RuntimeCallableSelector<'_>, - case: &RuntimeCallableCase, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - match selector { - RuntimeCallableSelector::Address(call_reg) => { - emit_branch_if_address_mismatch(call_reg, &case.label, next_case, emitter); - } - RuntimeCallableSelector::StringNameStack { - ptr_offset, - len_offset, - call_reg, - } => { - emit_branch_if_string_name_mismatch( - case, - *ptr_offset, - *len_offset, - call_reg, - next_case, - emitter, - ctx, - data, - ); - } - } -} - -/// Computes the callable signature metadata for specialized runtime case. -fn specialized_runtime_case_sig( - sig: &FunctionSig, - source_elem_ty: Option<&PhpType>, -) -> FunctionSig { - let Some(source_elem_ty) = source_elem_ty else { - return sig.clone(); - }; - let mut sig = sig.clone(); - let source_ty = source_elem_ty.codegen_repr(); - if matches!(source_ty, PhpType::Void | PhpType::Never) { - return sig; - } - let visible_param_count = sig.params.len(); - let regular_param_count = if sig.variadic.is_some() { - visible_param_count.saturating_sub(1) - } else { - visible_param_count - }; - for i in 0..regular_param_count { - if sig.declared_params.get(i).copied().unwrap_or(false) - || sig.ref_params.get(i).copied().unwrap_or(false) - { - continue; - } - if let Some((_, param_ty)) = sig.params.get_mut(i) { - if !matches!(param_ty.codegen_repr(), PhpType::Int) { - continue; - } - *param_ty = source_ty.clone(); - } - } - if sig.variadic.is_some() { - let variadic_idx = visible_param_count.saturating_sub(1); - if !sig - .declared_params - .get(variadic_idx) - .copied() - .unwrap_or(false) - { - if let Some((_, param_ty)) = sig.params.get_mut(variadic_idx) { - *param_ty = PhpType::Array(Box::new(source_ty)); - } - } - } - sig -} - -/// Emits assembly for branch if address mismatch. -fn emit_branch_if_address_mismatch( - call_reg: &str, - candidate_label: &str, - next_case: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x9", candidate_label); - emitter.instruction(&format!("cmp {}, x9", call_reg)); // does the runtime callable entry match this AOT signature case? - emitter.instruction(&format!("b.ne {}", next_case)); // try the next callable signature case when the pointer differs - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "r10", candidate_label); - emitter.instruction(&format!("cmp {}, r10", call_reg)); // does the runtime callable entry match this AOT signature case? - emitter.instruction(&format!("jne {}", next_case)); // try the next callable signature case when the pointer differs - } - } -} - -/// Emits assembly for branch if string name mismatch. -#[allow(clippy::too_many_arguments)] -fn emit_branch_if_string_name_mismatch( - case: &RuntimeCallableCase, - ptr_offset: usize, - len_offset: usize, - call_reg: &str, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let Some(php_name) = case.php_name.as_ref() else { - abi::emit_jump(emitter, next_case); - return; - }; - - let matched_label = ctx.next_label("callable_string_match"); - let mut candidates = vec![php_name.clone()]; - if !php_name.starts_with('\\') { - candidates.push(format!("\\{}", php_name)); - } - - for candidate in candidates { - emit_string_name_compare( - ptr_offset, - len_offset, - candidate.as_bytes(), - &matched_label, - emitter, - data, - ); - } - abi::emit_jump(emitter, next_case); - - emitter.label(&matched_label); - abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); -} - -/// Emits assembly for string name compare. -fn emit_string_name_compare( - ptr_offset: usize, - len_offset: usize, - candidate: &[u8], - matched_label: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (candidate_label, candidate_len) = data.add_string(candidate); - match emitter.target.arch { - 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", &candidate_label); - abi::emit_load_int_immediate(emitter, "x4", candidate_len as i64); - abi::emit_call_label(emitter, "__rt_strcasecmp"); - emitter.instruction("cmp x0, #0"); // did the runtime string callback name match this userland target? - emitter.instruction(&format!("b.eq {}", matched_label)); // select this callable case when names match case-insensitively - } - 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", &candidate_label); - abi::emit_load_int_immediate(emitter, "rcx", candidate_len as i64); - abi::emit_call_label(emitter, "__rt_strcasecmp"); - emitter.instruction("test rax, rax"); // did the runtime string callback name match this userland target? - emitter.instruction(&format!("je {}", matched_label)); // select this callable case when names match case-insensitively - } - } -} diff --git a/src/codegen/callables.rs b/src/codegen/callables.rs deleted file mode 100644 index 23de518f37..0000000000 --- a/src/codegen/callables.rs +++ /dev/null @@ -1,288 +0,0 @@ -//! Purpose: -//! Shares callable metadata lookups used by indirect calls and callback builtins. -//! Centralizes capture and signature discovery so callable codegen paths stay aligned. -//! -//! Called from: -//! - `crate::codegen::expr::calls` -//! - `crate::codegen::builtins::arrays` -//! -//! Key details: -//! - Complex callable expressions can only expose captures when their runtime shape is statically direct. -//! - Branch-shaped callable signatures are reused only when every branch has the same call contract. - -use crate::codegen::context::Context; -use crate::names::php_symbol_key; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver}; -use crate::types::FunctionSig; - -use super::builtins::callable_lookup::{lookup_function, FunctionLookup}; - -/// Returns the capture list for a callable expression. -/// -/// For closures and first-class callables, returns the captures stored in the -/// deferred closure context. For `$var` FCC variables, returns the captures -/// registered for that variable name. Otherwise returns an empty vector. -/// -/// Each capture is a tuple of `(name, PhpType, is_mutable)` describing the -/// captured variable's name, PHP type, and whether it's mutated by the closure. -pub(crate) fn callable_captures( - callback: &Expr, - ctx: &mut Context, -) -> Vec<(String, crate::types::PhpType, bool)> { - match &callback.kind { - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) => ctx - .deferred_closures - .last() - .map(|closure| closure.captures.clone()) - .unwrap_or_default(), - ExprKind::Variable(name) => { - ctx.mark_fcc_used(name); - ctx.closure_captures.get(name).cloned().unwrap_or_default() - } - _ => Vec::new(), - } -} - -/// Returns the FunctionSig for a callable expression. -/// -/// Resolves string literals via `lookup_function` and checks user-defined functions -/// and include variants. Resolves `$var` variables from `ctx.closure_sigs`. Handles -/// first-class callables via `first_class_callable_sig`. For array-access expressions -/// where the array is a variable, resolves from `ctx.closure_sigs`. Delegates to -/// `matching_branch_sig` for ternary and null-coalescing branches that must share -/// the same signature. Returns `None` for expressions with no statically resolvable signature. -pub(crate) fn callable_sig(callback: &Expr, ctx: &Context) -> Option { - match &callback.kind { - ExprKind::StringLiteral(name) => match lookup_function(ctx, name) { - Some(FunctionLookup::UserFunction(name)) - | Some(FunctionLookup::IncludeVariant(name)) => ctx.functions.get(&name).cloned(), - _ => ctx.functions.get(name).cloned(), - }, - ExprKind::Variable(name) => ctx.closure_sigs.get(name).cloned(), - ExprKind::FirstClassCallable(target) => { - crate::codegen::expr::calls::first_class_callable_sig(target, ctx) - } - ExprKind::FunctionCall { name, .. } => { - let resolved_name = match lookup_function(ctx, name.as_str()) { - Some(FunctionLookup::UserFunction(name)) - | Some(FunctionLookup::IncludeVariant(name)) => name, - _ => name.as_str().to_string(), - }; - ctx.callable_return_sigs.get(&resolved_name).cloned() - } - ExprKind::MethodCall { object, method, .. } => { - method_return_callable_sig(object, method, ctx, false) - } - ExprKind::StaticMethodCall { - receiver, method, .. - } => static_method_return_callable_sig(receiver, method, ctx, false), - ExprKind::ArrayAccess { array, .. } => { - if let ExprKind::Variable(name) = &array.kind { - ctx.closure_sigs.get(name).cloned() - } else { - None - } - } - ExprKind::Assignment { value, .. } => callable_sig(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => matching_branch_sig(then_expr, else_expr, ctx), - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => matching_branch_sig(value, default, ctx), - _ => None, - } -} - -/// Returns the element callable signature for an expression that yields an array of callables. -/// -/// This is kept separate from `callable_sig()` because a function returning -/// `array` is not itself callable, but callers that later read an element -/// from the returned array still need the element descriptor signature. -pub(crate) fn callable_array_sig(callback_array: &Expr, ctx: &Context) -> Option { - match &callback_array.kind { - ExprKind::ArrayLiteral(elems) => matching_array_element_sig(elems.iter(), ctx), - ExprKind::ArrayLiteralAssoc(entries) => { - matching_array_element_sig(entries.iter().map(|(_, value)| value), ctx) - } - ExprKind::FunctionCall { name, .. } => { - let resolved_name = match lookup_function(ctx, name.as_str()) { - Some(FunctionLookup::UserFunction(name)) - | Some(FunctionLookup::IncludeVariant(name)) => name, - _ => name.as_str().to_string(), - }; - ctx.callable_array_return_sigs.get(&resolved_name).cloned() - } - ExprKind::MethodCall { object, method, .. } => { - method_return_callable_sig(object, method, ctx, true) - } - ExprKind::StaticMethodCall { - receiver, method, .. - } => static_method_return_callable_sig(receiver, method, ctx, true), - ExprKind::Variable(name) => ctx.closure_sigs.get(name).cloned(), - ExprKind::Assignment { value, .. } => callable_array_sig(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => matching_array_branch_sig(then_expr, else_expr, ctx), - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - matching_array_branch_sig(value, default, ctx) - } - _ => None, - } -} - -/// Returns a shared callable signature only when every array element has the same contract. -fn matching_array_element_sig<'a>( - values: impl Iterator, - ctx: &Context, -) -> Option { - let mut shared_sig: Option = None; - let mut saw_value = false; - for value in values { - saw_value = true; - let sig = callable_sig(value, ctx)?; - match &shared_sig { - Some(existing) if existing != &sig => return None, - Some(_) => {} - None => shared_sig = Some(sig), - } - } - if saw_value { - shared_sig - } else { - None - } -} - -/// Returns a callable-array signature only when both branches share one element contract. -fn matching_array_branch_sig(left: &Expr, right: &Expr, ctx: &Context) -> Option { - let left_sig = callable_array_sig(left, ctx)?; - let right_sig = callable_array_sig(right, ctx)?; - if left_sig == right_sig { - Some(left_sig) - } else { - None - } -} - -/// Resolves callable-return metadata for an instance method call expression. -fn method_return_callable_sig( - object: &Expr, - method: &str, - ctx: &Context, - array_return: bool, -) -> Option { - let object_ty = crate::codegen::functions::infer_contextual_type(object, ctx); - let class_name = crate::codegen::functions::singular_object_class(&object_ty)?.to_string(); - let method_key = php_symbol_key(method); - let impl_class = ctx - .classes - .get(&class_name) - .and_then(|class_info| class_info.method_impl_classes.get(&method_key)) - .cloned() - .unwrap_or(class_name); - stored_method_return_callable_sig(&impl_class, &method_key, ctx, array_return) -} - -/// Resolves callable-return metadata for a static method call expression. -fn static_method_return_callable_sig( - receiver: &StaticReceiver, - method: &str, - ctx: &Context, - array_return: bool, -) -> Option { - let class_name = resolve_static_method_metadata_class(receiver, ctx)?; - let method_key = php_symbol_key(method); - let class_info = ctx.classes.get(&class_name)?; - let impl_class = class_info - .static_method_impl_classes - .get(&method_key) - .or_else(|| class_info.method_impl_classes.get(&method_key)) - .cloned() - .unwrap_or(class_name); - stored_method_return_callable_sig(&impl_class, &method_key, ctx, array_return) -} - -/// Looks up stored callable-return metadata for a class method. -fn stored_method_return_callable_sig( - class_name: &str, - method_key: &str, - ctx: &Context, - array_return: bool, -) -> Option { - let class_info = ctx.classes.get(class_name)?; - if array_return { - class_info - .callable_array_method_return_sigs - .get(method_key) - .cloned() - } else { - class_info.callable_method_return_sigs.get(method_key).cloned() - } -} - -/// Resolves the concrete class whose static method metadata should be inspected. -fn resolve_static_method_metadata_class( - receiver: &StaticReceiver, - ctx: &Context, -) -> Option { - match receiver { - StaticReceiver::Named(name) => resolve_class_name(ctx, name.as_str()).map(str::to_string), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|current_class| ctx.classes.get(current_class)) - .and_then(|class_info| class_info.parent.clone()), - } -} - -/// Resolves a class name case-insensitively for metadata lookups. -fn resolve_class_name<'a>(ctx: &'a Context, class_name: &str) -> Option<&'a str> { - let class_key = php_symbol_key(class_name.trim_start_matches('\\')); - ctx.classes - .keys() - .find(|existing| php_symbol_key(existing) == class_key) - .map(String::as_str) -} - -/// Computes the callable signature metadata for direct first class function. -pub(crate) fn direct_first_class_function_sig( - callback: &Expr, - ctx: &Context, -) -> Option<(String, FunctionSig)> { - let target = match &callback.kind { - ExprKind::FirstClassCallable(target) => Some(target), - ExprKind::Variable(name) => ctx.first_class_callable_targets.get(name), - _ => None, - }?; - let CallableTarget::Function(name) = target else { - return None; - }; - let resolved_name = match lookup_function(ctx, name.as_str())? { - FunctionLookup::UserFunction(name) | FunctionLookup::IncludeVariant(name) => name, - FunctionLookup::Builtin(_) | FunctionLookup::Extern(_) => return None, - }; - let sig = ctx.functions.get(&resolved_name)?.clone(); - Some((resolved_name, sig)) -} - -/// Returns the common signature when both branches of a ternary or null-coalesce resolve to the same signature. -/// -/// Recursively resolves signatures for the left and right expressions using `callable_sig`. -/// Returns the shared signature only if both branches resolve to an identical `FunctionSig`; -/// otherwise returns `None`. Used to determine whether a branch-shaped callable can be -/// emitted with a single code path. -fn matching_branch_sig(left: &Expr, right: &Expr, ctx: &Context) -> Option { - let left_sig = callable_sig(left, ctx)?; - let right_sig = callable_sig(right, ctx)?; - if left_sig == right_sig { - Some(left_sig) - } else { - None - } -} diff --git a/src/codegen/class_methods.rs b/src/codegen/class_methods.rs deleted file mode 100644 index 8a4e0b416a..0000000000 --- a/src/codegen/class_methods.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! Purpose: -//! Builds and emits codegen signatures for class, interface, enum, and trait methods. -//! Handles receiver layout, static dispatch symbols, and method body emission. -//! -//! Called from: -//! - `crate::codegen::generate()` when class metadata contains methods -//! -//! Key details: -//! - Generated signatures must line up with object dispatch, vtables, and inherited method metadata. - -use std::collections::{HashMap, HashSet}; - -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::functions; -use crate::names::{method_symbol, php_symbol_key, static_method_symbol}; -use crate::parser::ast::ExprKind; -use crate::types::{ - ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, InterfaceInfo, - PackedClassInfo, PhpType, -}; - -/// Emits all non-abstract method bodies for a class, interface, enum, or trait. -/// -/// Skips abstract methods. For `ReflectionAttribute::newInstance`, synthesizes a -/// dispatch body that routes to attribute factories based on `this->__factory`. -#[allow(clippy::too_many_arguments)] -pub(super) fn emit_class_methods( - emitter: &mut Emitter, - data: &mut DataSection, - class_name: &str, - class_info: &ClassInfo, - functions: &HashMap, - callable_param_sigs: &HashMap<(String, String), FunctionSig>, - callable_return_sigs: &HashMap, - callable_array_return_sigs: &HashMap, - fiber_return_sigs: &HashMap, - function_variant_groups: &HashSet, - global_constants: &HashMap, - interfaces: &HashMap, - traits: &HashSet, - classes: &HashMap, - enums: &HashMap, - packed_classes: &HashMap, - extern_functions: &HashMap, - extern_classes: &HashMap, - extern_globals: &HashMap, -) { - for method in &class_info.method_decls { - let method_key = php_symbol_key(&method.name); - if method.is_abstract { - continue; - } - let (label, sig) = if method.is_static { - build_static_method_codegen_sig(class_name, class_info, &method_key, method) - } else { - build_instance_method_codegen_sig(class_name, class_info, &method_key, method) - }; - let epilogue_label = format!("{}_epilogue", label); - let generated_body = if class_name == "ReflectionAttribute" && method_key == "newinstance" { - Some(crate::codegen::reflection::build_attribute_new_instance_body(classes)) - } else if class_name == "ReflectionAttribute" && method_key == "getarguments" { - // Mirror the EIR backend: materialize captured attribute arguments - // through the normal array lowering so the two backends agree. - Some(crate::codegen::reflection::build_attribute_get_arguments_body(classes)) - } else { - None - }; - let body = generated_body.as_deref().unwrap_or(&method.body); - functions::emit_method( - emitter, - data, - &label, - &epilogue_label, - &sig, - body, - functions, - callable_param_sigs, - callable_return_sigs, - callable_array_return_sigs, - fiber_return_sigs, - function_variant_groups, - global_constants, - interfaces, - traits, - classes, - enums, - packed_classes, - class_name, - extern_functions, - extern_classes, - extern_globals, - ); - } -} - -/// Builds the symbol label and `FunctionSig` for a static method. -/// -/// The signature prepends a hidden `__elephc_called_class_id: Int` parameter for -/// static dispatch, then merges the declared parameters. Falls back to inferring -/// parameter types and defaults from the AST method declaration when no resolved -/// signature is available in `class_info.static_methods`. -fn build_static_method_codegen_sig( - class_name: &str, - class_info: &ClassInfo, - method_key: &str, - method: &crate::parser::ast::ClassMethod, -) -> (String, FunctionSig) { - let label = static_method_symbol(class_name, method_key); - let class_static_sig = class_info.static_methods.get(method_key); - let mut params: Vec<(String, PhpType)> = - vec![("__elephc_called_class_id".to_string(), PhpType::Int)]; - if let Some(sig) = class_static_sig { - params.extend(sig.params.clone()); - } else { - params.extend( - method - .params - .iter() - .map(|(n, _, _, _)| (n.clone(), PhpType::Int)), - ); - } - let mut defaults: Vec> = vec![None]; - if let Some(sig) = class_static_sig { - defaults.extend(sig.defaults.clone()); - } else { - defaults.extend(method.params.iter().map(|(_, _, d, _)| d.clone())); - if method.variadic.is_some() { - defaults.push(None); - } - } - let mut ref_params: Vec = vec![false]; - if let Some(sig) = class_static_sig { - ref_params.extend(sig.ref_params.clone()); - } else { - ref_params.extend(method.params.iter().map(|(_, _, _, r)| *r)); - if method.variadic.is_some() { - ref_params.push(false); - } - } - let mut declared_params: Vec = vec![false]; - if let Some(sig) = class_static_sig { - declared_params.extend(sig.declared_params.clone()); - } else { - declared_params.extend( - method - .params - .iter() - .map(|(_, type_ann, _, _)| type_ann.is_some()), - ); - if method.variadic.is_some() { - declared_params.push(false); - } - } - let return_type = class_static_sig - .map(|s| s.return_type.clone()) - .unwrap_or(PhpType::Int); - let declared_return = class_static_sig - .map(|s| s.declared_return) - .unwrap_or(method.return_type.is_some()); - ( - label, - FunctionSig { - params, - defaults, - return_type, - declared_return, - by_ref_return: method.by_ref_return, - ref_params, - declared_params, - variadic: method.variadic.clone(), - deprecation: None, - }, - ) -} - -/// Builds the symbol label and `FunctionSig` for an instance method. -/// -/// The signature prepends a `this: Object` receiver parameter, then -/// merges declared parameters. Falls back to inferring parameter types and defaults -/// from the AST method declaration when no resolved signature is available in -/// `class_info.methods`. -fn build_instance_method_codegen_sig( - class_name: &str, - class_info: &ClassInfo, - method_key: &str, - method: &crate::parser::ast::ClassMethod, -) -> (String, FunctionSig) { - let label = method_symbol(class_name, method_key); - let class_method_sig = class_info.methods.get(method_key); - let mut params: Vec<(String, PhpType)> = vec![ - ("this".to_string(), PhpType::Object(class_name.to_string())), - ]; - if let Some(sig) = class_method_sig { - params.extend(sig.params.clone()); - } else { - params.extend( - method - .params - .iter() - .map(|(n, _, _, _)| (n.clone(), PhpType::Int)), - ); - } - let mut defaults: Vec> = vec![None]; - if let Some(sig) = class_method_sig { - defaults.extend(sig.defaults.clone()); - } else { - defaults.extend(method.params.iter().map(|(_, _, d, _)| d.clone())); - if method.variadic.is_some() { - defaults.push(None); - } - } - let mut ref_params: Vec = vec![false]; - if let Some(sig) = class_method_sig { - ref_params.extend(sig.ref_params.clone()); - } else { - ref_params.extend(method.params.iter().map(|(_, _, _, r)| *r)); - if method.variadic.is_some() { - ref_params.push(false); - } - } - let mut declared_params: Vec = vec![false]; - if let Some(sig) = class_method_sig { - declared_params.extend(sig.declared_params.clone()); - } else { - declared_params.extend( - method - .params - .iter() - .map(|(_, type_ann, _, _)| type_ann.is_some()), - ); - if method.variadic.is_some() { - declared_params.push(false); - } - } - let return_type = class_method_sig - .map(|s| s.return_type.clone()) - .unwrap_or(PhpType::Int); - let declared_return = class_method_sig - .map(|s| s.declared_return) - .unwrap_or(method.return_type.is_some()); - ( - label, - FunctionSig { - params, - defaults, - return_type, - declared_return, - by_ref_return: method.by_ref_return, - ref_params, - declared_params, - variadic: method.variadic.clone(), - deprecation: None, - }, - ) -} diff --git a/src/codegen/context.rs b/src/codegen/context.rs index e369373e0f..a53b46dcac 100644 --- a/src/codegen/context.rs +++ b/src/codegen/context.rs @@ -1,718 +1,740 @@ //! Purpose: -//! Carries mutable codegen state such as local slots, labels, class metadata, and ownership facts. -//! Provides the shared bookkeeping used while lowering expressions, statements, functions, and wrappers. +//! Holds per-function state while the EIR backend lowers SSA instructions to assembly. +//! Provides table lookups, value-slot loads/stores, data-pool access, and label creation. //! //! Called from: -//! - `crate::codegen::generate()` and nested codegen emitters +//! - `crate::codegen::block_emit`, `crate::codegen::lower_inst`, and +//! `crate::codegen::lower_term`. //! //! Key details: -//! - Ownership states must remain conservative across branches, temporaries, and cleanup paths. +//! - Phase 04 stores every SSA value in a stack slot and reloads result registers at use sites. +//! - The context delegates target-specific movement to `crate::codegen::abi`. use std::collections::{HashMap, HashSet}; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use crate::parser::ast::{CallableTarget, ExprKind, Stmt}; -use crate::span::Span; -use crate::types::{ - ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, InterfaceInfo, - PackedClassInfo, PhpType, -}; - -/// Global counter for generating unique labels across all codegen contexts. -/// Uses sequential atomic increments to ensure label uniqueness. -static GLOBAL_LABEL_COUNTER: AtomicUsize = AtomicUsize::new(0); - -/// Size of the pre-allocated try handler slot (224 bytes). -pub(crate) const TRY_HANDLER_SLOT_SIZE: usize = 224; -/// Offset within the try handler slot for the diagnostic depth field (16 bytes from slot start). -pub(crate) const TRY_HANDLER_DIAG_DEPTH_OFFSET: usize = 16; -/// Offset within the try handler slot for the `jmp_buf` field (24 bytes from slot start). -pub(crate) const TRY_HANDLER_JMP_BUF_OFFSET: usize = 24; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// Heap ownership tracking. -pub enum HeapOwnership { - NonHeap, - Owned, - Borrowed, - MaybeOwned, + +use crate::codegen::{abi, emit_box_current_owned_value_as_mixed, emit_box_current_value_as_mixed}; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::ir::{BlockId, DataId, Function, LocalKind, LocalSlotId, Module, Op, Ownership, ValueDef, ValueId}; +use crate::ir_passes::Allocation; +use crate::types::PhpType; + +use super::frame::FrameLayout; +use super::value_placement::ValuePlacement; +use super::{CodegenIrError, Result}; + +/// Mutable backend state for one EIR function. +pub(crate) struct FunctionContext<'a> { + pub(super) module: &'a Module, + pub(super) function: &'a Function, + pub(super) emitter: &'a mut Emitter, + pub(super) data: &'a mut DataSection, + pub(super) placement: ValuePlacement, + pub(super) allocation: Allocation, + pub(super) callee_saved_offsets: Vec<(&'static str, usize)>, + local_offsets: HashMap, + promoted_ref_cells: HashSet, + try_handler_offsets: HashMap, + pub(super) frame_size: usize, + pub(super) concat_base_offset: usize, + pub(super) epilogue_emitted: bool, + pub(super) is_main: bool, + pub(super) web: bool, + pub(super) gc_stats: bool, + pub(super) heap_debug: bool, + pub(super) epilogue_label: Option, + label_counter: usize, } -impl HeapOwnership { - /// Returns the heap ownership for a given PHP type. - pub fn for_type(ty: &PhpType) -> Self { - if ty.is_refcounted() || matches!(ty, PhpType::Str | PhpType::Callable) { - HeapOwnership::MaybeOwned - } else { - HeapOwnership::NonHeap +impl<'a> FunctionContext<'a> { + /// Creates a lowering context with finalized frame and value-placement metadata. + pub(super) fn new( + module: &'a Module, + function: &'a Function, + emitter: &'a mut Emitter, + data: &'a mut DataSection, + layout: FrameLayout, + is_main: bool, + gc_stats: bool, + heap_debug: bool, + epilogue_label: Option, + ) -> Self { + Self { + module, + function, + emitter, + data, + placement: layout.value_placement, + allocation: layout.allocation, + callee_saved_offsets: layout.callee_saved_offsets, + local_offsets: layout.local_offsets, + promoted_ref_cells: HashSet::new(), + try_handler_offsets: layout.try_handler_offsets, + frame_size: layout.frame_size, + concat_base_offset: layout.concat_base_offset, + epilogue_emitted: false, + is_main, + web: false, + gc_stats, + heap_debug, + epilogue_label, + label_counter: 0, } } - /// Returns the local owner heap ownership for a given PHP type. - pub fn local_owner_for_type(ty: &PhpType) -> Self { - if ty.is_refcounted() || matches!(ty, PhpType::Str | PhpType::Callable) { - HeapOwnership::Owned - } else { - HeapOwnership::NonHeap - } + /// Returns a unique local label with a readable prefix. + pub(super) fn next_label(&mut self, prefix: &str) -> String { + let label = format!( + "_eir_{}_{}_{}", + label_fragment(&self.function.name), + label_fragment(prefix), + self.label_counter + ); + self.label_counter += 1; + label } - /// Returns the borrowed alias heap ownership for a given PHP type. - pub fn borrowed_alias_for_type(ty: &PhpType) -> Self { - if ty.is_refcounted() || matches!(ty, PhpType::Str | PhpType::Callable) { - HeapOwnership::Borrowed - } else { - HeapOwnership::NonHeap - } + /// Returns the assembly label for a non-entry EIR block. + pub(super) fn block_label(&self, block_name: &str, raw: u32) -> String { + format!("_eir_{}_{}_{}", label_fragment(&self.function.name), label_fragment(block_name), raw) } - /// Merges two ownership states conservatively. - pub fn merge(self, other: Self) -> Self { - use HeapOwnership::*; - match (self, other) { - (NonHeap, NonHeap) => NonHeap, - (Owned, Owned) => Owned, - (Borrowed, Borrowed) => Borrowed, - (MaybeOwned, _) | (_, MaybeOwned) => MaybeOwned, - (Owned, Borrowed) | (Borrowed, Owned) => MaybeOwned, - (NonHeap, x) | (x, NonHeap) => x, - } + /// Returns the assembly label for a block id. + pub(super) fn block_label_for_id(&self, block: BlockId) -> Result { + let block = self + .function + .block(block) + .ok_or_else(|| CodegenIrError::missing_entry("block", block.as_raw()))?; + Ok(self.block_label(&block.name, block.id.as_raw())) } -} - -/// A closure body to be emitted after the current function. -/// -/// Deferred closures capture variables from the enclosing scope and are stored -/// until the enclosing function's epilogue, at which point their wrapper and -/// body are emitted. The `needed` flag controls whether the full body or a -/// minimal `ret`-only stub is emitted. -#[allow(dead_code)] -pub struct DeferredClosure { - /// Unique label for the closure body. - pub label: String, - pub params: Vec, - pub body: Vec, - pub sig: FunctionSig, - pub captures: Vec<(String, PhpType, bool)>, - pub hidden_params: Vec<(String, PhpType, bool)>, - pub current_class: Option, - /// `true` when the wrapper body must be emitted because the runtime can - /// invoke it. Real closures default to `true` (the only way to call them is - /// via the wrapper). First-class-callable wrappers are downgraded to `false` - /// at the FCC variable assignment site and only flipped back to `true` if - /// the variable's value is read in a context other than the short-circuit - /// (see `emit_variable`). When `false`, the wrapper is replaced by a tiny - /// `ret`-only stub that keeps the symbol resolvable for the address load. - pub needed: bool, -} - -/// A Fiber entry wrapper emitted next to deferred closure bodies. -/// -/// Fiber wrappers adapt user functions for `Fiber::call()` invocation, exposing -/// a known calling convention with visible and hidden parameters. -pub struct DeferredFiberWrapper { - pub label: String, - pub sig: FunctionSig, - pub visible_param_count: usize, - pub hidden_arg_types: Vec, - /// `true` when hidden descriptor captures must be retained before calling the - /// wrapped closure because that closure frame will release them as owned params. - pub retain_hidden_args_for_closure_call: bool, - /// `true` when the wrapper should call the callable descriptor's uniform - /// invoker instead of re-materializing a statically known ABI signature. - pub use_descriptor_invoker: bool, -} - -/// A callback wrapper that adapts callback builtins to closures with hidden captures. -/// -/// Callback wrappers bridge PHP closures to C callback interfaces (e.g., `array_walk`). -/// They inject hidden capture parameters populated by the builtin and forward -/// visible arguments to the wrapped closure. -pub struct DeferredCallbackWrapper { - pub label: String, - pub visible_arg_types: Vec, - pub target_visible_arg_types: Option>, - pub capture_types: Vec, - pub descriptor_prefix_types: Vec, - pub descriptor_return_type: Option, -} -/// A C-ABI callback trampoline backed by a callable descriptor slot. -/// -/// Extern `callable` parameters receive a plain function pointer, so stateful -/// descriptors need a stable generated symbol that reloads the descriptor from -/// global storage before invoking the uniform runtime callable invoker. -pub struct DeferredExternCallbackTrampoline { - pub label: String, - pub descriptor_slot_label: String, - pub visible_arg_types: Vec, - pub return_type: PhpType, -} + /// Returns a module function by PHP name using PHP's case-insensitive lookup. + pub(super) fn function_by_name(&self, name: &str) -> Option<&'a Function> { + let key = crate::names::php_symbol_key(name.trim_start_matches('\\')); + self.module + .functions + .iter() + .chain(self.module.closures.iter()) + .find(|function| { + crate::names::php_symbol_key(function.name.trim_start_matches('\\')) == key + }) + } -/// A generated runtime callable invoker with a descriptor-based ABI. -/// -/// Invokers receive a callable descriptor pointer plus a normalized Mixed -/// argument array, load the target entry from the descriptor, materialize -/// arguments according to the stored signature, and return a boxed `Mixed` result. -pub struct DeferredRuntimeCallableInvoker { - pub label: String, - pub sig: FunctionSig, - pub captures: Vec<(String, PhpType, bool)>, -} + /// Returns true when an extern declaration exists for a PHP function name. + pub(super) fn has_extern_function(&self, name: &str) -> bool { + let key = crate::names::php_symbol_key(name.trim_start_matches('\\')); + self.module.extern_decls.iter().any(|function| { + crate::names::php_symbol_key(function.name.trim_start_matches('\\')) == key + }) + } -/// Carries mutable codegen state while lowering expressions, statements, functions, and wrappers. -/// -/// Context tracks local variable stack slots, loop labels, class/interface/enum metadata, -/// deferred closure/fiber/callback wrapper emission, ownership facts, and control-flow -/// continuation state for `finally` blocks. All fields are public for direct access by -/// codegen emitters; ownership states must remain conservative across branches, temporaries, -/// and cleanup paths. -pub struct Context { - pub variables: HashMap, - pub stack_offset: usize, - pub loop_stack: Vec, - pub return_label: Option, - pub functions: HashMap, - pub function_variant_groups: HashSet, - pub deferred_closures: Vec, - pub deferred_fiber_wrappers: Vec, - pub deferred_callback_wrappers: Vec, - pub deferred_extern_callback_trampolines: Vec, - pub deferred_runtime_callable_invokers: Vec, - pub constants: HashMap, - /// Variables declared with `global $var` in the current function scope. - pub global_vars: HashSet, - /// Variables declared with `static $var` in functions — maps "func_var" to type. - pub static_vars: HashSet, - /// Reference parameters in the current function — stores their address, not value. - pub ref_params: HashSet, - /// Hidden flags for compiler-created local reference cells. - /// A non-zero flag means the variable's reference slot owns a 16-byte heap cell - /// instead of borrowing storage from a caller, global, or array element. - pub local_ref_cell_flags: HashMap, - /// Whether we're in the main scope (not inside a function). - pub in_main: bool, - /// Set of all variable names that are used globally across the program. - pub all_global_var_names: HashSet, - /// Static variable declarations: (func_name, var_name) -> type - pub all_static_vars: HashMap<(String, String), PhpType>, - /// Closure signatures keyed by variable name, for resolving defaults at call sites. - pub closure_sigs: HashMap, - /// Callable locals whose signature is known but whose receiver/capture environment - /// must be loaded from the runtime descriptor rather than reconstructed locally. - pub runtime_callable_vars: HashSet, - /// Temporary expected wrapper signature for first-class callables evaluated - /// as arguments to APIs that store and invoke the callable later. - pub expected_first_class_callable_sig: Option, - /// Callable signatures inferred for user-function callable parameters. - pub callable_param_sigs: HashMap<(String, String), FunctionSig>, - /// Callable-typed parameters in the current emitted function or method. - pub callable_param_names: HashSet, - /// Callable signatures inferred for user-function callable returns. - pub callable_return_sigs: HashMap, - /// Callable element signatures inferred for user-function array returns. - pub callable_array_return_sigs: HashMap, - /// Fiber callback start signatures inferred for variables holding `Fiber` objects. - pub fiber_start_sigs: HashMap, - /// Fiber callback start signatures inferred for functions returning `Fiber` objects. - pub fiber_return_sigs: HashMap, - /// Captured variables per closure variable name: maps $fn -> [(capture_name, type, by_ref)]. - pub closure_captures: HashMap>, - /// Runtime-dispatch wrappers synthesized for PHP builtin callbacks selected - /// by a dynamic string name. The key is the canonical builtin name. - pub runtime_callable_builtin_wrappers: HashMap, - /// Runtime-dispatch wrappers synthesized for extern callbacks selected by - /// a dynamic string name. The key is the declared extern function name. - pub runtime_callable_extern_wrappers: HashMap, - /// Runtime-dispatch wrappers synthesized for `Class::method` string - /// callbacks. The key is the PHP-visible `Class::method` name. - pub runtime_callable_static_method_wrappers: HashMap, - /// Runtime-dispatch wrappers synthesized for instance-method and - /// `__invoke` descriptors. The key is the receiver class plus method name. - pub runtime_callable_instance_method_wrappers: HashMap, - /// Callable array targets assigned to variables, for PHP forms such as - /// `$cb = [$object, "method"]` and `$cb = [ClassName::class, "method"]`. - pub callable_array_targets: HashMap, - /// First-class callable target stored in a variable, mirroring the Checker's - /// `first_class_callable_targets` so call sites can short-circuit to a direct - /// function/method/static-method call instead of going through the closure - /// wrapper. Populated at assignment time; cleared on reassignment to a - /// non-FCC value. See `emit_closure_call` for consumers. - pub first_class_callable_targets: HashMap, - /// For each variable currently bound to an FCC, the label of the deferred - /// wrapper that materialises that FCC. Used by `emit_variable` to mark a - /// wrapper as `needed = true` when the FCC value escapes to anything other - /// than a short-circuited call — at which point the dead-wrapper stub - /// optimisation must back off and emit the full body. - pub variable_fcc_label: HashMap, - /// Frame slot holding the descriptor passed to a runtime callable invoker. - /// - /// When set, callback argument lowering appends hidden captures by reading - /// runtime capture slots from this descriptor instead of caller locals. - pub runtime_capture_descriptor_offset: Option, - /// Class definitions for OOP support. - pub classes: HashMap, - /// Interface definitions for OOP support. - pub interfaces: HashMap, - /// Trait declarations preserved for AOT introspection builtins. - pub traits: HashSet, - /// Enum definitions. - pub enums: HashMap, - /// Packed layout-only record definitions. - pub packed_classes: HashMap, - /// Name of the class currently being compiled (for $this resolution). - pub current_class: Option, - /// Extern function declarations (FFI). - pub extern_functions: HashMap, - /// Extern class (C struct) declarations (FFI). - pub extern_classes: HashMap, - /// Extern global variable declarations (FFI). - pub extern_globals: HashMap, - /// Current function return type for return/finally control-flow handling. - pub return_type: PhpType, - /// Hidden activation-record slot offsets: prev frame / cleanup callback / frame base. - pub activation_prev_offset: Option, - pub activation_cleanup_offset: Option, - pub activation_frame_base_offset: Option, - /// Hidden control-flow continuation state used to route return/break/continue through finally blocks. - pub pending_action_offset: Option, - pub pending_target_offset: Option, - pub nested_concat_offset_offset: Option, - /// Hidden frame slot holding the `_concat_off` value inherited from the caller at - /// function entry. Per-statement concat resets restore `_concat_off` to this base - /// (instead of 0) so a `_concat_buf`-slice argument passed by the caller is preserved - /// across the callee's statement boundaries. `None` in `main`/raw contexts (reset to 0). - pub concat_base_offset: Option, - pub pending_return_value_offset: Option, - /// Pre-allocated exception handler slots for try/catch lowering. - pub try_slot_offsets: Vec, - pub next_try_slot_idx: usize, - /// Stack of active finally regions (innermost last). - pub finally_stack: Vec, -} + /// Returns the public include-variant group name matching a PHP function name. + pub(super) fn function_variant_group_name(&self, name: &str) -> Option { + let key = crate::names::php_symbol_key(name.trim_start_matches('\\')); + super::function_variants::collect_dispatch_groups(self.module) + .into_iter() + .find(|group| crate::names::php_symbol_key(group.name.trim_start_matches('\\')) == key) + .map(|group| group.name) + } -/// Metadata for a local variable tracked during codegen. -pub struct VarInfo { - pub ty: PhpType, - pub static_ty: PhpType, - pub stack_offset: usize, - pub slot_size: usize, - pub ownership: HeapOwnership, - pub epilogue_cleanup_safe: bool, -} + /// Returns the concrete function whose signature should be used for a PHP call target. + pub(super) fn callable_function_by_name(&self, name: &str) -> Option<&'a Function> { + self.function_by_name(name) + .or_else(|| super::function_variants::variant_callee_for_group(self.module, name)) + } -/// Metadata for a compiler-created local reference cell flag. -/// -/// A non-zero flag indicates the variable's reference slot owns a 16-byte heap cell -/// instead of borrowing storage from a caller, global, or array element. -pub struct LocalRefCellFlag { - pub variable: String, - pub offset: usize, - pub value_ty: Option, -} + /// Returns a function value or a structured backend error. + pub(super) fn value_php_type(&self, value: ValueId) -> Result { + self.function + .value(value) + .map(|metadata| metadata.php_type.codegen_repr()) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw())) + } -/// Labels and stack-adjustment info for a loop or switch construct. -/// -/// `continue_label` is the target for `continue` statements; `break_label` is the target -/// for `break` statements. `sp_adjust` indicates bytes pushed to the stack by the loop -/// entry (e.g., switch tables) so `return` inside the loop can pop before branching. -pub struct LoopLabels { - pub continue_label: String, - pub break_label: String, - /// If true, this loop entry is a switch that pushed 16 bytes to the stack. - /// Return statements inside need to pop this before jumping to epilogue. - pub sp_adjust: usize, -} + /// Returns a function value's source PHP metadata before codegen representation erasure. + pub(super) fn raw_value_php_type(&self, value: ValueId) -> Result { + self.function + .value(value) + .map(|metadata| metadata.php_type.clone()) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw())) + } -/// Metadata for an active `finally` block during codegen. -/// -/// `entry_label` marks the start of the finally block's code, used by `pending_action` -/// and `pending_target` to route `return`/`break`/`continue` through the finally before -/// reaching the actual target. -#[derive(Debug, Clone)] -pub struct FinallyContext { - pub entry_label: String, -} + /// Returns the EIR ownership metadata attached to an SSA value. + pub(super) fn value_ownership(&self, value: ValueId) -> Result { + self.function + .value(value) + .map(|metadata| metadata.ownership) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw())) + } -impl Default for Context { - /// Builds the default value for the surrounding type. - fn default() -> Self { - Self::new() + /// Returns the runtime PHP type stored in a local slot. + pub(super) fn local_php_type(&self, slot: LocalSlotId) -> Result { + self.function + .locals + .get(slot.as_raw() as usize) + .map(|metadata| metadata.php_type.codegen_repr()) + .ok_or_else(|| CodegenIrError::missing_entry("local slot", slot.as_raw())) } -} -impl Context { - // Inherits module-level doc from `Context` struct. + /// Returns the semantic role attached to a local slot. + pub(super) fn local_kind(&self, slot: LocalSlotId) -> Result { + self.function + .locals + .get(slot.as_raw() as usize) + .map(|metadata| metadata.kind) + .ok_or_else(|| CodegenIrError::missing_entry("local slot", slot.as_raw())) + } - /// Creates a default `Context` for top-level (non-function) codegen. - /// - /// All maps and vectors are empty; `in_main` is `false`; `return_type` is `Void`. - /// Use `crate::codegen::generate()` to obtain a fully initialized context for a program. - pub fn new() -> Self { - Self { - variables: HashMap::new(), - stack_offset: 0, - loop_stack: Vec::new(), - return_label: None, - functions: HashMap::new(), - function_variant_groups: HashSet::new(), - deferred_closures: Vec::new(), - deferred_fiber_wrappers: Vec::new(), - deferred_callback_wrappers: Vec::new(), - deferred_extern_callback_trampolines: Vec::new(), - deferred_runtime_callable_invokers: Vec::new(), - constants: HashMap::new(), - global_vars: HashSet::new(), - static_vars: HashSet::new(), - ref_params: HashSet::new(), - local_ref_cell_flags: HashMap::new(), - in_main: false, - all_global_var_names: HashSet::new(), - all_static_vars: HashMap::new(), - closure_sigs: HashMap::new(), - runtime_callable_vars: HashSet::new(), - expected_first_class_callable_sig: None, - callable_param_sigs: HashMap::new(), - callable_param_names: HashSet::new(), - callable_return_sigs: HashMap::new(), - callable_array_return_sigs: HashMap::new(), - fiber_start_sigs: HashMap::new(), - fiber_return_sigs: HashMap::new(), - closure_captures: HashMap::new(), - runtime_callable_builtin_wrappers: HashMap::new(), - runtime_callable_extern_wrappers: HashMap::new(), - runtime_callable_static_method_wrappers: HashMap::new(), - runtime_callable_instance_method_wrappers: HashMap::new(), - callable_array_targets: HashMap::new(), - first_class_callable_targets: HashMap::new(), - variable_fcc_label: HashMap::new(), - runtime_capture_descriptor_offset: None, - classes: HashMap::new(), - interfaces: HashMap::new(), - traits: HashSet::new(), - enums: HashMap::new(), - packed_classes: HashMap::new(), - current_class: None, - extern_functions: HashMap::new(), - extern_classes: HashMap::new(), - extern_globals: HashMap::new(), - return_type: PhpType::Void, - activation_prev_offset: None, - activation_cleanup_offset: None, - activation_frame_base_offset: None, - pending_action_offset: None, - pending_target_offset: None, - nested_concat_offset_offset: None, - concat_base_offset: None, - pending_return_value_offset: None, - try_slot_offsets: Vec::new(), - next_try_slot_idx: 0, - finally_stack: Vec::new(), - } + /// Returns the local slot with the requested source name. + pub(super) fn local_slot_by_name(&self, name: &str) -> Option { + self.function + .locals + .iter() + .find(|local| local.name.as_deref() == Some(name)) + .map(|local| local.id) } - /// Allocates a local variable slot on the stack. - pub fn alloc_var(&mut self, name: &str, ty: PhpType) -> usize { - self.alloc_var_with_static_type(name, ty.clone(), ty) + /// Marks a local slot as storing a heap reference cell pointer instead of its raw value. + pub(super) fn mark_promoted_ref_cell(&mut self, slot: LocalSlotId) { + self.promoted_ref_cells.insert(slot); } - /// Allocates a local variable with a distinct static type. - pub fn alloc_var_with_static_type( - &mut self, - name: &str, - ty: PhpType, - static_ty: PhpType, - ) -> usize { - let slot_size = ty.stack_size(); - self.stack_offset += slot_size; - let offset = self.stack_offset; - let ownership = HeapOwnership::for_type(&ty); - self.variables.insert( - name.to_string(), - VarInfo { - ty, - static_ty, - stack_offset: offset, - slot_size, - ownership, - epilogue_cleanup_safe: true, - }, - ); - offset + /// Marks a local slot as storing its raw value again after an `unset()` unbind. + pub(super) fn unmark_promoted_ref_cell(&mut self, slot: LocalSlotId) { + self.promoted_ref_cells.remove(&slot); } - /// Ensures an already-collected local has enough reserved frame space for a type. - /// - /// This is only safe during pre-emission local collection. When a later write needs a - /// wider representation, such as replacing an 8-byte int slot with a 16-byte string - /// slot, the variable is moved to a fresh slot and the old slot is left unused. - pub fn ensure_var_slot_capacity_for_type(&mut self, name: &str, ty: &PhpType) { - let required_size = ty.stack_size(); - let Some(var) = self.variables.get_mut(name) else { - return; - }; - if var.slot_size >= required_size { - return; - } - self.stack_offset += required_size; - var.stack_offset = self.stack_offset; - var.slot_size = required_size; + /// Returns true when a local slot has been promoted to a heap reference cell. + pub(super) fn is_promoted_ref_cell(&self, slot: LocalSlotId) -> bool { + self.promoted_ref_cells.contains(&slot) } - /// Allocates a hidden stack slot of the given size. - pub fn alloc_hidden_slot(&mut self, size: usize) -> usize { - self.stack_offset += size; - self.stack_offset + /// Returns true when a local slot stores a heap reference-cell pointer. + pub(super) fn local_stores_ref_cell_pointer(&self, slot: LocalSlotId) -> bool { + self.is_by_ref_param_slot(slot) || self.is_promoted_ref_cell(slot) } - /// Generates a key for a local reference cell flag from variable name and span. - pub fn foreach_local_ref_cell_flag_key(name: &str, span: Span) -> String { - format!("{}:{}:{}", name, span.line, span.col) + /// Returns true when the local slot is the storage slot for a by-reference parameter. + fn is_by_ref_param_slot(&self, slot: LocalSlotId) -> bool { + self.function + .params + .get(slot.as_raw() as usize) + .is_some_and(|param| param.by_ref) } - /// Ensures a local reference cell flag exists for the given key, allocating a hidden slot if needed. - pub fn ensure_local_ref_cell_flag(&mut self, key: String, name: &str) -> usize { - if let Some(flag) = self.local_ref_cell_flags.get(&key) { - return flag.offset; + /// Loads a stored SSA value into the target's canonical result register(s). + /// + /// When the value lives in an allocated register, it is moved from there + /// into the result register instead of loaded from a stack slot. + pub(super) fn load_value_to_result(&mut self, value: ValueId) -> Result { + let ty = self.value_php_type(value)?; + if let Some(reg) = self.allocation.register_of(value) { + let dst = if ty.codegen_repr() == PhpType::Float { + abi::float_result_reg(self.emitter) + } else { + abi::int_result_reg(self.emitter) + }; + abi::emit_reg_move(self.emitter, dst, reg); + } else { + let offset = self.value_offset(value)?; + abi::emit_load(self.emitter, &ty.codegen_repr(), offset); } - let offset = self.alloc_hidden_slot(8); - self.local_ref_cell_flags.insert( - key, - LocalRefCellFlag { - variable: name.to_string(), - offset, - value_ty: None, - }, - ); - offset + Ok(ty) } - /// Sets the value type for a local reference cell flag. - pub fn set_local_ref_cell_flag_type(&mut self, key: &str, value_ty: PhpType) { - if let Some(flag) = self.local_ref_cell_flags.get_mut(key) { - flag.value_ty = Some(value_ty); + /// Loads a single-register SSA value into a caller-selected register. + /// + /// When the value lives in an allocated register, it is moved register to + /// register (a no-op when the source already is the requested register). + pub(super) fn load_value_to_reg(&mut self, value: ValueId, reg: &str) -> Result { + let ty = self.value_php_type(value)?; + if let Some(home) = self.allocation.register_of(value) { + abi::emit_reg_move(self.emitter, reg, home); + } else { + let offset = self.value_offset(value)?; + abi::load_at_offset(self.emitter, reg, offset); } + Ok(ty) } - /// Sets the heap ownership for the named variable, overwriting the previous value. - pub fn set_var_ownership(&mut self, name: &str, ownership: HeapOwnership) { - if let Some(var) = self.variables.get_mut(name) { - var.ownership = ownership; + /// Loads a string SSA value into a caller-selected register pair. + pub(super) fn load_string_value_to_regs( + &mut self, + value: ValueId, + ptr_reg: &str, + len_reg: &str, + ) -> Result<()> { + let ty = self.value_php_type(value)?; + if ty != PhpType::Str { + return Err(CodegenIrError::unsupported(format!( + "string register materialization for PHP type {:?}", + ty + ))); } + let offset = self.value_offset(value)?; + abi::load_at_offset(self.emitter, ptr_reg, offset); + abi::load_at_offset(self.emitter, len_reg, offset - 8); + Ok(()) } - /// Marks a variable as not safe for epilogue cleanup. - pub fn disable_epilogue_cleanup(&mut self, name: &str) { - if let Some(var) = self.variables.get_mut(name) { - var.epilogue_cleanup_safe = false; + /// Loads a local slot into the target's canonical result register(s). + pub(super) fn load_local_to_result(&mut self, slot: LocalSlotId) -> Result { + if self.local_stores_ref_cell_pointer(slot) { + return self.load_ref_cell_local_to_result(slot); } + let ty = self.local_php_type(slot)?; + let offset = self.local_offset(slot)?; + abi::emit_load(self.emitter, &ty.codegen_repr(), offset); + Ok(ty) } - /// Marks a variable as safe for epilogue cleanup. - pub fn enable_epilogue_cleanup(&mut self, name: &str) { - if let Some(var) = self.variables.get_mut(name) { - var.epilogue_cleanup_safe = true; + /// Loads the value pointed to by a local ref-cell pointer slot. + fn load_ref_cell_local_to_result(&mut self, slot: LocalSlotId) -> Result { + let ty = self.local_php_type(slot)?; + reject_multiword_ref_cell_local(&ty, "load")?; + 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 ty.codegen_repr() { + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(self.emitter); + abi::emit_load_from_address(self.emitter, ptr_reg, pointer_reg, 0); + abi::emit_load_from_address(self.emitter, len_reg, pointer_reg, 8); + } + PhpType::Float => { + abi::emit_load_from_address(self.emitter, abi::float_result_reg(self.emitter), pointer_reg, 0); + } + PhpType::TaggedScalar => { + abi::emit_load_from_address(self.emitter, abi::int_result_reg(self.emitter), pointer_reg, 0); + abi::emit_load_from_address( + self.emitter, + crate::codegen::sentinels::tagged_scalar_tag_reg(self.emitter), + pointer_reg, + 8, + ); + } + _ => { + abi::emit_load_from_address(self.emitter, abi::int_result_reg(self.emitter), pointer_reg, 0); + } } + Ok(ty) } - /// Updates both the runtime type and heap ownership for a variable. - pub fn update_var_type_and_ownership( - &mut self, - name: &str, - ty: PhpType, - ownership: HeapOwnership, - ) { - self.update_var_type_static_and_ownership(name, ty.clone(), ty, ownership); - } - - /// Marks the deferred FCC wrapper backing `var` as `needed = true`, so the - /// emission loop emits its body instead of the dead-wrapper stub. Call this - /// from any site that consumes an FCC variable's runtime value (loads its - /// address for an indirect call, threads its captures through a callback - /// builtin, materialises it into a Fiber, etc.). The short-circuit paths - /// in `emit_closure_call` deliberately do NOT call this — that's the whole - /// point of the optimisation. - pub fn mark_fcc_used(&mut self, var: &str) { - if let Some(label) = self.variable_fcc_label.get(var).cloned() { - if let Some(deferred) = - self.deferred_closures.iter_mut().find(|d| d.label == label) - { - deferred.needed = true; - } + /// Stores the current result register(s) into the SSA value's home. + /// + /// When the value lives in an allocated register, the result register is + /// moved into it; otherwise it is stored into the value's stack slot. + pub(super) fn store_result_value(&mut self, value: ValueId) -> Result<()> { + let ty = self.value_php_type(value)?; + if let Some(reg) = self.allocation.register_of(value) { + let src = if ty.codegen_repr() == PhpType::Float { + abi::float_result_reg(self.emitter) + } else { + abi::int_result_reg(self.emitter) + }; + abi::emit_reg_move(self.emitter, reg, src); + } else { + let offset = self.value_offset(value)?; + self.store_current_result_at_offset(&ty, offset); } + Ok(()) } - /// Updates the runtime type, static type, and heap ownership for a variable. - pub fn update_var_type_static_and_ownership( - &mut self, - name: &str, - ty: PhpType, - static_ty: PhpType, - ownership: HeapOwnership, - ) { - if let Some(var) = self.variables.get_mut(name) { - var.ty = ty; - var.static_ty = static_ty; - var.ownership = ownership; + /// Stores the integer result register as a single machine word into the SSA value's home. + /// + /// Reference-cell pointers are always one pointer-sized word regardless of the element + /// type they alias (a `string` cell pointer is still one word, not a `{ptr,len}` pair). + /// `LoadPropRefCell` and by-reference call results materialize the cell pointer into the + /// integer result register, so it must be stored single-word; the type-driven + /// `store_result_value` would otherwise split a `Str`/`Float` result across the string or + /// float result registers and drop the pointer. + pub(super) fn store_int_result_value(&mut self, value: ValueId) -> Result<()> { + if let Some(reg) = self.allocation.register_of(value) { + abi::emit_reg_move(self.emitter, reg, abi::int_result_reg(self.emitter)); + } else { + let offset = self.value_offset(value)?; + abi::store_at_offset(self.emitter, abi::int_result_reg(self.emitter), offset); } + Ok(()) } - /// Finds the most specific common object type between two class names. - pub fn common_object_type(&self, left: &str, right: &str) -> Option { - if left == right { - return Some(PhpType::Object(left.to_string())); + /// Stores an SSA value into an addressable local slot. + pub(super) fn store_value_to_local(&mut self, slot: LocalSlotId, value: ValueId) -> Result<()> { + if self.local_stores_ref_cell_pointer(slot) { + return self.store_value_to_ref_cell_local(slot, value); } - if self.is_subclass_of(left, right) - || self.class_implements_interface(left, right) - || self.interface_extends_interface(left, right) - { - return Some(PhpType::Object(right.to_string())); + let source_ty = self.load_value_to_result(value)?; + let target_ty = self.local_php_type(slot)?; + if target_ty == PhpType::Mixed && source_ty != PhpType::Mixed { + if self.value_can_own_mixed_box_source(value)? { + emit_box_current_owned_value_as_mixed(self.emitter, &source_ty); + } else { + emit_box_current_value_as_mixed(self.emitter, &source_ty); + } } - if self.is_subclass_of(right, left) - || self.class_implements_interface(right, left) - || self.interface_extends_interface(right, left) + // Narrow Mixed to Int when the local slot is typed Int but the value + // is Mixed (from checked integer arithmetic that may overflow to float). + // The runtime cast helper truncates floats and extracts ints. The + // original Mixed box is released after narrowing to avoid leaks. + if matches!(target_ty.codegen_repr(), PhpType::Int) + && matches!(source_ty.codegen_repr(), PhpType::Mixed) { - return Some(PhpType::Object(left.to_string())); + let result_reg = abi::int_result_reg(self.emitter); + let arg_reg = abi::int_arg_reg_name(self.emitter.target, 0); + if result_reg != arg_reg { + abi::emit_reg_move(self.emitter, arg_reg, result_reg); + } + abi::emit_push_reg(self.emitter, result_reg); + abi::emit_push_reg(self.emitter, arg_reg); + abi::emit_call_label(self.emitter, "__rt_mixed_cast_int"); + match self.emitter.target.arch { + Arch::AArch64 => { + self.emitter.instruction("str x0, [sp, #16]"); // save the int result to the placeholder slot + } + Arch::X86_64 => { + self.emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // save the int result to the placeholder slot + } + } + abi::emit_pop_reg(self.emitter, result_reg); + abi::emit_call_label(self.emitter, "__rt_decref_mixed"); + abi::emit_pop_reg(self.emitter, result_reg); } + coerce_current_result_for_target_store(self.emitter, &source_ty, &target_ty)?; + let offset = self.local_offset(slot)?; + self.store_current_result_at_offset(&target_ty, offset); + Ok(()) + } - let mut left_ancestors = HashSet::new(); - let mut current = Some(left.to_string()); - while let Some(class_name) = current { - left_ancestors.insert(class_name.clone()); - current = self - .classes - .get(&class_name) - .and_then(|class_info| class_info.parent.clone()); + /// 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 + /// storage when `value` was loaded from a global — i.e. a superglobal such as + /// `$_SERVER`/`$_GET`/`$_POST`. Mirrors the local-slot write-back that array + /// and hash set/append lowerings already perform; without it a global array + /// that grows past its initial capacity leaves the global symbol pointing at + /// freed storage (corruption / crash). No-op unless `value` came from + /// `Op::LoadGlobal`. + pub(super) fn writeback_global_array_source(&mut self, value: ValueId) -> Result<()> { + let Some(value_ref) = self.function.value(value) else { + return Err(CodegenIrError::missing_entry("value", value.as_raw())); + }; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Ok(()); + }; + let Some(inst_ref) = self.function.instruction(inst) else { + return Err(CodegenIrError::missing_entry("instruction", inst.as_raw())); + }; + if inst_ref.op != Op::LoadGlobal { + return Ok(()); } + let Some(crate::ir::Immediate::GlobalName(data)) = inst_ref.immediate else { + return Ok(()); + }; + let name = self.global_name_data(data)?.to_string(); + let symbol = crate::names::ir_global_symbol(&name); + let ty = self.value_php_type(value)?; + self.data.add_comm(symbol.clone(), ty.codegen_repr().stack_size().max(8)); + self.load_value_to_result(value)?; + abi::emit_store_result_to_symbol(self.emitter, &symbol, &ty, false); + Ok(()) + } - let mut current = Some(right.to_string()); - while let Some(class_name) = current { - if left_ancestors.contains(&class_name) { - return Some(PhpType::Object(class_name)); + /// Stores an SSA value through a local ref-cell pointer slot. + fn store_value_to_ref_cell_local(&mut self, slot: LocalSlotId, value: ValueId) -> Result<()> { + let source_ty = self.load_value_to_result(value)?; + let target_ty = self.local_php_type(slot)?; + reject_multiword_ref_cell_local(&target_ty, "store")?; + if target_ty == PhpType::Mixed && source_ty != PhpType::Mixed { + if self.value_can_own_mixed_box_source(value)? { + emit_box_current_owned_value_as_mixed(self.emitter, &source_ty); + } else { + emit_box_current_value_as_mixed(self.emitter, &source_ty); } - current = self - .classes - .get(&class_name) - .and_then(|class_info| class_info.parent.clone()); } - - None + coerce_current_result_for_target_store(self.emitter, &source_ty, &target_ty)?; + 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(()) } - /// Returns true when subclass of. - fn is_subclass_of(&self, class_name: &str, ancestor_name: &str) -> bool { - let mut current = self - .classes - .get(class_name) - .and_then(|class_info| class_info.parent.as_deref()); - while let Some(parent) = current { - if parent == ancestor_name { - return true; + /// 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() { + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(self.emitter); + abi::store_at_offset(self.emitter, ptr_reg, offset); + abi::store_at_offset(self.emitter, len_reg, offset - 8); + } + PhpType::TaggedScalar => { + abi::store_at_offset(self.emitter, abi::int_result_reg(self.emitter), offset); + abi::store_at_offset( + self.emitter, + crate::codegen::sentinels::tagged_scalar_tag_reg(self.emitter), + offset - 8, + ); + } + PhpType::Float => { + abi::store_at_offset(self.emitter, abi::float_result_reg(self.emitter), offset); + } + PhpType::Void => { + abi::store_at_offset(self.emitter, abi::int_result_reg(self.emitter), offset); + } + PhpType::Never => {} + _ => { + abi::store_at_offset(self.emitter, abi::int_result_reg(self.emitter), offset); } - current = self - .classes - .get(parent) - .and_then(|class_info| class_info.parent.as_deref()); } - false } - /// Checks if a type (class or interface) implements a given interface. - pub(crate) fn object_type_implements_interface( - &self, - type_name: &str, - interface_name: &str, - ) -> bool { - if self.classes.contains_key(type_name) { - return self.class_implements_interface(type_name, interface_name); - } - if self.interfaces.contains_key(type_name) { - return type_name == interface_name - || self.interface_extends_interface(type_name, interface_name); + /// Returns true when a value producer can leave an owned source consumed by Mixed boxing. + pub(super) fn value_can_own_mixed_box_source(&self, value: ValueId) -> Result { + if self.value_php_type(value)?.codegen_repr() == PhpType::Str { + return self.value_is_heap_owned_string_for_mixed_box(value); } - false + let Some(value_ref) = self.function.value(value) else { + return Err(CodegenIrError::missing_entry("value", value.as_raw())); + }; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Ok(false); + }; + let inst = self + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + Ok(matches!( + inst.op, + Op::Acquire + | Op::ArrayNew + | Op::HashNew + | Op::ArrayToMixed + | Op::ArrayCloneShallow + | Op::HashCloneShallow + | Op::ArrayUnion + | Op::HashUnion + | Op::ArrayHashUnion + | Op::HashArrayUnion + | Op::ArrayToHash + | Op::ObjectNew + | Op::DynamicObjectNew + | Op::DynamicObjectNewMixed + | Op::ClosureNew + | Op::FirstClassCallableNew + | Op::CallableArrayNew + | Op::BufferNew + | Op::GeneratorNew + | Op::Call + | Op::FunctionVariantCall + | Op::BuiltinCall + | Op::RuntimeCall + | Op::ExternCall + | Op::MethodCall + | Op::NullsafeMethodCall + | Op::StaticMethodCall + | Op::ClosureCall + | Op::CallableDescriptorInvoke + | Op::ExprCall + | Op::PipeCall + | Op::IteratorMethodCall + | Op::SplRuntimeCall + | Op::FiberRuntimeCall + )) } - /// Computes implements interface for the PHP class-introspection builtin. - fn class_implements_interface(&self, class_name: &str, interface_name: &str) -> bool { - self.classes.get(class_name).is_some_and(|class_info| { - class_info.interfaces.iter().any(|implemented| { - implemented == interface_name - || self.interface_extends_interface(implemented, interface_name) - }) - }) + /// Returns true when a string producer leaves a heap-owned payload that Mixed boxing may consume. + fn value_is_heap_owned_string_for_mixed_box(&self, value: ValueId) -> Result { + let Some(value_ref) = self.function.value(value) else { + return Err(CodegenIrError::missing_entry("value", value.as_raw())); + }; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Ok(false); + }; + let inst = self + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + Ok(matches!( + inst.op, + Op::Acquire + | Op::StrPersist + | Op::Call + | Op::FunctionVariantCall + | Op::ExternCall + | Op::MethodCall + | Op::NullsafeMethodCall + | Op::StaticMethodCall + | Op::ClosureCall + | Op::CallableDescriptorInvoke + | Op::ExprCall + | Op::PipeCall + | Op::IteratorMethodCall + | Op::SplRuntimeCall + | Op::FiberRuntimeCall + )) } - /// Provides the Interface extends interface helper used by the context module. - fn interface_extends_interface(&self, child_name: &str, ancestor_name: &str) -> bool { - if child_name == ancestor_name { - return true; - } - self.interfaces.get(child_name).is_some_and(|interface_info| { - interface_info.parents.iter().any(|parent| { - parent == ancestor_name || self.interface_extends_interface(parent, ancestor_name) - }) - }) + /// Interns a module data-pool string into the assembly data section. + pub(super) fn intern_string_data(&mut self, data_id: DataId) -> Result<(String, usize)> { + let value = self + .module + .data + .strings + .get(data_id.as_raw() as usize) + .ok_or_else(|| CodegenIrError::missing_entry("data string", data_id.as_raw()))?; + let bytes = crate::string_bytes::literal_bytes(value); + Ok(self.data.add_string(&bytes)) } - /// Generates a unique label with the given prefix. - pub fn next_label(&mut self, prefix: &str) -> String { - let id = GLOBAL_LABEL_COUNTER.fetch_add(1, Ordering::SeqCst); - format!("_{}_{}", prefix, id) + /// Interns a module class-name data-pool entry into the assembly data section. + pub(super) fn intern_class_name_data(&mut self, data_id: DataId) -> Result<(String, usize)> { + let value = self + .module + .data + .class_names + .get(data_id.as_raw() as usize) + .ok_or_else(|| CodegenIrError::missing_entry("class data", data_id.as_raw()))?; + Ok(self.data.add_string(value.as_bytes())) } - /// Returns the next pre-allocated try handler slot offset. - pub fn next_try_slot(&mut self) -> usize { - let offset = *self - .try_slot_offsets - .get(self.next_try_slot_idx) - .expect("codegen bug: missing pre-allocated try handler slot"); - self.next_try_slot_idx += 1; - offset + /// Returns a module data-pool function name. + pub(super) fn function_name_data(&self, data_id: DataId) -> Result<&str> { + self.module + .data + .function_names + .get(data_id.as_raw() as usize) + .map(String::as_str) + .ok_or_else(|| CodegenIrError::missing_entry("function data", data_id.as_raw())) + } + + /// Returns a module data-pool global name. + pub(super) fn global_name_data(&self, data_id: DataId) -> Result<&str> { + self.module + .data + .global_names + .get(data_id.as_raw() as usize) + .map(String::as_str) + .ok_or_else(|| CodegenIrError::missing_entry("global data", data_id.as_raw())) + } + + /// Returns true when the EIR module has interned a matching global name. + pub(super) fn has_global_name(&self, name: &str) -> bool { + let normalized = name.trim_start_matches('\\'); + self.module + .data + .global_names + .iter() + .any(|candidate| candidate.trim_start_matches('\\') == normalized) + } + + /// Returns the frame offset assigned to a value by Phase 04 placement. + fn value_offset(&self, value: ValueId) -> Result { + self.placement + .slot(value) + .ok_or_else(|| CodegenIrError::missing_entry("value slot", value.as_raw())) + } + + /// Returns the frame offset assigned to a value for custom multi-word lowerings. + pub(super) fn value_frame_offset(&self, value: ValueId) -> Result { + self.value_offset(value) + } + + /// Returns the frame offset assigned to an addressable EIR local. + pub(super) fn local_offset(&self, slot: LocalSlotId) -> Result { + self.local_offsets + .get(&slot) + .copied() + .ok_or_else(|| CodegenIrError::missing_entry("local slot offset", slot.as_raw())) + } + + /// Returns the frame offset assigned to a high-level try-handler token. + pub(super) fn try_handler_offset(&self, token: i64) -> Result { + self.try_handler_offsets + .get(&token) + .copied() + .ok_or_else(|| CodegenIrError::invalid_module(format!("missing try handler token {}", token))) } } -#[cfg(test)] -mod tests { - use super::HeapOwnership; - use crate::types::PhpType; - - /// Verifies that heap ownership type classification. - #[test] - fn test_heap_ownership_type_classification() { - assert_eq!(HeapOwnership::for_type(&PhpType::Int), HeapOwnership::NonHeap); - assert_eq!(HeapOwnership::for_type(&PhpType::Str), HeapOwnership::MaybeOwned); - assert_eq!( - HeapOwnership::local_owner_for_type(&PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Int), - }), - HeapOwnership::Owned - ); - assert_eq!( - HeapOwnership::borrowed_alias_for_type(&PhpType::Object("Foo".to_string())), - HeapOwnership::Borrowed - ); +/// Rejects local ref-cell operations whose frame representation spans multiple words. +fn reject_multiword_ref_cell_local(ty: &PhpType, action: &str) -> Result<()> { + let _ = (ty, action); + Ok(()) +} + +/// Coerces the currently loaded result registers before storing into a typed local slot. +fn coerce_current_result_for_target_store( + emitter: &mut Emitter, + source_ty: &PhpType, + target_ty: &PhpType, +) -> Result<()> { + if target_ty.codegen_repr() != PhpType::TaggedScalar { + return Ok(()); } + match source_ty.codegen_repr() { + PhpType::TaggedScalar => Ok(()), + PhpType::Int | PhpType::Bool | PhpType::Callable => { + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + Ok(()) + } + PhpType::Void | PhpType::Never => { + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + emit_mixed_result_as_tagged_scalar(emitter); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "local store from PHP type {:?} to PHP type TaggedScalar", + other + ))), + } +} - /// Verifies that heap ownership merge. - #[test] - fn test_heap_ownership_merge() { - assert_eq!( - HeapOwnership::Owned.merge(HeapOwnership::Owned), - HeapOwnership::Owned - ); - assert_eq!( - HeapOwnership::Borrowed.merge(HeapOwnership::Borrowed), - HeapOwnership::Borrowed - ); - assert_eq!( - HeapOwnership::Owned.merge(HeapOwnership::Borrowed), - HeapOwnership::MaybeOwned - ); - assert_eq!( - HeapOwnership::NonHeap.merge(HeapOwnership::Borrowed), - HeapOwnership::Borrowed - ); +/// Reorders `__rt_mixed_unbox` output into the EIR tagged-scalar result registers. +fn emit_mixed_result_as_tagged_scalar(emitter: &mut Emitter) { + abi::emit_call_label(emitter, "__rt_mixed_unbox"); + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("mov x9, x0"); // preserve the unboxed Mixed tag before moving the payload + emitter.instruction("mov x0, x1"); // place the unboxed payload into the tagged-scalar payload register + emitter.instruction("mov x1, x9"); // place the unboxed Mixed tag into the tagged-scalar tag register + } + Arch::X86_64 => { + emitter.instruction("mov r10, rax"); // preserve the unboxed Mixed tag before moving the payload + emitter.instruction("mov rax, rdi"); // place the unboxed payload into the tagged-scalar payload register + emitter.instruction("mov rdx, r10"); // place the unboxed Mixed tag into the tagged-scalar tag register + } } } + +/// Converts arbitrary names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} diff --git a/src/codegen/driver_support.rs b/src/codegen/driver_support.rs deleted file mode 100644 index cd292afefc..0000000000 --- a/src/codegen/driver_support.rs +++ /dev/null @@ -1,842 +0,0 @@ -//! Purpose: -//! Provides support emitters that bridge generated user code with runtime value conventions. -//! Boxes runtime payloads, emits runtime assembly fragments, and normalizes helper call results. -//! -//! Called from: -//! - `crate::codegen::generate()` and runtime-facing codegen helpers -//! -//! Key details: -//! - Mixed boxing and target register choices must match the runtime object layout exactly. - -use crate::parser::ast::Expr; -use crate::types::{ClassInfo, EnumInfo, PhpType}; - -use super::abi; -use super::context::{Context, HeapOwnership}; -use super::data_section::DataSection; -use super::emit::Emitter; -use super::expr::{coerce_result_to_type, emit_expr, expr_result_heap_ownership}; -use super::functions; -use super::platform::{Arch, Target}; -use super::runtime; -use super::runtime_features::RuntimeFeatures; -use super::sentinels::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; - -/// Emits a write syscall for a labeled literal string to stderr, using the given -/// label (from the data section) and its byte length. Handles target-specific -/// register conventions for the write syscall arguments. -pub(crate) fn emit_write_literal_stderr(emitter: &mut Emitter, label: &str, len: usize) { - match emitter.target.arch { - Arch::AArch64 => { - crate::codegen::abi::emit_symbol_address(emitter, "x1", label); // load the page address of the stderr literal on AArch64 - emitter.instruction(&format!("mov x2, #{}", len)); // materialize the stderr literal byte length in the AArch64 write-length register - emitter.instruction("mov x0, #2"); // target the stderr file descriptor on AArch64 - emitter.syscall(4); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", label); - emitter.instruction(&format!("mov edx, {}", len)); // materialize the stderr literal byte length in the x86_64 write-length register - emitter.instruction("mov edi, 2"); // target the stderr file descriptor on x86_64 - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall number 1 = write - emitter.instruction("syscall"); // write the requested literal bytes to stderr on x86_64 - } - } -} - -/// Emits a write syscall for the current string in result registers to stderr. -/// Loads pointer/length from the appropriate ABI registers for the target. -pub(crate) fn emit_write_current_string_stderr(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // target the stderr file descriptor on AArch64 - emitter.syscall(4); - } - Arch::X86_64 => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - emitter.instruction(&format!("mov rsi, {}", ptr_reg)); // move the current string pointer into the x86_64 write buffer register - emitter.instruction(&format!("mov rdx, {}", len_reg)); // move the current string length into the x86_64 write length register - emitter.instruction("mov edi, 2"); // target the stderr file descriptor on x86_64 - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall number 1 = write - emitter.instruction("syscall"); // write the current string payload to stderr on x86_64 - } - } -} - -/// Assembles the complete runtime assembly string for a given heap size and target. -#[allow(dead_code)] -pub fn generate_runtime(heap_size: usize, target: Target) -> String { - generate_runtime_with_features(heap_size, target, RuntimeFeatures::all()) -} - -/// Assembles runtime assembly for the requested optional helper families. -pub fn generate_runtime_with_features( - heap_size: usize, - target: Target, - features: RuntimeFeatures, -) -> String { - generate_runtime_with_features_pic(heap_size, target, features, false) -} - -/// Same as `generate_runtime_with_features` but emits position-independent -/// data references when `pic` is true. Required for the runtime object linked -/// into a `--emit cdylib` artifact, where cross-section symbol references must -/// resolve through the GOT instead of via direct PC-relative relocations. -pub fn generate_runtime_with_features_pic( - heap_size: usize, - target: Target, - features: RuntimeFeatures, - pic: bool, -) -> String { - let mut emitter = if pic { - Emitter::new_pic(target) - } else { - Emitter::new(target) - }; - emitter.emit_text_prelude(); - runtime::emit_runtime(&mut emitter, features); - let mut output = emitter.output(); - output.push('\n'); - output.push_str(&runtime::emit_runtime_data_fixed(heap_size, target)); - // The PIC runtime object only ever links into an ELF cdylib, where every - // runtime global must bind locally: hidden visibility prevents dynamic - // preemption (two loaded elephc modules aliasing one runtime state) and - // keeps the .so's dynamic symbol table down to the public ABI. - if pic && target.platform == crate::codegen::platform::Platform::Linux { - output = crate::codegen::visibility::append_hidden_directives( - &output, - &std::collections::HashSet::new(), - ); - } - output -} - -/// Emits global singleton initializers for all enum cases in sorted order. -pub(super) fn emit_enum_singleton_initializers( - emitter: &mut Emitter, - data: &mut DataSection, - ctx: &Context, - allowed_class_names: Option<&std::collections::HashSet>, -) { - let mut sorted_enums: Vec<(&String, &EnumInfo)> = ctx.enums.iter().collect(); - sorted_enums.sort_by_key(|(name, _)| name.as_str()); - for (enum_name, enum_info) in sorted_enums { - if allowed_class_names.is_some_and(|allowed| !allowed.contains(enum_name)) { - continue; - } - let Some(class_info) = ctx.classes.get(enum_name) else { - continue; - }; - for case in &enum_info.cases { - emitter.comment(&format!("initialize enum singleton {}::{}", enum_name, case.name)); - let obj_size = 8 + class_info.properties.len() * 16; - let result_reg = abi::int_result_reg(emitter); - let object_reg = abi::symbol_scratch_reg(emitter); - let temp_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, result_reg, obj_size as i64); // enum singleton object size in bytes in the heap allocator input register - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate enum singleton object storage - abi::emit_load_int_immediate(emitter, temp_reg, 4); // heap kind 4 = object instance - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("str {}, [{}, #-8]", temp_reg, result_reg)); // store object kind in the uniform heap header just before the payload pointer - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, 0x{:x}", temp_reg, (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word with the uniform heap marker - emitter.instruction(&format!("mov QWORD PTR [{} - 8], {}", result_reg, temp_reg)); // store object kind in the x86_64 uniform heap header just before the payload pointer - } - } - abi::emit_load_int_immediate(emitter, temp_reg, class_info.class_id as i64); // load compile-time enum class id - abi::emit_store_to_address(emitter, temp_reg, result_reg, 0); // store enum class id at object header - abi::emit_push_reg(emitter, result_reg); // save singleton object pointer while initializing properties - - for i in 0..class_info.properties.len() { - let offset = 8 + i * 16; - abi::emit_load_temporary_stack_slot(emitter, object_reg, 0); // peek enum singleton pointer from the temporary stack slot - abi::emit_store_zero_to_address(emitter, object_reg, offset); // zero-initialize the low property word - abi::emit_store_zero_to_address(emitter, object_reg, offset + 8); // zero-initialize the high property word - } - - if let Some(case_value) = &case.value { - abi::emit_load_temporary_stack_slot(emitter, object_reg, 0); // reload enum singleton pointer for backing-value initialization - match case_value { - crate::types::EnumCaseValue::Int(value) => { - load_immediate(emitter, temp_reg, *value); // materialize the enum int backing value - abi::emit_store_to_address(emitter, temp_reg, object_reg, 8); // store the int backing value in the first property slot - abi::emit_store_zero_to_address(emitter, object_reg, 16); // clear the metadata/high word for the int property - } - crate::types::EnumCaseValue::Str(value) => { - let bytes = crate::string_bytes::literal_bytes(value); - let (label, len) = data.add_string(&bytes); - abi::emit_symbol_address(emitter, temp_reg, &label); // materialize the enum string backing literal address - abi::emit_store_to_address(emitter, temp_reg, object_reg, 8); // store the string backing pointer in the first property slot - abi::emit_load_int_immediate(emitter, temp_reg, len as i64); // materialize the enum string backing length - abi::emit_store_to_address(emitter, temp_reg, object_reg, 16); // store the string backing length in the second property word - } - } - } - - abi::emit_pop_reg(emitter, result_reg); // pop initialized enum singleton pointer into the active integer result register - let slot_label = crate::names::enum_case_symbol(enum_name, &case.name); - abi::emit_store_reg_to_symbol(emitter, result_reg, &slot_label, 0); // publish the enum singleton pointer in its global slot - } - } -} - -/// Emits initialization for static properties, including uninitialized sentinels. -/// -/// `allowed_class_names` must match the filter used when emitting static-property *storage* -/// (`emit_runtime_data_user`): classes outside that set get no `.comm` slot, so initializing their -/// statics here would reference an undefined symbol. This matters for builtin/synthetic classes, -/// which are only emitted when actually used (unlike declared user classes); without the filter, a -/// declared-but-unused synthetic class carrying a static property (e.g. `DateTime`/`DateTimeImmutable` -/// sharing one) would emit an initializer for a slot that was never defined. -pub(super) fn emit_static_property_initializers( - emitter: &mut Emitter, - data: &mut DataSection, - ctx: &mut Context, - allowed_class_names: Option<&std::collections::HashSet>, -) { - let mut initializers = Vec::new(); - let mut uninitialized_static_properties = Vec::new(); - let mut sorted_classes: Vec<(&String, &ClassInfo)> = ctx.classes.iter().collect(); - sorted_classes.sort_by_key(|(class_name, _)| class_name.as_str()); - for (class_name, class_info) in sorted_classes { - if allowed_class_names.is_some_and(|allowed| !allowed.contains(class_name.as_str())) { - continue; - } - for (index, (property_name, prop_ty)) in class_info.static_properties.iter().enumerate() { - let declaring_class = class_info - .static_property_declaring_classes - .get(property_name) - .map(String::as_str) - .unwrap_or(class_name.as_str()); - if declaring_class != class_name { - continue; - } - let default_expr = class_info.static_defaults.get(index).cloned().flatten(); - if default_expr.is_none() && class_info.declared_static_properties.contains(property_name) { - uninitialized_static_properties.push((class_name.clone(), property_name.clone())); - } - let Some(default_expr) = default_expr else { - continue; - }; - let declared = class_info.declared_static_properties.contains(property_name); - initializers.push(( - class_name.clone(), - property_name.clone(), - prop_ty.clone(), - default_expr, - declared, - )); - } - } - - for (class_name, property_name) in uninitialized_static_properties { - emitter.comment(&format!( - "mark static property {}::${} uninitialized", - class_name, property_name - )); - let marker_reg = abi::int_result_reg(emitter); - abi::emit_load_int_immediate(emitter, marker_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); - let symbol = crate::names::static_property_symbol(&class_name, &property_name); - abi::emit_store_reg_to_symbol(emitter, marker_reg, &symbol, 8); - } - - for (class_name, property_name, prop_ty, default_expr, declared) in initializers { - emitter.comment(&format!( - "initialize static property {}::${}", - class_name, property_name - )); - let actual_ty = emit_expr(&default_expr, emitter, ctx, data); - let store_ty = if declared { - coerce_result_to_type(emitter, ctx, data, &actual_ty, &prop_ty); - prop_ty - } else { - actual_ty - }; - let symbol = crate::names::static_property_symbol(&class_name, &property_name); - abi::emit_store_result_to_symbol(emitter, &symbol, &store_ty, false); - if !matches!(store_ty.codegen_repr(), PhpType::Str) { - abi::emit_store_zero_to_symbol(emitter, &symbol, 8); - } - } -} - -/// Emits all deferred closures, fiber wrappers, and callback wrappers into the output. -pub(crate) fn emit_deferred_closures( - emitter: &mut Emitter, - data: &mut DataSection, - ctx: &mut Context, -) { - while !ctx.deferred_closures.is_empty() - || !ctx.deferred_fiber_wrappers.is_empty() - || !ctx.deferred_callback_wrappers.is_empty() - || !ctx.deferred_extern_callback_trampolines.is_empty() - || !ctx.deferred_runtime_callable_invokers.is_empty() - { - let closures: Vec<_> = ctx.deferred_closures.drain(..).collect(); - for closure in closures { - if closure.needed { - functions::emit_closure( - emitter, - data, - &closure.label, - &closure.sig, - &closure.hidden_params, - &closure.body, - closure.current_class.as_deref(), - &ctx.functions, - &ctx.callable_return_sigs, - &ctx.callable_array_return_sigs, - &ctx.fiber_return_sigs, - &ctx.function_variant_groups, - &ctx.constants, - &ctx.interfaces, - &ctx.traits, - &ctx.classes, - &ctx.enums, - &ctx.packed_classes, - &ctx.extern_functions, - &ctx.extern_classes, - &ctx.extern_globals, - ); - } else { - emitter.blank(); - emitter.comment(&format!("uninvoked FCC wrapper {} (stubbed)", closure.label)); - emitter.label_global(&closure.label); - crate::codegen::abi::emit_load_int_immediate( - emitter, - crate::codegen::abi::int_result_reg(emitter), - 0, - ); - crate::codegen::abi::emit_return(emitter); - } - } - let wrappers: Vec<_> = ctx.deferred_fiber_wrappers.drain(..).collect(); - for wrapper in wrappers { - functions::emit_fiber_wrapper(emitter, &wrapper); - } - let callback_wrappers: Vec<_> = ctx.deferred_callback_wrappers.drain(..).collect(); - for wrapper in callback_wrappers { - functions::emit_callback_wrapper(emitter, &wrapper); - } - let extern_trampolines: Vec<_> = - ctx.deferred_extern_callback_trampolines.drain(..).collect(); - for trampoline in extern_trampolines { - functions::emit_extern_callback_trampoline(emitter, &trampoline); - } - let invokers: Vec<_> = ctx.deferred_runtime_callable_invokers.drain(..).collect(); - for invoker in invokers { - crate::codegen::runtime_callable_invoker::emit_runtime_callable_invoker( - emitter, - data, - ctx, - &invoker, - ); - } - } -} - -/// Emits code to push the main function's exception cleanup activation record. -pub(super) fn emit_main_activation_record_push( - emitter: &mut Emitter, - ctx: &Context, - cleanup_label: &str, -) { - let prev_offset = ctx - .activation_prev_offset - .expect("codegen bug: missing main activation prev slot"); - let cleanup_offset = ctx - .activation_cleanup_offset - .expect("codegen bug: missing main activation cleanup slot"); - let frame_base_offset = ctx - .activation_frame_base_offset - .expect("codegen bug: missing main activation frame-base slot"); - - emitter.comment("register main exception cleanup frame"); - let scratch = abi::temp_int_reg(emitter.target); - abi::emit_load_symbol_to_reg(emitter, scratch, "_exc_call_frame_top", 0); - abi::store_at_offset(emitter, scratch, prev_offset); // save the previous call-frame pointer in the main activation record - abi::emit_symbol_address(emitter, scratch, cleanup_label); - abi::store_at_offset(emitter, scratch, cleanup_offset); // save the main cleanup callback address in the activation record - abi::emit_copy_frame_pointer(emitter, scratch); - abi::store_at_offset(emitter, scratch, frame_base_offset); // save the main frame pointer in the activation record - abi::emit_store_zero_to_local_slot(emitter, ctx.pending_action_offset.expect("codegen bug: missing main pending-action slot")); // clear any stale finally action before running main - abi::emit_frame_slot_address(emitter, scratch, prev_offset); // compute the address of the main activation record's first slot - abi::emit_store_reg_to_symbol(emitter, scratch, "_exc_call_frame_top", 0); -} - -/// Emits code to pop and restore the previous exception cleanup frame on main exit. -pub(super) fn emit_main_activation_record_pop(emitter: &mut Emitter, ctx: &Context) { - let prev_offset = ctx - .activation_prev_offset - .expect("codegen bug: missing main activation prev slot"); - - emitter.comment("unregister main exception cleanup frame"); - let scratch = abi::temp_int_reg(emitter.target); - abi::load_at_offset(emitter, scratch, prev_offset); // reload the previous call-frame pointer from the main activation record - abi::emit_store_reg_to_symbol(emitter, scratch, "_exc_call_frame_top", 0); -} - -/// Emits the main cleanup callback label and body for exception unwinding. -pub(super) fn emit_main_cleanup_callback( - emitter: &mut Emitter, - cleanup_label: &str, - ctx: &Context, -) { - emitter.label(cleanup_label); - abi::emit_cleanup_callback_prologue(emitter, abi::int_arg_reg_name(emitter.target, 0)); - functions::emit_owned_local_epilogue_cleanup(emitter, ctx, cleanup_label); - abi::emit_cleanup_callback_epilogue(emitter); - emitter.blank(); -} - -/// Returns the runtime value tag byte for a PhpType (used in heap header encoding). -pub(crate) fn runtime_value_tag(ty: &PhpType) -> u8 { - match ty { - PhpType::Int => 0, - PhpType::Str => 1, - PhpType::Float => 2, - PhpType::Bool => 3, - PhpType::Array(_) => 4, - PhpType::AssocArray { .. } => 5, - PhpType::Object(_) => 6, - PhpType::Mixed => 7, - PhpType::Union(_) => 7, - PhpType::Iterable => 7, - PhpType::Void => 8, - PhpType::Resource(_) => 9, - PhpType::Callable => 10, - PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) | PhpType::Never => 0, - PhpType::TaggedScalar => { - unreachable!("TaggedScalar carries its runtime tag in the tag register, not a static tag") - } - } -} - -/// Boxes raw register-based value components into a runtime Mixed cell via __rt_mixed_from_value. -pub(crate) fn emit_box_runtime_payload_as_mixed( - emitter: &mut Emitter, - value_tag_reg: &str, - value_lo_reg: &str, - value_hi_reg: &str, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, {}", value_tag_reg)); // x0 = runtime value tag for the mixed boxing helper - emitter.instruction(&format!("mov x1, {}", value_lo_reg)); // x1 = low payload word for the mixed boxing helper - emitter.instruction(&format!("mov x2, {}", value_hi_reg)); // x2 = high payload word for the mixed boxing helper - emitter.instruction("bl __rt_mixed_from_value"); // retain/persist the payload as needed and return a boxed mixed cell - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", value_tag_reg)); // rax = runtime value tag for the mixed boxing helper - emitter.instruction(&format!("mov rdi, {}", value_lo_reg)); // rdi = low payload word for the mixed boxing helper - emitter.instruction(&format!("mov rsi, {}", value_hi_reg)); // rsi = high payload word for the mixed boxing helper - emitter.instruction("call __rt_mixed_from_value"); // box the payload into a temporary mixed cell on x86_64 - } - } -} - -/// Boxes the current expression result in the ABI result registers (x0/d0 or rax) into -/// a runtime Mixed cell, dispatching on the PHP type to emit the appropriate tag and -/// payload word setup. Ownership is not tracked here; callers must ensure the value -/// is safe to box (e.g., not a borrowed temporary that may be invalidated). -pub(crate) fn emit_box_current_value_as_mixed(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Mixed | PhpType::Union(_) => {} - PhpType::Iterable => emit_box_iterable_as_mixed(emitter), - PhpType::TaggedScalar => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x9, x0"); // stage the tagged scalar payload while the tag moves into the helper tag register - emitter.instruction("mov x0, x1"); // pass the dynamic runtime tag as the mixed boxing helper tag argument - emitter.instruction("mov x1, x9"); // pass the tagged scalar payload as the mixed boxing helper low word - emitter.instruction("mov x2, xzr"); // tagged scalar payloads do not use a second word - emitter.instruction("bl __rt_mixed_from_value"); // box the tagged scalar payload into a mixed cell - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // pass the tagged scalar payload as the mixed boxing helper low word - emitter.instruction("mov rax, rdx"); // pass the dynamic runtime tag as the mixed boxing helper tag argument - emitter.instruction("xor rsi, rsi"); // tagged scalar payloads do not use a second word - emitter.instruction("call __rt_mixed_from_value"); // box the tagged scalar payload into a mixed cell - } - }, - PhpType::Int | PhpType::Bool | PhpType::Void | PhpType::Never | PhpType::Resource(_) => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the current scalar payload into the mixed helper argument register - emitter.instruction("mov x2, xzr"); // scalar mixed payloads do not use a second word - emitter.instruction(&format!("mov x0, #{}", runtime_value_tag(ty))); // materialize the static value tag for this scalar - emitter.instruction("bl __rt_mixed_from_value"); // box the scalar payload into a mixed cell - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the current scalar payload into the mixed helper low-word register - emitter.instruction("xor rsi, rsi"); // scalar mixed payloads do not use a second word - abi::emit_load_int_immediate(emitter, "rax", runtime_value_tag(ty) as i64); - emitter.instruction("call __rt_mixed_from_value"); // box the scalar payload into a mixed cell - } - }, - PhpType::Float => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmov x1, d0"); // move the current float bits into the mixed helper payload register - emitter.instruction("mov x2, xzr"); // float payloads only use the low word - emitter.instruction("mov x0, #2"); // runtime tag 2 = float - emitter.instruction("bl __rt_mixed_from_value"); // box the float payload into a mixed cell - } - Arch::X86_64 => { - emitter.instruction("movq rdi, xmm0"); // move the current float bits into the mixed helper payload register - emitter.instruction("xor rsi, rsi"); // float payloads only use the low word - abi::emit_load_int_immediate(emitter, "rax", 2); - emitter.instruction("call __rt_mixed_from_value"); // box the float payload into a mixed cell - } - }, - PhpType::Str => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // runtime tag 1 = string - emitter.instruction("bl __rt_mixed_from_value"); // persist the string payload and box it into a mixed cell - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the current string pointer into the mixed helper low-word register - emitter.instruction("mov rsi, rdx"); // move the current string length into the mixed helper high-word register - abi::emit_load_int_immediate(emitter, "rax", 1); - emitter.instruction("call __rt_mixed_from_value"); // box the string payload into a mixed cell - } - }, - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the current heap pointer into the mixed helper payload register - emitter.instruction("mov x2, xzr"); // heap-backed payloads only use the low word - emitter.instruction(&format!("mov x0, #{}", runtime_value_tag(ty))); // materialize the heap payload tag for the mixed helper - emitter.instruction("bl __rt_mixed_from_value"); // retain the heap child and box it into a mixed cell - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the current heap pointer into the mixed helper payload register - emitter.instruction("xor rsi, rsi"); // heap-backed payloads only use the low word - abi::emit_load_int_immediate(emitter, "rax", runtime_value_tag(ty) as i64); - emitter.instruction("call __rt_mixed_from_value"); // box the heap child into a mixed cell - } - } - } - PhpType::Callable => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the callable descriptor into the mixed helper payload register - emitter.instruction("mov x2, xzr"); // callable descriptor payloads only use the low word - emitter.instruction("mov x0, #10"); // runtime tag 10 = callable descriptor - emitter.instruction("bl __rt_mixed_from_value"); // retain the callable descriptor and box it into a mixed cell - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the callable descriptor into the mixed helper payload register - emitter.instruction("xor rsi, rsi"); // callable descriptor payloads only use the low word - abi::emit_load_int_immediate(emitter, "rax", 10); - emitter.instruction("call __rt_mixed_from_value"); // retain the callable descriptor and box it into a mixed cell - } - }, - PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the raw pointer into the mixed helper payload register - emitter.instruction("mov x2, xzr"); // raw pointers only use the low word - emitter.instruction("mov x0, #0"); // treat unsupported raw pointers as integer-like payloads for now - emitter.instruction("bl __rt_mixed_from_value"); // box the raw pointer bits into a mixed cell - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // move the raw pointer into the mixed helper payload register - emitter.instruction("xor rsi, rsi"); // raw pointers only use the low word - abi::emit_load_int_immediate(emitter, "rax", 0); - emitter.instruction("call __rt_mixed_from_value"); // box the raw pointer bits into a mixed cell - } - } - } - } -} - -/// Boxes the current expression result as Mixed, applying ownership-aware handling for containers. -pub(crate) fn emit_box_current_expr_value_as_mixed_for_container( - emitter: &mut Emitter, - expr: &Expr, - ty: &PhpType, -) { - if !matches!( - ty, - PhpType::Str - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Object(_) - | PhpType::Callable - ) || expr_result_heap_ownership(expr) != HeapOwnership::Owned - { - emit_box_current_value_as_mixed(emitter, ty); - return; - } - - match ty { - PhpType::Str => emit_box_current_owned_string_as_mixed(emitter), - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) | PhpType::Callable => { - emit_box_current_owned_refcounted_as_mixed_for_container(emitter, ty); - } - _ => emit_box_current_value_as_mixed(emitter, ty), - } -} - -/// Releases the pushed temporary refcounted value after an array push operation. -pub(crate) fn emit_release_pushed_refcounted_temp_after_array_push( - emitter: &mut Emitter, - ty: &PhpType, -) { - if !ty.is_refcounted() { - return; - } - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the updated array pointer while releasing the pushed temporary - emitter.instruction("ldr x0, [sp, #16]"); // reload the pushed temporary pointer saved below the array result - abi::emit_decref_if_refcounted(emitter, ty); - emitter.instruction("ldr x0, [sp], #16"); // restore the updated array pointer after releasing the pushed temporary - emitter.instruction("add sp, sp, #16"); // discard the saved pushed temporary pointer - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve a temporary slot for the updated array pointer - emitter.instruction("mov QWORD PTR [rsp], rax"); // preserve the updated array pointer while releasing the pushed temporary - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the pushed temporary pointer saved below the array result - abi::emit_decref_if_refcounted(emitter, ty); - emitter.instruction("mov rax, QWORD PTR [rsp]"); // restore the updated array pointer after releasing the pushed temporary - emitter.instruction("add rsp, 32"); // discard the array-result slot and the pushed temporary slot - } - } -} - -/// Boxes an owned current result into Mixed and releases the original owner afterward. -pub(crate) fn emit_box_current_owned_value_as_mixed(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Str => emit_box_current_owned_string_as_mixed(emitter), - PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Iterable - | PhpType::Object(_) - | PhpType::Callable => { - emit_box_current_owned_refcounted_as_mixed_for_container(emitter, ty); - } - _ => emit_box_current_value_as_mixed(emitter, ty), - } -} - -/// Transfers the owned string result into a freshly allocated Mixed string cell. -/// -/// Unlike `__rt_mixed_from_value`, this path does not persist another copy of -/// the payload. The caller has already proven the string is heap-owned and that -/// the Mixed box is the new owner. -fn emit_box_current_owned_string_as_mixed(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the owned string payload while allocating the Mixed cell - emitter.instruction("mov x0, #24"); // mixed cells store tag plus two payload words - emitter.instruction("bl __rt_heap_alloc"); // allocate a fresh Mixed cell payload - emitter.instruction("mov x9, #5"); // heap kind 5 = boxed Mixed cell - emitter.instruction("str x9, [x0, #-8]"); // stamp the Mixed heap header - emitter.instruction("mov x10, #1"); // runtime tag 1 = string - emitter.instruction("str x10, [x0]"); // store the string runtime tag - emitter.instruction("ldp x11, x12, [sp], #16"); // restore the transferred string pointer and length - emitter.instruction("stp x11, x12, [x0, #8]"); // move the string payload into the Mixed cell - } - Arch::X86_64 => { - emitter.instruction("sub rsp, 16"); // reserve spill space for the owned string payload - emitter.instruction("mov QWORD PTR [rsp], rax"); // preserve the owned string pointer while allocating the Mixed cell - emitter.instruction("mov QWORD PTR [rsp + 8], rdx"); // preserve the owned string length while allocating the Mixed cell - emitter.instruction("mov rax, 24"); // mixed cells store tag plus two payload words - emitter.instruction("call __rt_heap_alloc"); // allocate a fresh Mixed cell payload - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5)); // materialize the Mixed heap marker - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the Mixed heap header - emitter.instruction("mov QWORD PTR [rax], 1"); // store runtime tag 1 = string - emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the transferred string pointer - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // move the string pointer into the Mixed payload - emitter.instruction("mov r10, QWORD PTR [rsp + 8]"); // reload the transferred string length - emitter.instruction("mov QWORD PTR [rax + 16], r10"); // move the string length into the Mixed payload - emitter.instruction("add rsp, 16"); // discard the temporary string payload spill - } - } -} - -/// Boxes an owned refcounted value from the result register into a Mixed cell while -/// preserving the original heap pointer, boxing it, releasing the original via -/// decref, and restoring the boxed result. Used for owned arrays, iterables, -/// objects, and callables that must be transferred into a Mixed container without -/// double-freeing. -fn emit_box_current_owned_refcounted_as_mixed_for_container(emitter: &mut Emitter, ty: &PhpType) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the owned source heap value while boxing it into Mixed - emit_box_current_value_as_mixed(emitter, ty); - emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed Mixed result while releasing the original owner - emitter.instruction("ldr x0, [sp, #16]"); // reload the original heap value retained by the Mixed box - abi::emit_decref_if_refcounted(emitter, ty); - emitter.instruction("ldr x0, [sp], #16"); // restore the boxed Mixed result - emitter.instruction("add sp, sp, #16"); // discard the saved original heap value pointer - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); - emit_box_current_value_as_mixed(emitter, ty); - abi::emit_push_reg(emitter, "rax"); - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the original heap value retained by the Mixed box - abi::emit_decref_if_refcounted(emitter, ty); - abi::emit_pop_reg(emitter, "rax"); - emitter.instruction("add rsp, 16"); // discard the saved original heap value pointer - } - } -} - -/// Converts an Iterable to Mixed, returning true if the conversion was applied. -pub(crate) fn emit_box_iterable_value_for_mixed_container( - emitter: &mut Emitter, - ty: &mut PhpType, -) -> bool { - if !matches!(ty, PhpType::Iterable) { - return false; - } - emit_box_iterable_as_mixed(emitter); - *ty = PhpType::Mixed; - true -} - -/// Probes the iterable's heap kind via `__rt_heap_kind`, maps it to the corresponding -/// Mixed tag (array→4, assoc→5, object→6), and boxes the iterable into a Mixed cell -/// via `__rt_mixed_from_value`. Preserves the iterable pointer across the kind probe -/// using a stack spill slot. Falls back to mixed tag 8 for unknown kinds. -fn emit_box_iterable_as_mixed(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("str x0, [sp, #-16]!"); // preserve the iterable heap pointer while probing its concrete heap kind - emitter.instruction("bl __rt_heap_kind"); // classify the raw iterable pointer by its heap-kind tag - emitter.instruction("mov x9, x0"); // keep the heap kind available for tag normalization - emitter.instruction("cmp x0, #2"); // is the heap kind at least the indexed-array tag? - emitter.instruction("cset x10, hs"); // record whether the iterable is in the supported heap-backed range lower bound - emitter.instruction("cmp x0, #4"); // is the heap kind no greater than the object tag? - emitter.instruction("cset x11, ls"); // record whether the iterable is in the supported heap-backed range upper bound - emitter.instruction("and x10, x10, x11"); // combine the lower and upper bound checks into one predicate - emitter.instruction("add x9, x9, #2"); // map heap kind 2/3/4 to mixed tag 4/5/6 - emitter.instruction("mov x0, #8"); // default unknown iterable payloads to the null mixed tag - emitter.instruction("cmp x10, #0"); // did the heap kind fall inside the supported iterable range? - emitter.instruction("csel x0, x9, x0, ne"); // choose the mapped concrete mixed tag when the range check succeeded - emitter.instruction("ldr x1, [sp], #16"); // restore the iterable heap pointer as the mixed payload low word - emitter.instruction("mov x2, xzr"); // iterable payloads do not use a high payload word - emitter.instruction("bl __rt_mixed_from_value"); // retain the concrete heap payload and return an owned mixed cell - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the iterable heap pointer while probing its concrete heap kind - emitter.instruction("call __rt_heap_kind"); // classify the raw iterable pointer by its heap-kind tag - emitter.instruction("mov r10, rax"); // keep the heap kind available for tag normalization - emitter.instruction("cmp rax, 2"); // is the heap kind at least the indexed-array tag? - emitter.instruction("setae r11b"); // record whether the iterable is in the supported heap-backed range lower bound - emitter.instruction("cmp rax, 4"); // is the heap kind no greater than the object tag? - emitter.instruction("setbe dl"); // record whether the iterable is in the supported heap-backed range upper bound - emitter.instruction("and dl, r11b"); // combine the lower and upper bound checks into one predicate byte - emitter.instruction("add r10, 2"); // map heap kind 2/3/4 to mixed tag 4/5/6 - emitter.instruction("mov rax, 8"); // default unknown iterable payloads to the null mixed tag - emitter.instruction("test dl, dl"); // did the heap kind fall inside the supported iterable range? - emitter.instruction("cmovne rax, r10"); // choose the mapped concrete mixed tag when the range check succeeded - abi::emit_pop_reg(emitter, "rdi"); // restore the iterable heap pointer as the mixed payload low word - emitter.instruction("xor rsi, rsi"); // iterable payloads do not use a high payload word - emitter.instruction("call __rt_mixed_from_value"); // retain the concrete heap payload and return an owned mixed cell - } - } -} - -/// Emits code to normalize an array key expression into the hash ABI (key_lo, key_hi registers). -pub(crate) fn emit_normalized_hash_key( - expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let key_ty = emit_expr(expr, emitter, ctx, data).codegen_repr(); - match &key_ty { - PhpType::Int | PhpType::Bool => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // move the integer array key payload into the normalized key low word - emitter.instruction("mov x2, #-1"); // key_hi sentinel marks the associative-array key as integer - } - Arch::X86_64 => { - emitter.instruction("mov rdx, -1"); // key_hi sentinel marks the associative-array key as integer while rax keeps key_lo - } - }, - PhpType::Float => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fcvtzs x1, d0"); // PHP casts float array keys to integer keys - emitter.instruction("mov x2, #-1"); // key_hi sentinel marks the associative-array key as integer - } - Arch::X86_64 => { - emitter.instruction("cvttsd2si rax, xmm0"); // PHP casts float array keys to integer keys - emitter.instruction("mov rdx, -1"); // key_hi sentinel marks the associative-array key as integer - } - }, - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_hash_normalize_key"); // normalize numeric-string array keys to their integer PHP form - } - PhpType::Mixed | PhpType::Union(_) => { - let string_key = ctx.next_label("mixed_hash_key_string"); - let scalar_key = ctx.next_label("mixed_hash_key_scalar"); - let done = ctx.next_label("mixed_hash_key_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("bl __rt_mixed_unbox"); // decode the boxed key before normalizing it for hash storage - emitter.instruction("cmp x0, #1"); // string mixed keys need PHP numeric-string normalization - emitter.instruction(&format!("b.eq {}", string_key)); // route string keys through the normal hash-key helper - emitter.instruction("cmp x0, #0"); // integer mixed keys are already scalar hash keys - emitter.instruction(&format!("b.eq {}", scalar_key)); // keep integer keys as integer hash keys - emitter.instruction("cmp x0, #3"); // boolean mixed keys normalize like integer keys - emitter.instruction(&format!("b.eq {}", scalar_key)); // keep boolean keys as integer hash keys - emitter.instruction("mov x1, #0"); // unsupported mixed key tags fall back to integer key zero - emitter.label(&scalar_key); - emitter.instruction("mov x2, #-1"); // key_hi sentinel marks scalar mixed keys as integers - emitter.instruction(&format!("b {}", done)); // skip the string-key normalization path - emitter.label(&string_key); - emitter.instruction("bl __rt_hash_normalize_key"); // normalize string mixed keys to PHP int/string hash keys - emitter.label(&done); - } - Arch::X86_64 => { - emitter.instruction("call __rt_mixed_unbox"); // decode the boxed key before normalizing it for hash storage - emitter.instruction("cmp rax, 1"); // string mixed keys need PHP numeric-string normalization - emitter.instruction(&format!("je {}", string_key)); // route string keys through the normal hash-key helper - emitter.instruction("cmp rax, 0"); // integer mixed keys are already scalar hash keys - emitter.instruction(&format!("je {}", scalar_key)); // keep integer keys as integer hash keys - emitter.instruction("cmp rax, 3"); // boolean mixed keys normalize like integer keys - emitter.instruction(&format!("je {}", scalar_key)); // keep boolean keys as integer hash keys - emitter.instruction("xor eax, eax"); // unsupported mixed key tags fall back to integer key zero - emitter.instruction("mov rdx, -1"); // key_hi sentinel marks fallback mixed keys as integers - emitter.instruction(&format!("jmp {}", done)); // skip the string-key normalization path - emitter.label(&scalar_key); - emitter.instruction("mov rax, rdi"); // publish the unboxed scalar payload as key_lo - emitter.instruction("mov rdx, -1"); // key_hi sentinel marks scalar mixed keys as integers - emitter.instruction(&format!("jmp {}", done)); // skip the string-key normalization path - emitter.label(&string_key); - emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the hash normalizer input - emitter.instruction("call __rt_hash_normalize_key"); // normalize string mixed keys to PHP int/string hash keys - emitter.label(&done); - } - } - } - _ => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // treat unsupported key payloads as integer-like low words for the hash ABI - emitter.instruction("mov x2, #-1"); // key_hi sentinel marks the associative-array key as integer - } - Arch::X86_64 => { - emitter.instruction("mov rdx, -1"); // treat unsupported key payloads as integer-like low words for the hash ABI - } - }, - } - key_ty -} - -/// Rounds `n` up to the nearest 16-byte boundary. Used to align stack frame sizes -/// and heap allocation sizes to the 16-byte ABI requirement on both AArch64 and x86_64. -pub(super) fn align16(n: usize) -> usize { - (n + 15) & !15 -} - -/// Materializes an immediate i64 value into the given register via the target-aware -/// ABI helper (`emit_load_int_immediate`). Handles large immediates that may require -/// multiple instructions on the target architecture. -fn load_immediate(emitter: &mut Emitter, reg: &str, value: i64) { - abi::emit_load_int_immediate(emitter, reg, value); // materialize the immediate through the shared target-aware helper -} diff --git a/src/codegen/emit.rs b/src/codegen/emit.rs deleted file mode 100644 index a587c15857..0000000000 --- a/src/codegen/emit.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Purpose: -//! Owns the assembly text builder and target-aware syntax helpers used by all emitters. -//! Centralizes labels, directives, relocation forms, comments, and raw text output. -//! -//! Called from: -//! - `crate::codegen::generate()` and all `crate::codegen::*` emitters -//! -//! Key details: -//! - Instruction comments are emitted by callers; this module preserves target syntax and output ordering. - -use std::fmt::Write; - -use super::platform::{Arch, Platform, Target}; - -/// Assembly emitter. -pub struct Emitter { - buf: String, - pub target: Target, - pub platform: Platform, - /// When `true`, the `emit_*_symbol_*` helpers in `codegen::abi::symbols` - /// route global-symbol references through the GOT (`@GOTPCREL` on x86_64, - /// `:got:` + `:got_lo12:` on AArch64) instead of using direct PC-relative - /// addressing. Required for shared-library output, where the loader cannot - /// resolve cross-object `R_X86_64_PC32` relocations at dlopen time. - pub pic_data_refs: bool, -} - -impl Emitter { - /// Creates an emitter for the specified target platform. - pub fn new(target: Target) -> Self { - Self { - buf: String::with_capacity(4096), - target, - platform: target.platform, - pic_data_refs: false, - } - } - - /// Returns a new emitter configured for position-independent data - /// references. Used by `--emit cdylib` so global symbol accesses survive - /// dynamic loading as a shared object. - pub fn new_pic(target: Target) -> Self { - let mut emitter = Self::new(target); - emitter.pic_data_refs = true; - emitter - } - - /// Emits a single assembly instruction with standard indentation. - pub fn instruction(&mut self, instr: &str) { - let _ = writeln!(self.buf, " {}", instr); - } - - /// Emits a local label (name:). - pub fn label(&mut self, name: &str) { - let _ = writeln!(self.buf, "{}:", name); - } - - /// Emit a label that is visible across object files (for two-object linking). - pub fn label_global(&mut self, name: &str) { - let _ = writeln!(self.buf, ".globl {}", name); - let _ = writeln!(self.buf, "{}:", name); - } - - /// Emits a line comment using the target's comment prefix. - pub fn comment(&mut self, text: &str) { - let _ = writeln!( - self.buf, - " {} {}", - self.target.line_comment_prefix(), - text - ); - } - - /// Emits a blank line for visual separation. - pub fn blank(&mut self) { - self.buf.push('\n'); - } - - /// Emits raw text directly to the output buffer without formatting. - pub fn raw(&mut self, text: &str) { - self.buf.push_str(text); - self.buf.push('\n'); - } - - /// Emits the .text section prelude, including Intel syntax switch for x86_64. - pub fn emit_text_prelude(&mut self) { - if self.target.arch == Arch::X86_64 { - self.raw(".intel_syntax noprefix"); - } - self.raw(".text"); - } - - /// Returns the accumulated assembly output as a String. - pub fn output(self) -> String { - self.buf - } - - // ── Platform-aware relocation helpers ───────────────────────────── - - /// Emit `adrp reg, sym@PAGE` (macOS) or `adrp reg, sym` (Linux). - pub fn adrp(&mut self, reg: &str, sym: &str) { - self.target - .ensure_aarch64_backend("adrp relocation emission"); - match self.platform { - Platform::MacOS => self.instruction(&format!("adrp {}, {}@PAGE", reg, sym)), - Platform::Linux => self.instruction(&format!("adrp {}, {}", reg, sym)), - } - } - - /// Emit `add dst, src, sym@PAGEOFF` (macOS) or `add dst, src, :lo12:sym` (Linux). - pub fn add_lo12(&mut self, dst: &str, src: &str, sym: &str) { - self.target - .ensure_aarch64_backend("lo12 relocation emission"); - match self.platform { - Platform::MacOS => self.instruction(&format!("add {}, {}, {}@PAGEOFF", dst, src, sym)), - Platform::Linux => self.instruction(&format!("add {}, {}, :lo12:{}", dst, src, sym)), - } - } - - /// Emit `ldr reg, [base, sym@PAGEOFF]` (macOS) or `ldr reg, [base, :lo12:sym]` (Linux). - pub fn ldr_lo12(&mut self, reg: &str, base: &str, sym: &str) { - self.target.ensure_aarch64_backend("lo12 load emission"); - match self.platform { - Platform::MacOS => { - self.instruction(&format!("ldr {}, [{}, {}@PAGEOFF]", reg, base, sym)) - } - Platform::Linux => self.instruction(&format!("ldr {}, [{}, :lo12:{}]", reg, base, sym)), - } - } - - /// Emit `adrp reg, sym@GOTPAGE` (macOS) or `adrp reg, :got:sym` (Linux). - pub fn adrp_got(&mut self, reg: &str, sym: &str) { - self.target - .ensure_aarch64_backend("GOT page relocation emission"); - match self.platform { - Platform::MacOS => self.instruction(&format!("adrp {}, {}@GOTPAGE", reg, sym)), - Platform::Linux => self.instruction(&format!("adrp {}, :got:{}", reg, sym)), - } - } - - /// Emit `ldr reg, [base, sym@GOTPAGEOFF]` (macOS) or `ldr reg, [base, :got_lo12:sym]` (Linux). - pub fn ldr_got_lo12(&mut self, reg: &str, base: &str, sym: &str) { - self.target.ensure_aarch64_backend("GOT lo12 load emission"); - match self.platform { - Platform::MacOS => { - self.instruction(&format!("ldr {}, [{}, {}@GOTPAGEOFF]", reg, base, sym)) - } - Platform::Linux => { - self.instruction(&format!("ldr {}, [{}, :got_lo12:{}]", reg, base, sym)) - } - } - } - - // ── Platform-aware syscall helper ───────────────────────────────── - - /// Emit a complete syscall sequence: sets the syscall register and traps. - /// On macOS: `mov x16, #N` + `svc #0x80`. - /// On Linux: optional AT_FDCWD arg shift + `mov x8, #M` + `svc #0`. - pub fn syscall(&mut self, macos_num: u32) { - self.target.ensure_aarch64_backend("syscall emission"); - match self.platform { - Platform::MacOS => { - self.instruction(&format!("mov x16, #{}", macos_num)); - self.instruction("svc #0x80"); - } - Platform::Linux => { - let target = self.target; - target.emit_linux_syscall(self, macos_num); - } - } - } - - // ── Platform-aware C symbol call ───────────────────────────────── - - /// Emit `bl _func` (macOS) or `bl func` (Linux) for C library calls. - pub fn bl_c(&mut self, func: &str) { - match (self.platform, self.target.arch) { - (Platform::MacOS, Arch::AArch64) => self.instruction(&format!("bl _{}", func)), - (Platform::Linux, Arch::AArch64) => self.instruction(&format!("bl {}", func)), - (Platform::Linux, Arch::X86_64) => self.instruction(&format!("call {}", func)), - (Platform::MacOS, Arch::X86_64) => { - panic!("C symbol calls are not implemented yet for target macos-x86_64"); - } - } - } - - // ── Platform-aware entry point ─────────────────────────────────── - - /// Emit the program entry point label: `_main` (macOS) or `main` (Linux). - pub fn entry_label(&mut self) { - match self.target.arch { - Arch::AArch64 => match self.platform { - Platform::MacOS => self.label_global("_main"), - Platform::Linux => self.label_global("main"), - }, - Arch::X86_64 => self.label_global("main"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Verifies comment prefix is platform aware. - #[test] - fn test_comment_prefix_is_platform_aware() { - let mut mac = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); - mac.comment("-- block --"); - assert_eq!(mac.output(), " ; -- block --\n"); - - let mut linux = Emitter::new(Target::new(Platform::Linux, Arch::AArch64)); - linux.comment("-- block --"); - assert_eq!(linux.output(), " // -- block --\n"); - - let mut linux_x86 = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); - linux_x86.comment("-- block --"); - assert_eq!(linux_x86.output(), " # -- block --\n"); - } - - /// Verifies text prelude switches x86 to intel syntax. - #[test] - fn test_text_prelude_switches_x86_to_intel_syntax() { - let mut mac = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); - mac.emit_text_prelude(); - assert_eq!(mac.output(), ".text\n"); - - let mut linux_x86 = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); - linux_x86.emit_text_prelude(); - assert_eq!(linux_x86.output(), ".intel_syntax noprefix\n.text\n"); - } -} diff --git a/src/codegen/expr.rs b/src/codegen/expr.rs deleted file mode 100644 index f62357b54c..0000000000 --- a/src/codegen/expr.rs +++ /dev/null @@ -1,425 +0,0 @@ -//! Purpose: -//! Dispatches expression AST nodes into focused lowering modules and shared coercion helpers. -//! Defines result conventions for scalars, strings, arrays, objects, calls, and special PHP operators. -//! -//! Called from: -//! - `crate::codegen::stmt`, `crate::codegen::functions`, and top-level emission -//! -//! Key details: -//! - Each expression leaves its result in the type-specific ABI result registers expected by callers. - -pub(crate) mod arrays; -mod assignment; -mod binops; -mod chains; -/// calls -pub(crate) mod calls; -mod coerce; -mod compare; -mod diagnostics; -mod helpers; -/// objects -pub(crate) mod objects; -mod ownership; -mod scalars; -mod ternary; -mod variables; - -use super::abi; -use super::context::Context; -use super::data_section::DataSection; -use super::emit::Emitter; -use crate::parser::ast::{BinOp, Expr, ExprKind}; -use crate::types::PhpType; - -pub(crate) use helpers::{can_coerce_result_to_type, coerce_result_to_type}; -pub(crate) use objects::{emit_method_call_with_pushed_args, push_magic_property_name_arg}; -pub(crate) use ownership::{ - expr_result_heap_ownership, string_result_is_owned_call_temp, - string_result_uses_transient_concat_buffer, -}; -pub use coerce::{ - coerce_null_to_zero, coerce_to_int, coerce_to_string, coerce_to_string_releasing_owned, - coerce_to_truthiness, -}; -use helpers::{retain_borrowed_heap_arg, widen_codegen_type}; - -/// Dispatches an expression AST node to the appropriate lowering module. - /// - /// Returns the resulting `PhpType` after code generation. Result values follow - /// target ABI conventions: integers in `x0`, floats in `d0`, strings in `x1` (ptr) - /// and `x2` (len). For expressions that emit no value (e.g., `Throw`), returns the - /// bottom type for the context. - /// - /// Handles nullsafe chains first via `chains::emit_nullsafe_postfix_chain` before - /// falling through to the standard dispatch table. All other `ExprKind` variants - /// are delegated to their respective submodules. -pub fn emit_expr( - expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if let Some(ty) = chains::emit_nullsafe_postfix_chain(expr, emitter, ctx, data) { - return ty; - } - - 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::BoolLiteral(b) => { - scalars::emit_bool_literal(*b, emitter) - } - ExprKind::Null => { - scalars::emit_null_literal(emitter) - } - ExprKind::StringLiteral(s) => { - scalars::emit_string_literal(s, emitter, data) - } - ExprKind::IntLiteral(n) => { - scalars::emit_int_literal(*n, emitter) - } - ExprKind::FloatLiteral(f) => { - scalars::emit_float_literal(*f, emitter, data) - } - ExprKind::Variable(name) => { - variables::emit_variable(name, emitter, ctx) - } - ExprKind::Negate(inner) => { - scalars::emit_negate(inner, emitter, ctx, data) - } - ExprKind::ArrayLiteral(elems) => arrays::emit_array_literal(elems, emitter, ctx, data), - ExprKind::ArrayLiteralAssoc(pairs) if pairs.is_empty() => { - arrays::emit_empty_assoc_array_literal(PhpType::Mixed, PhpType::Mixed, emitter) - } - ExprKind::ArrayLiteralAssoc(pairs) => { - arrays::emit_assoc_array_literal(pairs, emitter, ctx, data) - } - ExprKind::Match { - subject, - arms, - default, - } => arrays::emit_match_expr(subject, arms, default, emitter, ctx, data), - ExprKind::ArrayAccess { array, index } => { - arrays::emit_array_access(array, index, emitter, ctx, data) - } - ExprKind::BufferNew { element_type, len } => { - arrays::emit_buffer_new(element_type, len, emitter, ctx, data) - } - ExprKind::Not(inner) => { - scalars::emit_not(inner, emitter, ctx, data) - } - ExprKind::BitNot(inner) => { - scalars::emit_bit_not(inner, emitter, ctx, data) - } - ExprKind::Throw(inner) => { - variables::emit_throw(inner, emitter, ctx, data) - } - ExprKind::ErrorSuppress(inner) => { - diagnostics::emit_error_suppress(inner, emitter, ctx, data) - } - ExprKind::Print(inner) => { - emit_print_expr(inner, emitter, ctx, data) - } - ExprKind::NullCoalesce { value, default } => { - compare::emit_null_coalesce(value, default, emitter, ctx, data) - } - ExprKind::Pipe { value, callable } => { - calls::emit_pipe(value, callable, expr.span, emitter, ctx, data) - } - ExprKind::Assignment { - target, - value, - result_target, - prelude, - conditional_value_temp, - } => { - assignment::emit_assignment_expr( - target, - value, - result_target.as_deref(), - prelude, - conditional_value_temp.as_deref(), - emitter, - ctx, - data, - ) - } - ExprKind::PreIncrement(name) => { - variables::emit_pre_increment(name, emitter, ctx) - } - ExprKind::PostIncrement(name) => { - variables::emit_post_increment(name, emitter, ctx) - } - ExprKind::PreDecrement(name) => { - variables::emit_pre_decrement(name, emitter, ctx) - } - ExprKind::PostDecrement(name) => { - variables::emit_post_decrement(name, emitter, ctx) - } - ExprKind::Ternary { - condition, - then_expr, - else_expr, - } => ternary::emit_ternary(condition, then_expr, else_expr, emitter, ctx, data), - ExprKind::ShortTernary { value, default } => { - ternary::emit_short_ternary(value, default, emitter, ctx, data) - } - ExprKind::Cast { target, expr } => compare::emit_cast(target, expr, emitter, ctx, data), - ExprKind::FunctionCall { name, args } => { - if ctx.extern_functions.contains_key(name.as_str()) { - return super::ffi::emit_extern_call(name.as_str(), args, expr.span, emitter, ctx, data); - } - if let Some(ty) = - super::builtins::emit_builtin_call(name.as_str(), args, expr.span, emitter, ctx, data) - { - return ty; - } - calls::emit_function_call(name.as_str(), args, emitter, ctx, data) - } - ExprKind::Closure { - params, - return_type, - body, - is_arrow: _, - is_static: _, - variadic, - variadic_type: _, - captures, - capture_refs, - by_ref_return: _, - } => calls::emit_closure( - params, - variadic, - return_type, - body, - captures, - capture_refs, - emitter, - ctx, - data, - ), - ExprKind::FirstClassCallable(target) => { - calls::emit_first_class_callable(target, emitter, ctx, data) - } - ExprKind::ClosureCall { var, args } => { - calls::emit_closure_call(var, args, emitter, ctx, data) - } - ExprKind::ExprCall { callee, args } => { - if let Some(ret_ty) = - calls::emit_callable_array_literal_call(callee, args, emitter, ctx, data) - { - return ret_ty; - } - let loaded_callee_ty = emit_expr(callee, emitter, ctx, data); - calls::emit_loaded_expr_call(callee, args, &loaded_callee_ty, emitter, ctx, data) - } - ExprKind::ConstRef(name) => { - let (value, ty) = match ctx.constants.get(name.as_str()) { - Some(c) => c.clone(), - None => { - emitter.comment(&format!("WARNING: undefined constant {}", name)); - return PhpType::Int; - } - }; - let is_literal_constant = matches!( - value, - ExprKind::IntLiteral(_) - | ExprKind::FloatLiteral(_) - | ExprKind::StringLiteral(_) - | ExprKind::BoolLiteral(_) - | ExprKind::Null - ); - let synthetic_expr = Expr::new(value, expr.span); - let emitted_ty = emit_expr(&synthetic_expr, emitter, ctx, data); - if is_literal_constant { - ty - } else { - emitted_ty - } - } - ExprKind::BinaryOp { left, op, right } => emit_binop(left, op, right, emitter, ctx, data), - ExprKind::InstanceOf { value, target } => { - objects::emit_instanceof(value, target, emitter, ctx, data) - } - ExprKind::Spread(inner) => { - // Spread is handled at call site / array literal level. - // If we reach here, just evaluate the inner expression. - emit_expr(inner, emitter, ctx, data) - } - ExprKind::NamedArg { value, .. } => emit_expr(value, emitter, ctx, data), - ExprKind::NewObject { class_name, args } => { - objects::emit_new_object(class_name.as_str(), args, emitter, ctx, data) - } - ExprKind::NewDynamic { name_expr, args } => { - objects::emit_new_dynamic(name_expr, args, emitter, ctx, data) - } - ExprKind::NewDynamicObject { - class_name, - fallback_class, - required_parent, - args, - } => objects::emit_new_dynamic_object( - class_name, - fallback_class.as_str(), - required_parent.as_str(), - args, - emitter, - ctx, - data, - ), - ExprKind::PropertyAccess { object, property } => { - objects::emit_property_access(object, property, emitter, ctx, data) - } - ExprKind::DynamicPropertyAccess { object, property } => { - objects::emit_dynamic_property_access(object, property, emitter, ctx, data) - } - ExprKind::NullsafePropertyAccess { object, property } => { - objects::emit_nullsafe_property_access(object, property, emitter, ctx, data) - } - ExprKind::NullsafeDynamicPropertyAccess { object, property } => { - objects::emit_nullsafe_dynamic_property_access(object, property, emitter, ctx, data) - } - ExprKind::StaticPropertyAccess { receiver, property } => { - objects::emit_static_property_access(receiver, property, emitter, ctx, data) - } - ExprKind::MethodCall { - object, - method, - args, - } => objects::emit_method_call(object, method, args, emitter, ctx, data), - ExprKind::NullsafeMethodCall { - object, - method, - args, - } => objects::emit_nullsafe_method_call(object, method, args, emitter, ctx, data), - ExprKind::StaticMethodCall { - receiver, - method, - args, - } => objects::emit_static_method_call(receiver, method, args, emitter, ctx, data), - ExprKind::This => { - variables::emit_this(emitter, ctx) - } - ExprKind::PtrCast { target_type, expr } => { - emitter.comment(&format!("ptr_cast<{}>()", target_type)); - emit_expr(expr, emitter, ctx, data); - // Value stays in x0 unchanged — only the type tag changes - PhpType::Pointer(Some(target_type.clone())) - } - ExprKind::ClassConstant { receiver } => { - objects::emit_class_constant(receiver, emitter, ctx, data) - } - ExprKind::ScopedConstantAccess { receiver, name } => { - objects::emit_scoped_constant_access(receiver, name, emitter, ctx, data) - } - ExprKind::NewScopedObject { receiver, args } => { - objects::emit_new_scoped_object(receiver, args, emitter, ctx, data) - } - ExprKind::Yield { .. } | ExprKind::YieldFrom(_) => { - unreachable!("yield expressions must be lowered by the generator-function codegen path") - } - ExprKind::MagicConstant(_) => { - unreachable!("MagicConstant must be lowered before codegen") - } - } -} - -/// Emits a PHP `print` expression: writes `inner` to stdout and returns integer `1`. - /// - /// PHP print always succeeds and evaluates to `1`. The result is placed in - /// `int_result_reg` per ABI convention. -fn emit_print_expr( - inner: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("print expression"); - super::stmt::emit_expr_to_stdout(inner, emitter, ctx, data); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1); - PhpType::Int -} - -/// Delegates binary operation code generation to `binops::emit_binop`. - /// - /// Returns the `PhpType` produced by the operation, which depends on the operand types - /// and the operator (e.g., int+int → int, int+str → str, etc.). -fn emit_binop( - left: &Expr, - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - binops::emit_binop(left, op, right, emitter, ctx, data) -} - -/// Saves the current concat offset before a nested function call. -/// On ARM64 this pushes the offset onto a temporary stack; on x86_64 it spills -/// the offset into a dedicated frame slot when one is allocated. -pub(crate) fn save_concat_offset_before_nested_call(emitter: &mut Emitter, ctx: &Context) { - let scratch = abi::temp_int_reg(emitter.target); - abi::emit_load_symbol_to_reg(emitter, scratch, "_concat_off", 0); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_push_reg(emitter, scratch); // save caller concat offset across nested call on the temporary stack - } - crate::codegen::platform::Arch::X86_64 => { - if let Some(slot) = ctx.nested_concat_offset_offset { - abi::store_at_offset(emitter, scratch, slot); // spill caller concat offset into the dedicated frame slot so nested x86_64 calls cannot clobber it - } else { - abi::emit_push_reg(emitter, scratch); // fall back to the temporary stack in raw emitter/unit-test contexts that do not allocate hidden frame slots - } - } - } -} - -/// Restores the concat offset after a nested function call returns. -/// If the return type is `Str`, persists the returned string before restoring the offset. -pub(crate) fn restore_concat_offset_after_nested_call( - emitter: &mut Emitter, - ctx: &Context, - return_ty: &PhpType, -) { - restore_concat_offset_after_nested_call_impl(emitter, ctx, *return_ty == PhpType::Str); -} - -/// Restores the concat offset after a call that returns an owned string. -/// Does not persist the string (caller already handles ownership). -pub(crate) fn restore_concat_offset_after_owned_string_call( - emitter: &mut Emitter, - ctx: &Context, -) { - restore_concat_offset_after_nested_call_impl(emitter, ctx, false); -} - -/// Internal implementation for restoring concat offset after a nested call. -/// Optionally persists the returned string before restoring the offset. -fn restore_concat_offset_after_nested_call_impl( - emitter: &mut Emitter, - ctx: &Context, - persist_string_result: bool, -) { - if persist_string_result { - abi::emit_call_label(emitter, "__rt_str_persist"); // persist returned string before restoring caller concat cursor - } - let scratch = abi::temp_int_reg(emitter.target); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_pop_reg(emitter, scratch); // pop the saved caller concat offset from the temporary stack - } - crate::codegen::platform::Arch::X86_64 => { - if let Some(slot) = ctx.nested_concat_offset_offset { - abi::load_at_offset(emitter, scratch, slot); // reload the saved caller concat offset from the dedicated x86_64 frame slot - } else { - abi::emit_pop_reg(emitter, scratch); // fall back to the temporary stack in raw emitter/unit-test contexts that do not allocate hidden frame slots - } - } - } - abi::emit_store_reg_to_symbol(emitter, scratch, "_concat_off", 0); -} diff --git a/src/codegen/expr/arrays.rs b/src/codegen/expr/arrays.rs deleted file mode 100644 index a81a75950d..0000000000 --- a/src/codegen/expr/arrays.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Purpose: -//! Groups array expression lowering for literals, associative arrays, spreads, and element access. -//! Keeps array construction and read paths behind one expression-module interface. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Array values are refcounted heap objects and must preserve ownership across literal and access results. - -mod access; -mod assoc; -mod indexed; - -pub(super) use access::{ - emit_array_access, emit_array_access_with_loaded_base, emit_buffer_new, emit_match_expr, -}; -pub(crate) use assoc::{ - emit_array_literal_as_assoc_target, emit_assoc_array_literal, emit_empty_assoc_array_literal, -}; -pub(super) use indexed::emit_array_literal; -pub(crate) use access::{ - emit_array_access_offset_exists, emit_array_access_offset_set, - emit_array_access_offset_unset, type_is_array_access_object, -}; -pub(crate) use indexed::emit_array_value_type_stamp; diff --git a/src/codegen/expr/arrays/access/buffer.rs b/src/codegen/expr/arrays/access/buffer.rs deleted file mode 100644 index aab23ab89b..0000000000 --- a/src/codegen/expr/arrays/access/buffer.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Purpose: -//! Lowers typed buffer element reads using pointer arithmetic and element sizes. -//! Produces expression results while preserving container ownership and bounds/null behavior. -//! -//! Called from: -//! - `crate::codegen::expr::arrays::access` -//! -//! Key details: -//! - Element layout and boxed Mixed handling must stay aligned with array runtime helpers. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, TypeExpr}; -use crate::types::{packed_type_size, PhpType}; - -/// Allocates a new buffer with `element_type` elements of the given length expression. -/// -/// The length is evaluated first, then the element stride is loaded into the appropriate -/// integer argument register for the target architecture before calling `__rt_buffer_new`. -/// Returns `PhpType::Buffer` wrapping the resolved element type. -pub(crate) fn emit_buffer_new( - element_type: &TypeExpr, - len: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let len_ty = emit_expr(len, emitter, ctx, data); - let elem_ty = resolve_buffer_element_type(element_type, ctx); - let stride = packed_type_size(&elem_ty, &ctx.packed_classes).unwrap_or(8); - if len_ty != PhpType::Int { - emitter.comment("WARNING: buffer_new length was not statically typed as int"); - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x1, #{}", stride)); // pass the element stride to the ARM buffer allocation helper in the second integer argument register - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, {}", stride)); // pass the element stride to the x86_64 buffer allocation helper without clobbering the computed length in rax - } - } - abi::emit_call_label(emitter, "__rt_buffer_new"); // allocate the buffer header plus contiguous payload through the target-aware runtime helper - PhpType::Buffer(Box::new(elem_ty)) -} - -/// Resolves a `TypeExpr` to a `PhpType` for buffer element type checking. -/// -/// Unpacked named types are resolved against the packed class table; unknown named types -/// fall back to `PhpType::Int`. Nullable, union, and iterable types are not valid buffer -/// elements and resolve to `PhpType::Int` as a safe approximation. -fn resolve_buffer_element_type(type_expr: &TypeExpr, ctx: &Context) -> PhpType { - match type_expr { - TypeExpr::Int => PhpType::Int, - TypeExpr::Float => PhpType::Float, - TypeExpr::Bool => PhpType::Bool, - TypeExpr::Never => PhpType::Never, - TypeExpr::Ptr(target) => { - PhpType::Pointer(target.as_ref().map(|name| name.as_str().to_string())) - } - TypeExpr::Named(name) => { - if ctx.packed_classes.contains_key(name.as_str()) { - PhpType::Packed(name.as_str().to_string()) - } else { - PhpType::Int - } - } - TypeExpr::Buffer(inner) => { - PhpType::Buffer(Box::new(resolve_buffer_element_type(inner, ctx))) - } - TypeExpr::Str => PhpType::Str, - TypeExpr::Void => PhpType::Void, - TypeExpr::Array(_) - | TypeExpr::Nullable(_) - | TypeExpr::Union(_) - | TypeExpr::Intersection(_) - | TypeExpr::Iterable => PhpType::Int, - } -} diff --git a/src/codegen/expr/arrays/access/indexed.rs b/src/codegen/expr/arrays/access/indexed.rs deleted file mode 100644 index a6ba134ce1..0000000000 --- a/src/codegen/expr/arrays/access/indexed.rs +++ /dev/null @@ -1,611 +0,0 @@ -//! Purpose: -//! Lowers indexed and associative array element reads including nullable and Mixed results. -//! Produces expression results while preserving container ownership and bounds/null behavior. -//! -//! Called from: -//! - `crate::codegen::expr::arrays::access` -//! -//! Key details: -//! - Element layout and boxed Mixed handling must stay aligned with array runtime helpers. - -use crate::codegen::abi; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::emit_box_runtime_payload_as_mixed; -use crate::codegen::expr::{coerce_result_to_type, emit_expr}; -use crate::codegen::platform::Arch; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::object; -use super::string_offset::emit_string_offset_index; - -/// Emits indexed or associative array element read with nullable/Mixed result support. -/// Dispatches to `object::emit_offset_get` for ArrayAccess objects, otherwise evaluates -/// the array expression and routes to `emit_array_access_with_loaded_base` with the -/// resulting type. -/// -/// Returns the element type of the accessed value (including Mixed for nullable/boxed bases). -pub(crate) fn emit_array_access( - array: &Expr, - index: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if object::expr_is_array_access_object(array, ctx) { - return object::emit_offset_get(array, index, emitter, ctx, data); - } - - let arr_ty = emit_expr(array, emitter, ctx, data); - emit_array_access_with_loaded_base(&arr_ty, index, emitter, ctx, data, false) -} - -/// Emits array access with an already-loaded base type, supporting buffers and nullable boxing. -/// Routes to target-specific buffer access, string indexing, Mixed dispatch, associative array -/// lookup, or indexed array access depending on `arr_ty`. The `box_nullable_base` flag causes -/// nullable array bases to be boxed into Mixed results and enables runtime null-sentinel fallbacks -/// for out-of-bounds access. -/// -/// Returns the element type after any boxing or conversion. -pub(crate) fn emit_array_access_with_loaded_base( - arr_ty: &PhpType, - index: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - box_nullable_base: bool, -) -> PhpType { - if let PhpType::Buffer(elem_ty) = arr_ty { - let buffer_reg = abi::symbol_scratch_reg(emitter); - let len_reg = abi::temp_int_reg(emitter.target); - let stride_reg = match emitter.target.arch { - Arch::AArch64 => "x11", - Arch::X86_64 => "rcx", - }; - let result_reg = abi::int_result_reg(emitter); - abi::emit_push_reg(emitter, result_reg); // preserve the buffer header pointer while evaluating the index expression - emit_expr(index, emitter, ctx, data); - abi::emit_pop_reg(emitter, buffer_reg); // restore the buffer header pointer into a scratch register - emitter.comment("buffer access"); - let uaf_ok = ctx.next_label("buf_uaf_ok"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbnz {}, {}", buffer_reg, uaf_ok)); // skip the fatal helper when the buffer header pointer is still live - emitter.instruction("b __rt_buffer_use_after_free"); // abort immediately when the buffer local was nulled by buffer_free() - } - Arch::X86_64 => { - emitter.instruction(&format!("test {}, {}", buffer_reg, buffer_reg)); // check whether the restored buffer header pointer is null - emitter.instruction(&format!("jne {}", uaf_ok)); // continue only when the buffer header pointer is still live - emitter.instruction("jmp __rt_buffer_use_after_free"); // abort immediately when the buffer local was nulled by buffer_free() - } - } - emitter.label(&uaf_ok); - let elem_ty = *elem_ty.clone(); - let bounds_ok = ctx.next_label("buffer_idx_ok"); - let oob_ok = ctx.next_label("buf_oob_ok"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #0", result_reg)); // reject negative buffer indexes before touching the payload - emitter.instruction(&format!("b.ge {}", oob_ok)); // continue once the requested index is non-negative - emitter.instruction("b __rt_buffer_bounds_fail"); // abort immediately on negative buffer indexes - emitter.label(&oob_ok); - abi::emit_load_from_address(emitter, len_reg, buffer_reg, 0); // load the logical buffer length from the header - emitter.instruction(&format!("cmp {}, {}", result_reg, len_reg)); // compare the requested index against the logical buffer length - emitter.instruction(&format!("b.lo {}", bounds_ok)); // continue once the requested index is still in bounds - emitter.instruction(&format!("mov x1, {}", len_reg)); // pass the logical buffer length to the fatal helper for parity with the ARM path - emitter.instruction("bl __rt_buffer_bounds_fail"); // abort with the dedicated buffer-bounds diagnostic - emitter.label(&bounds_ok); - abi::emit_load_from_address(emitter, stride_reg, buffer_reg, 8); // load the element stride from the buffer header - emitter.instruction(&format!("add {}, {}, #16", buffer_reg, buffer_reg)); // skip the buffer header to reach the contiguous payload base - emitter.instruction(&format!("madd {}, {}, {}, {}", buffer_reg, result_reg, stride_reg, buffer_reg)); // compute payload base + index*stride for the addressed buffer element - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, 0", result_reg)); // reject negative buffer indexes before touching the payload - emitter.instruction(&format!("jge {}", oob_ok)); // continue once the requested index is non-negative - emitter.instruction("jmp __rt_buffer_bounds_fail"); // abort immediately on negative buffer indexes - emitter.label(&oob_ok); - abi::emit_load_from_address(emitter, len_reg, buffer_reg, 0); // load the logical buffer length from the header - emitter.instruction(&format!("cmp {}, {}", result_reg, len_reg)); // compare the requested index against the logical buffer length - emitter.instruction(&format!("jl {}", bounds_ok)); // continue once the requested index is still in bounds - emitter.instruction("jmp __rt_buffer_bounds_fail"); // abort with the dedicated buffer-bounds diagnostic - emitter.label(&bounds_ok); - abi::emit_load_from_address(emitter, stride_reg, buffer_reg, 8); // load the element stride from the buffer header - emitter.instruction(&format!("add {}, 16", buffer_reg)); // skip the buffer header to reach the contiguous payload base - emitter.instruction(&format!("imul {}, {}", result_reg, stride_reg)); // scale the requested index by the element stride in bytes - emitter.instruction(&format!("add {}, {}", buffer_reg, result_reg)); // advance the payload base to the addressed buffer element - } - } - match &elem_ty { - PhpType::Float => { - abi::emit_load_from_address(emitter, abi::float_result_reg(emitter), buffer_reg, 0); // load the floating-point payload from the addressed buffer element slot - return PhpType::Float; - } - PhpType::Packed(name) => { - emitter.instruction(&format!("mov {}, {}", result_reg, buffer_reg)); // expose the packed element address as a typed pointer result - return PhpType::Pointer(Some(name.clone())); - } - _ => { - abi::emit_load_from_address(emitter, result_reg, buffer_reg, 0); // load the scalar or pointer payload from the addressed buffer element slot - return elem_ty; - } - } - } - - if *arr_ty == PhpType::Str { - let (str_ptr_reg, str_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, str_ptr_reg, str_len_reg); // preserve the indexed source string while evaluating the scalar offset expression - emit_string_offset_index(index, emitter, ctx, data); - emitter.comment("string indexing"); - - let non_negative = ctx.next_label("str_idx_pos"); - let oob = ctx.next_label("str_idx_oob"); - let end = ctx.next_label("str_idx_end"); - - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the indexed source string into the standard AArch64 string result registers after evaluating the scalar offset - emitter.instruction("cmp x0, #0"); // check whether the requested string offset is negative - emitter.instruction(&format!("b.ge {}", non_negative)); // keep non-negative offsets as-is - emitter.instruction("add x0, x2, x0"); // convert negative offsets to length + offset - emitter.instruction("cmp x0, #0"); // check whether the adjusted offset still points before the string - emitter.instruction(&format!("b.lt {}", oob)); // negative offsets beyond -len return empty string - emitter.label(&non_negative); - emitter.instruction("cmp x0, x2"); // compare the offset against the string length - emitter.instruction(&format!("b.ge {}", oob)); // offsets at or beyond length return empty string - emitter.instruction("add x1, x1, x0"); // advance the string pointer to the selected character - emitter.instruction("mov x2, #1"); // string indexing returns exactly one character when in bounds - emitter.instruction(&format!("b {}", end)); // skip the out-of-bounds fallback - emitter.label(&oob); - emitter.instruction("mov x2, #0"); // out-of-bounds: return empty string - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the computed scalar string offset in its own temporary slot before the original string pair is restored from the older stack slot - emitter.instruction("mov rax, QWORD PTR [rsp]"); // reload the computed scalar string offset from the top temporary stack slot without disturbing the older saved string pair yet - emitter.instruction("mov r8, QWORD PTR [rsp + 16]"); // reload the indexed source string pointer from the older temporary stack slot below the saved scalar offset - emitter.instruction("mov r9, QWORD PTR [rsp + 24]"); // reload the indexed source string length from the older temporary stack slot below the saved scalar offset - emitter.instruction("add rsp, 32"); // release both temporary stack slots after restoring the scalar index and indexed source string pair - emitter.instruction("cmp rax, 0"); // check whether the requested string offset is negative - emitter.instruction(&format!("jge {}", non_negative)); // keep non-negative offsets as-is - emitter.instruction("add rax, r9"); // convert negative offsets to length + offset - emitter.instruction("cmp rax, 0"); // check whether the adjusted offset still points before the string - emitter.instruction(&format!("jl {}", oob)); // negative offsets beyond -len return empty string - emitter.label(&non_negative); - emitter.instruction("cmp rax, r9"); // compare the offset against the string length - emitter.instruction(&format!("jge {}", oob)); // offsets at or beyond length return empty string - emitter.instruction("add r8, rax"); // advance the string pointer to the selected character - emitter.instruction("mov rax, r8"); // publish the addressed character pointer in the standard x86_64 string result pointer register - emitter.instruction("mov rdx, 1"); // string indexing returns exactly one character when in bounds - emitter.instruction(&format!("jmp {}", end)); // skip the out-of-bounds fallback - emitter.label(&oob); - emitter.instruction("mov rax, r8"); // preserve the original string pointer as the empty-string base pointer for out-of-bounds indexing - emitter.instruction("mov rdx, 0"); // out-of-bounds: return empty string - } - } - emitter.label(&end); - - return PhpType::Str; - } - - if matches!(arr_ty, PhpType::Mixed) { - // Mixed receiver: dispatch through the unified runtime helper. It - // unboxes the cell, branches on the runtime tag (indexed array, - // assoc, stdClass), and returns a Mixed cell — including - // Mixed(null) for misses, non-container payloads, or unrelated - // class types. The helper expects (mixed_ptr, key_lo, key_hi) - // matching the convention of `emit_normalized_hash_key`. - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed Mixed receiver across key evaluation - crate::codegen::emit_normalized_hash_key(index, emitter, ctx, data); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x0"); // restore the Mixed receiver into the helper's first argument - emitter.instruction("bl __rt_mixed_array_get"); // dispatch to the unified array/hash/stdclass reader - } - Arch::X86_64 => { - // emit_normalized_hash_key leaves key_lo in rax/rdx and - // key_hi in rdx/-1; SysV expects (rdi, rsi, rdx) for - // (mixed_ptr, key_lo, key_hi). - emitter.instruction("mov rsi, rax"); // shift key_lo from the hash-helper return register into the SysV second-arg slot - abi::emit_pop_reg(emitter, "rdi"); // restore the Mixed receiver into the SysV first-arg register - emitter.instruction("call __rt_mixed_array_get"); // dispatch to the unified array/hash/stdclass reader - } - } - return PhpType::Mixed; - } - - let assoc_value_ty = match arr_ty { - PhpType::AssocArray { value, .. } => Some(*value.clone()), - PhpType::Union(members) => members.iter().find_map(|member| { - if let PhpType::AssocArray { value, .. } = member { - Some(*value.clone()) - } else { - None - } - }), - _ => None, - }; - - if let Some(val_ty) = assoc_value_ty { - let boxed_assoc_base = matches!(arr_ty, PhpType::Mixed | PhpType::Union(_)); - let box_assoc_result = - box_nullable_base && boxed_assoc_base && !matches!(val_ty.codegen_repr(), PhpType::Mixed); - let boxed_assoc_fallback = - box_assoc_result || matches!(val_ty.codegen_repr(), PhpType::Mixed); - let done = ctx.next_label("hash_done"); - if boxed_assoc_base { - let hash_payload = ctx.next_label("hash_payload"); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed array|false value while evaluating the key expression - crate::codegen::emit_normalized_hash_key(index, emitter, ctx, data); - let (key_ptr_reg, key_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, key_ptr_reg, key_len_reg); // preserve the normalized key while unboxing the array|false value - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x0", 16); - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect a boxed array|false value after the key expression has run - emitter.instruction("cmp x0, #5"); // runtime tag 5 = associative array - emitter.instruction(&format!("b.eq {}", hash_payload)); // continue only when the boxed payload is a hash - abi::emit_release_temporary_stack(emitter, 32); // discard the saved key and boxed base before returning the null-like fallback - if boxed_assoc_fallback { - objects_boxed_null_for_array_access(emitter); - } else if crate::codegen::sentinels::null_repr_is_tagged() - && matches!(val_ty, PhpType::Int) - { - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), NULL_SENTINEL); - } - emitter.instruction(&format!("b {}", done)); // skip hash lookup when the boxed value is false/null - emitter.label(&hash_payload); - emitter.instruction("mov x0, x1"); // move the unboxed hash pointer into the standard result register - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the normalized key into the hash-get helper argument registers - abi::emit_release_temporary_stack(emitter, 16); // discard the original boxed base after extracting its hash payload - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rax", 16); - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect a boxed array|false value after the key expression has run - emitter.instruction("cmp rax, 5"); // runtime tag 5 = associative array - emitter.instruction(&format!("je {}", hash_payload)); // continue only when the boxed payload is a hash - abi::emit_release_temporary_stack(emitter, 32); // discard the saved key and boxed base before returning the null-like fallback - if boxed_assoc_fallback { - objects_boxed_null_for_array_access(emitter); - } else if crate::codegen::sentinels::null_repr_is_tagged() - && matches!(val_ty, PhpType::Int) - { - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), NULL_SENTINEL); - } - emitter.instruction(&format!("jmp {}", done)); // skip hash lookup when the boxed value is false/null - emitter.label(&hash_payload); - emitter.instruction("mov r8, rdi"); // preserve the unboxed hash pointer while restoring the normalized key - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore the normalized key into the hash-get helper argument registers - abi::emit_release_temporary_stack(emitter, 16); // discard the original boxed base after extracting its hash payload - emitter.instruction("mov rdi, r8"); // pass the unboxed hash pointer as the first hash-get argument - } - } - } else { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the hash-table pointer while evaluating the string key expression - crate::codegen::emit_normalized_hash_key(index, emitter, ctx, data); - let (key_ptr_reg, key_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, key_ptr_reg, key_len_reg); // preserve the computed key pointer and length while restoring the hash-table pointer - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the key pointer and length from the top stack slot into the hash-get helper argument registers - abi::emit_pop_reg(emitter, "x0"); // restore the saved hash-table pointer into the first hash-get helper argument register - } - Arch::X86_64 => { - abi::emit_pop_reg_pair(emitter, "rsi", "rdx"); // restore the key pointer and length from the top stack slot into the remaining SysV hash-get helper argument registers - abi::emit_pop_reg(emitter, "rdi"); // restore the saved hash-table pointer into the first SysV hash-get helper argument register - } - } - } - let tagged_int_result = crate::codegen::sentinels::null_repr_is_tagged() - && matches!(val_ty, PhpType::Int) - && !box_assoc_result; - emitter.comment("assoc array access"); - abi::emit_call_label(emitter, "__rt_hash_get"); // lookup key and return found-flag plus borrowed payload words through the target runtime ABI - - let not_found = ctx.next_label("hash_miss"); - abi::emit_branch_if_int_result_zero(emitter, ¬_found); // jump to the not-found handler when the hash lookup misses - - match emitter.target.arch { - Arch::AArch64 => match &val_ty { - PhpType::Int | PhpType::Bool => { - emitter.instruction("mov x0, x1"); // move the borrowed associative-array scalar payload into the standard integer result register - } - PhpType::Str => {} - PhpType::Float => { - emitter.instruction("fmov d0, x1"); // move the borrowed associative-array float bits into the standard float result register - } - PhpType::Mixed => { - let box_label = ctx.next_label("hash_mixed_box"); - let done_label = ctx.next_label("hash_mixed_done"); - emitter.instruction("cmp x3, #7"); // is the associative-array value already a boxed Mixed cell? - emitter.instruction(&format!("b.ne {}", box_label)); // box typed payloads while preserving existing Mixed cells directly - emitter.instruction("mov x0, x1"); // move the stored Mixed cell into the standard result register - abi::emit_call_label(emitter, "__rt_incref"); - emitter.instruction(&format!("b {}", done_label)); // skip typed-payload boxing after retaining the stored Mixed cell - emitter.label(&box_label); - emit_box_runtime_payload_as_mixed(emitter, "x3", "x1", "x2"); // box the borrowed associative-array payload into an owned mixed cell - emitter.label(&done_label); - } - _ => { - emitter.instruction("mov x0, x1"); // move the borrowed associative-array pointer payload into the standard integer result register - } - }, - Arch::X86_64 => match &val_ty { - PhpType::Int | PhpType::Bool => { - emitter.instruction("mov rax, rdi"); // move the borrowed associative-array scalar payload into the standard integer result register - } - PhpType::Str => { - emitter.instruction("mov rax, rdi"); // move the borrowed associative-array string pointer into the standard x86_64 string result register - emitter.instruction("mov rdx, rsi"); // move the borrowed associative-array string length into the paired x86_64 string result register - } - PhpType::Float => { - emitter.instruction("movq xmm0, rdi"); // move the borrowed associative-array float bits into the standard float result register - } - PhpType::Mixed => { - let box_label = ctx.next_label("hash_mixed_box"); - let done_label = ctx.next_label("hash_mixed_done"); - emitter.instruction("cmp rcx, 7"); // is the associative-array value already a boxed Mixed cell? - emitter.instruction(&format!("jne {}", box_label)); // box typed payloads while preserving existing Mixed cells directly - emitter.instruction("mov rax, rdi"); // move the stored Mixed cell into the standard result register - abi::emit_call_label(emitter, "__rt_incref"); - emitter.instruction(&format!("jmp {}", done_label)); // skip typed-payload boxing after retaining the stored Mixed cell - emitter.label(&box_label); - emit_box_runtime_payload_as_mixed(emitter, "rcx", "rdi", "rsi"); // box the borrowed associative-array payload into an owned mixed cell - emitter.label(&done_label); - } - _ => { - emitter.instruction("mov rax, rdi"); // move the borrowed associative-array pointer payload into the standard integer result register - } - }, - } - if box_assoc_result { - crate::codegen::emit_box_current_value_as_mixed(emitter, &val_ty); - } - if tagged_int_result { - crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - } - abi::emit_jump(emitter, &done); // skip the not-found fallback after materializing the successful lookup result - - emitter.label(¬_found); - if boxed_assoc_fallback { - objects_boxed_null_for_array_access(emitter); - } else if tagged_int_result { - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - } else { - emit_typed_null_fallback(emitter, &val_ty); // type-aware null: empty Str / 0.0 Float / int sentinel (no stale ptr/len/fp regs) - } - emitter.label(&done); - if tagged_int_result { - return PhpType::TaggedScalar; - } - return if box_assoc_result { PhpType::Mixed } else { val_ty }; - } - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the array pointer while evaluating the index expression - let index_ty = emit_expr(index, emitter, ctx, data); - coerce_result_to_type(emitter, ctx, data, &index_ty, &PhpType::Int); - let array_reg = abi::symbol_scratch_reg(emitter); - let len_reg = abi::temp_int_reg(emitter.target); - let result_reg = abi::int_result_reg(emitter); - abi::emit_pop_reg(emitter, array_reg); // restore the array pointer into a scratch register - emitter.comment("array access"); - let (elem_ty, boxed_indexed_base) = indexed_array_element_type(arr_ty, box_nullable_base); - let tagged_int_result = crate::codegen::sentinels::null_repr_is_tagged() - && matches!(elem_ty, PhpType::Int) - && !boxed_indexed_base; - - let null_label = ctx.next_label("arr_null"); - let ok_label = ctx.next_label("arr_ok"); - if boxed_indexed_base { - let array_payload = ctx.next_label("arr_payload"); - abi::emit_push_reg(emitter, result_reg); // preserve the evaluated array index while unboxing the nullable array base - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, {}", array_reg)); // move the boxed array base into the mixed-unbox input register - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect the boxed array base after the index expression has run - emitter.instruction("cmp x0, #4"); // runtime tag 4 = indexed array - emitter.instruction(&format!("b.eq {}", array_payload)); // continue only when the boxed payload is an indexed array - abi::emit_release_temporary_stack(emitter, 16); // discard the saved index before returning a boxed null fallback - objects_boxed_null_for_array_access(emitter); - emitter.instruction(&format!("b {}", ok_label)); // skip indexed-array bounds checks when the base is not an array - emitter.label(&array_payload); - emitter.instruction(&format!("mov {}, x1", array_reg)); // move the unboxed indexed-array pointer into the scratch array register - abi::emit_pop_reg(emitter, result_reg); // restore the evaluated index for bounds checking - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", array_reg)); // move the boxed array base into the mixed-unbox input register - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect the boxed array base after the index expression has run - emitter.instruction("cmp rax, 4"); // runtime tag 4 = indexed array - emitter.instruction(&format!("je {}", array_payload)); // continue only when the boxed payload is an indexed array - abi::emit_release_temporary_stack(emitter, 16); // discard the saved index before returning a boxed null fallback - objects_boxed_null_for_array_access(emitter); - emitter.instruction(&format!("jmp {}", ok_label)); // skip indexed-array bounds checks when the base is not an array - emitter.label(&array_payload); - emitter.instruction(&format!("mov {}, rdi", array_reg)); // move the unboxed indexed-array pointer into the scratch array register - abi::emit_pop_reg(emitter, result_reg); // restore the evaluated index for bounds checking - } - } - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // check if index is negative - emitter.instruction(&format!("b.lt {null_label}")); // negative index → null sentinel - abi::emit_load_from_address(emitter, len_reg, array_reg, 0); // load array length from header (offset 0) - emitter.instruction(&format!("cmp {}, {}", result_reg, len_reg)); // compare index against array length - emitter.instruction(&format!("b.ge {null_label}")); // index >= length → null sentinel - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, 0", result_reg)); // check if index is negative - emitter.instruction(&format!("jl {null_label}")); // negative index → null sentinel - abi::emit_load_from_address(emitter, len_reg, array_reg, 0); // load array length from header (offset 0) - emitter.instruction(&format!("cmp {}, {}", result_reg, len_reg)); // compare index against array length - emitter.instruction(&format!("jge {null_label}")); // index >= length → null sentinel - } - } - - match &elem_ty { - PhpType::Int | PhpType::Bool | PhpType::Callable | PhpType::Resource(_) => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip 24-byte array header to reach data - emitter.instruction(&format!("ldr {}, [{}, {}, lsl #3]", result_reg, array_reg, result_reg)); // load element at array + index*8 - } - Arch::X86_64 => { - emitter.instruction(&format!("lea {}, [{} + 24]", array_reg, array_reg)); // skip 24-byte array header to reach data - emitter.instruction(&format!("mov {}, QWORD PTR [{} + {} * 8]", result_reg, array_reg, result_reg)); // load element at array + index*8 - } - } - } - PhpType::Float => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip 24-byte array header to reach the contiguous float payload - emitter.instruction(&format!("ldr d0, [{}, {}, lsl #3]", array_reg, result_reg)); // load the float payload from data[index] - } - Arch::X86_64 => { - emitter.instruction(&format!("lea {}, [{} + 24]", array_reg, array_reg)); // skip 24-byte array header to reach the contiguous float payload - emitter.instruction(&format!("movsd xmm0, QWORD PTR [{} + {} * 8]", array_reg, result_reg)); // load the float payload from data[index] - } - }, - PhpType::Str => { - let (ptr_reg, len_result_reg) = abi::string_result_regs(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("lsl {}, {}, #4", result_reg, result_reg)); // multiply index by 16 (string = ptr+len pair) - emitter.instruction(&format!("add {}, {}, {}", array_reg, array_reg, result_reg)); // add scaled index offset to array base - emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip 24-byte array header to reach data - abi::emit_load_from_address(emitter, ptr_reg, array_reg, 0); // load string pointer from element slot - abi::emit_load_from_address(emitter, len_result_reg, array_reg, 8); // load string length from element slot - } - Arch::X86_64 => { - emitter.instruction(&format!("shl {}, 4", result_reg)); // multiply index by 16 (string = ptr+len pair) - emitter.instruction(&format!("add {}, {}", array_reg, result_reg)); // add scaled index offset to array base - emitter.instruction(&format!("add {}, 24", array_reg)); // skip 24-byte array header to reach data - abi::emit_load_from_address(emitter, ptr_reg, array_reg, 0); // load string pointer from element slot - abi::emit_load_from_address(emitter, len_result_reg, array_reg, 8); // load string length from element slot - } - } - } - PhpType::Mixed | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip 24-byte array header to reach data - emitter.instruction(&format!("ldr {}, [{}, {}, lsl #3]", result_reg, array_reg, result_reg)); // load pointer at index - } - Arch::X86_64 => { - emitter.instruction(&format!("lea {}, [{} + 24]", array_reg, array_reg)); // skip 24-byte array header to reach data - emitter.instruction(&format!("mov {}, QWORD PTR [{} + {} * 8]", result_reg, array_reg, result_reg)); // load pointer at index - } - } - } - _ => {} - } - if boxed_indexed_base { - crate::codegen::emit_box_current_value_as_mixed(emitter, &elem_ty); - } - if tagged_int_result { - crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - } - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("b {ok_label}")), // skip null sentinel fallback - Arch::X86_64 => emitter.instruction(&format!("jmp {ok_label}")), // skip null sentinel fallback - } - - emitter.label(&null_label); - emit_undefined_index_warning(emitter); - if boxed_indexed_base || matches!(elem_ty, PhpType::Mixed | PhpType::Union(_)) { - objects_boxed_null_for_array_access(emitter); - } else if tagged_int_result { - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - } else { - emit_typed_null_fallback(emitter, &elem_ty); // type-aware null: empty Str / 0.0 Float / int sentinel (no stale ptr/len/fp regs) - } - emitter.label(&ok_label); - - if tagged_int_result { - return PhpType::TaggedScalar; - } - if boxed_indexed_base { PhpType::Mixed } else { elem_ty } -} - -/// Determines the element type for indexed array access and whether the base requires boxing. -/// For `PhpType::Array(elem_ty)`, returns the element type with `boxed=false`. For unions, -/// extracts the first Array variant's element type or defaults to `PhpType::Int`; the second -/// return value is `box_nullable_base` when the union contains a nullable base. For unknown -/// types, defaults to `PhpType::Int` with `boxed=false`. -/// -/// Returns a tuple of `(element_type, requires_boxing)`. -fn indexed_array_element_type(arr_ty: &PhpType, box_nullable_base: bool) -> (PhpType, bool) { - match arr_ty { - PhpType::Array(elem_ty) => (*elem_ty.clone(), false), - PhpType::Union(members) => { - let elem_ty = members - .iter() - .find_map(|member| { - if let PhpType::Array(elem_ty) = member { - Some(*elem_ty.clone()) - } else { - None - } - }) - .unwrap_or(PhpType::Int); - (elem_ty, box_nullable_base) - } - _ => (PhpType::Int, false), - } -} - -/// Emits a boxed null for nullable array access. Materializes the runtime null sentinel -/// (i64::MAX - 1) into the integer result register and boxes it as a Mixed value using -/// `emit_box_current_value_as_mixed`. Used for out-of-bounds indexed access and nullable -/// associative array misses when the result must be a Mixed cell. -/// Materializes a type-appropriate "null" for an out-of-bounds indexed read or -/// an associative-array miss when the element type is a concrete scalar (not a -/// boxed Mixed/Union). Without this, only the integer result register is set on -/// the miss path, leaving the string (ptr/len) or float result registers -/// holding STALE values from a prior expression — manifesting as duplicated -/// output (Str) or a wrong value (Float). For `Str`: a valid empty string -/// (`_empty_str` pointer, length 0) so any echo/strlen path is safe regardless -/// of which registers are live at the call site. For `Float`: 0.0. Otherwise: -/// the existing integer null sentinel. Does not allocate or alias array -/// storage, so it is COW/ownership-safe on every release path. -fn emit_typed_null_fallback(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_symbol_address(emitter, ptr_reg, "_empty_str"); // valid readable pointer for a 0-length string - match emitter.target.arch { - Arch::AArch64 => emitter.instruction(&format!("mov {}, #0", len_reg)), // string length = 0 - Arch::X86_64 => emitter.instruction(&format!("xor {}, {}", len_reg, len_reg)), // string length = 0 - } - } - PhpType::Float => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("fmov d0, xzr"), // float null → 0.0 - Arch::X86_64 => emitter.instruction("xorps xmm0, xmm0"), // float null → 0.0 - }, - _ => abi::emit_load_int_immediate( - emitter, - abi::int_result_reg(emitter), - NULL_SENTINEL, - ), // integer/bool/resource/callable null sentinel - } -} - -/// Provides the objects boxed null for array access helper for codegen lowering in this module. -fn objects_boxed_null_for_array_access(emitter: &mut Emitter) { - abi::emit_load_int_immediate( - emitter, - abi::int_result_reg(emitter), - NULL_SENTINEL, - ); - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Void); -} - -/// Emits the PHP warning for an undefined indexed-array key. -fn emit_undefined_index_warning(emitter: &mut Emitter) { - abi::emit_call_label(emitter, "__rt_warn_undefined_array_key_int"); // emit or suppress the PHP undefined-array-key warning for the current index -} diff --git a/src/codegen/expr/arrays/access/match_expr.rs b/src/codegen/expr/arrays/access/match_expr.rs deleted file mode 100644 index cb6535b33a..0000000000 --- a/src/codegen/expr/arrays/access/match_expr.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Purpose: -//! Lowers array reads used by match expressions with PHP comparison semantics. -//! Produces expression results while preserving container ownership and bounds/null behavior. -//! -//! Called from: -//! - `crate::codegen::expr::arrays::access` -//! -//! Key details: -//! - Element layout and boxed Mixed handling must stay aligned with array runtime helpers. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Lowers a `match` expression with PHP equality semantics. -/// -/// The subject expression is evaluated and saved to a single 16-byte stack slot. -/// Each arm's patterns are then evaluated and compared against the saved subject -/// using PHP equality (`==`): strings via `__rt_str_eq`, floats via `fcmp`/`ucomisd`, -/// scalars via integer comparison. Matching jumps to the arm body; misses fall through -/// to the next arm or the default. If no arm matches and no default is present, -/// `__rt_match_unhandled` is called. -/// -/// Returns the `PhpType` of the selected arm expression (or `Void` if only the -/// unhandled abort path is taken). The stack slot is released before returning -/// without clobbering result registers. -/// -/// # Arguments -/// * `subject` – the match subject expression -/// * `arms` – pairs of pattern lists and their result expressions -/// * `default` – optional default expression when no arm matches -/// * `emitter` – target assembly emitter -/// * `ctx` – codegen context (labels, types, ownership) -/// * `data` – data section for literals/strings -pub(crate) fn emit_match_expr( - subject: &Expr, - arms: &[(Vec, Expr)], - default: &Option>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("match expression"); - let subj_ty = emit_expr(subject, emitter, ctx, data); - match &subj_ty { - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // save the string subject in one temporary stack slot using the active target ABI - } - PhpType::Float => { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // save the float subject in one temporary stack slot using the active target ABI - } - _ => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the scalar subject in one temporary stack slot using the active target ABI - } - } - - let end_label = ctx.next_label("match_end"); - let mut result_ty = PhpType::Void; - - for (patterns, result) in arms { - let arm_label = ctx.next_label("match_arm"); - let next_arm = ctx.next_label("match_next"); - - for (i, pattern) in patterns.iter().enumerate() { - let pat_ty = emit_expr(pattern, emitter, ctx, data); - match &subj_ty { - PhpType::Str => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, x1"); // move the pattern string pointer into the AArch64 right-hand compare register - emitter.instruction("mov x4, x2"); // move the pattern string length into the AArch64 right-hand compare register - emitter.instruction("ldp x1, x2, [sp]"); // reload the saved subject string into the AArch64 left-hand compare registers - abi::emit_call_label(emitter, "__rt_str_eq"); // compare the subject and pattern strings through the shared runtime helper - } - Arch::X86_64 => { - emitter.instruction("mov rcx, rdx"); // move the pattern string length into the SysV fourth argument register expected by __rt_str_eq - emitter.instruction("mov rdx, rax"); // move the pattern string pointer into the SysV third argument register expected by __rt_str_eq - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the saved subject string pointer into the SysV first argument register - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // reload the saved subject string length into the SysV second argument register - abi::emit_call_label(emitter, "__rt_str_eq"); // compare the subject and pattern strings through the shared runtime helper - } - }, - PhpType::Float => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr d1, [sp]"); // reload the saved subject float into the AArch64 scratch compare register - emitter.instruction("fcmp d1, d0"); // compare the saved subject float against the current pattern float - emitter.instruction("cset x0, eq"); // materialize the float equality result in the canonical AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("movsd xmm1, QWORD PTR [rsp]"); // reload the saved subject float into the x86_64 scratch compare register - emitter.instruction("ucomisd xmm1, xmm0"); // compare the saved subject float against the current pattern float - emitter.instruction("sete al"); // materialize the float equality result in the low x86_64 result byte - emitter.instruction("movzx eax, al"); // widen the x86_64 boolean byte back into the canonical integer result register - } - }, - _ => match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // reload the saved scalar subject into an AArch64 scratch register - emitter.instruction("cmp x9, x0"); // compare the saved scalar subject against the current pattern scalar - emitter.instruction("cset x0, eq"); // materialize the scalar equality result in the canonical AArch64 integer result register - } - Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the saved scalar subject into an x86_64 scratch register - emitter.instruction("cmp r10, rax"); // compare the saved scalar subject against the current pattern scalar - emitter.instruction("sete al"); // materialize the scalar equality result in the low x86_64 result byte - emitter.instruction("movzx eax, al"); // widen the x86_64 boolean byte back into the canonical integer result register - } - }, - } - abi::emit_branch_if_int_result_nonzero(emitter, &arm_label); // jump to the current match arm once the subject equals the current pattern - if i == patterns.len() - 1 { - abi::emit_jump(emitter, &next_arm); // continue with the next match arm when this arm's patterns all miss - } - let _ = pat_ty; - } - - emitter.label(&arm_label); - result_ty = emit_expr(result, emitter, ctx, data); - abi::emit_jump(emitter, &end_label); // skip the remaining match arms after evaluating the selected arm expression - emitter.label(&next_arm); - } - - if let Some(def) = default { - result_ty = emit_expr(def, emitter, ctx, data); - } else { - abi::emit_call_label(emitter, "__rt_match_unhandled"); // abort when no arm matched and the match expression has no default arm - } - - emitter.label(&end_label); - abi::emit_release_temporary_stack(emitter, 16); // release the saved subject slot without clobbering the match expression result registers - result_ty -} diff --git a/src/codegen/expr/arrays/access/mod.rs b/src/codegen/expr/arrays/access/mod.rs deleted file mode 100644 index 5c9ddb1b2b..0000000000 --- a/src/codegen/expr/arrays/access/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Purpose: -//! Dispatches array access expression lowering across indexed arrays, buffers, and match-specific reads. -//! Keeps container-specific addressing details out of the main expression dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::arrays` -//! -//! Key details: -//! - Access paths must agree on nullable, boxed Mixed, and borrowed-result ownership conventions. - -mod buffer; -mod indexed; -mod match_expr; -mod object; -mod string_offset; - -pub(crate) use buffer::emit_buffer_new; -pub(crate) use indexed::{emit_array_access, emit_array_access_with_loaded_base}; -pub(crate) use match_expr::emit_match_expr; -pub(crate) use object::{ - emit_offset_exists as emit_array_access_offset_exists, - emit_offset_set as emit_array_access_offset_set, - emit_offset_unset as emit_array_access_offset_unset, - type_is_array_access_object, -}; diff --git a/src/codegen/expr/arrays/access/object.rs b/src/codegen/expr/arrays/access/object.rs deleted file mode 100644 index 488d928dde..0000000000 --- a/src/codegen/expr/arrays/access/object.rs +++ /dev/null @@ -1,333 +0,0 @@ -//! Purpose: -//! Lowers `$obj[$key]` syntax for objects that implement PHP's `ArrayAccess`. -//! Routes read, write, isset, and unset forms through the corresponding `offset*` methods. -//! -//! Called from: -//! - `crate::codegen::expr::arrays::access` -//! - `crate::codegen::stmt::arrays` -//! - `crate::codegen::builtins` -//! -//! Key details: -//! - Receiver evaluation and argument evaluation reuse normal method-call lowering so Mixed boxing -//! follows the declared `ArrayAccess` method signatures. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::functions; -use crate::parser::ast::Expr; -use crate::types::{FunctionSig, PhpType}; - -/// Dispatch target for ArrayAccess method calls. -/// -/// `Class` indicates a single concrete class that directly implements ArrayAccess -/// and allows static dispatch. `Interface` indicates either an interface type or -/// a union of multiple distinct classes that require virtual dispatch through -/// the ArrayAccess vtable. -enum ArrayAccessDispatchTarget { - Class(String), - Interface(String), -} - -/// Returns true if the expression type resolves to a PHP ArrayAccess object. -pub(crate) fn expr_is_array_access_object(expr: &Expr, ctx: &Context) -> bool { - let ty = functions::infer_contextual_type(expr, ctx); - type_is_array_access_object(&ty, ctx) -} - -/// Returns true if the type is an object implementing PHP's ArrayAccess interface. -pub(crate) fn type_is_array_access_object(ty: &PhpType, ctx: &Context) -> bool { - match ty { - PhpType::Object(name) => ctx.object_type_implements_interface(name, "ArrayAccess"), - PhpType::Union(members) => { - let mut saw_object = false; - for member in members { - match member { - PhpType::Void => {} - PhpType::Object(name) => { - if !ctx.object_type_implements_interface(name, "ArrayAccess") { - return false; - } - saw_object = true; - } - _ => return false, - } - } - saw_object - } - _ => false, - } -} - -/// Emits `$obj[$key]` read via ArrayAccess::offsetGet. -pub(crate) fn emit_offset_get( - object: &Expr, - index: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_array_access_method(object, "offsetget", &[index.clone()], emitter, ctx, data) -} - -/// Emits `$obj[$key] = $value` via ArrayAccess::offsetSet. -pub(crate) fn emit_offset_set( - object: &Expr, - index: &Expr, - value: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_array_access_method( - object, - "offsetset", - &[index.clone(), value.clone()], - emitter, - ctx, - data, - ) -} - -/// Emits `isset($obj[$key])` via ArrayAccess::offsetExists. -pub(crate) fn emit_offset_exists( - object: &Expr, - index: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_array_access_method(object, "offsetexists", &[index.clone()], emitter, ctx, data) -} - -/// Emits `unset($obj[$key])` via ArrayAccess::offsetUnset. -pub(crate) fn emit_offset_unset( - object: &Expr, - index: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_array_access_method(object, "offsetunset", &[index.clone()], emitter, ctx, data) -} - -/// Shared lowering for all ArrayAccess subscript operations. -/// -/// Evaluates the receiver and index/value arguments, infers the static dispatch target -/// (class or interface), unboxes Mixed receivers, then delegates to the class or interface -/// method call emitter. Returns the declared return type of the resolved `offset*` method. -fn emit_array_access_method( - object: &Expr, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("ArrayAccess::{}()", method)); - let static_ty = functions::infer_contextual_type(object, ctx); - let Some(target) = array_access_dispatch_target(&static_ty, ctx) else { - emitter.comment("WARNING: ArrayAccess subscript on non-ArrayAccess receiver"); - return PhpType::Int; - }; - - let runtime_ty = crate::codegen::expr::emit_expr(object, emitter, ctx, data); - if matches!(runtime_ty, PhpType::Mixed | PhpType::Union(_)) { - let message = format!( - "Fatal error: Call to a member function {}() on null\n", - method - ); - crate::codegen::expr::objects::emit_unbox_mixed_object_or_fatal( - message.as_bytes(), - emitter, - ctx, - data, - ); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the ArrayAccess receiver below evaluated offset/value arguments - - let sig = array_access_method_sig(&target, method, ctx); - let emitted_args = crate::codegen::expr::objects::emit_pushed_method_args( - args, - sig.as_ref(), - emitter, - ctx, - data, - ); - - match target { - ArrayAccessDispatchTarget::Class(class_name) => { - crate::codegen::expr::objects::emit_method_call_with_saved_receiver_below_args( - &class_name, - method, - &emitted_args.arg_types, - emitted_args.source_temp_bytes, - emitter, - ctx, - ) - } - ArrayAccessDispatchTarget::Interface(interface_name) => { - emit_interface_method_call_with_saved_receiver_below_args( - &interface_name, - method, - &emitted_args.arg_types, - emitted_args.source_temp_bytes, - emitter, - ctx, - ) - } - } -} - -/// Infers the static dispatch target for an ArrayAccess method call from a static type. -/// Returns `Class(name)` for a single concrete class, `Interface("ArrayAccess")` when virtual -/// dispatch is required, or `None` if the type does not implement ArrayAccess. Union types must -/// contain only objects implementing ArrayAccess (or Void); any other member yields `None`. -fn array_access_dispatch_target( - ty: &PhpType, - ctx: &Context, -) -> Option { - match ty { - PhpType::Object(name) if ctx.classes.contains_key(name) => ctx - .object_type_implements_interface(name, "ArrayAccess") - .then(|| ArrayAccessDispatchTarget::Class(name.clone())), - PhpType::Object(name) if ctx.interfaces.contains_key(name) => ctx - .object_type_implements_interface(name, "ArrayAccess") - .then(|| ArrayAccessDispatchTarget::Interface("ArrayAccess".to_string())), - PhpType::Union(members) => { - let mut class_target: Option = None; - let mut needs_interface_dispatch = false; - let mut saw_array_access_object = false; - for member in members { - match member { - PhpType::Void => {} - PhpType::Object(name) if ctx.classes.contains_key(name) => { - if !ctx.object_type_implements_interface(name, "ArrayAccess") { - return None; - } - saw_array_access_object = true; - match &class_target { - Some(existing) if existing != name => { - needs_interface_dispatch = true; - } - None => { - class_target = Some(name.clone()); - } - _ => {} - } - } - PhpType::Object(name) if ctx.interfaces.contains_key(name) => { - if !ctx.object_type_implements_interface(name, "ArrayAccess") { - return None; - } - saw_array_access_object = true; - needs_interface_dispatch = true; - } - _ => return None, - } - } - if !saw_array_access_object { - None - } else if needs_interface_dispatch { - Some(ArrayAccessDispatchTarget::Interface( - "ArrayAccess".to_string(), - )) - } else { - class_target.map(ArrayAccessDispatchTarget::Class) - } - } - _ => None, - } -} - -/// Looks up the `offsetGet`/`offsetSet`/`offsetExists`/`offsetUnset` method signature -/// from the class or interface metadata for the given dispatch target. -fn array_access_method_sig( - target: &ArrayAccessDispatchTarget, - method: &str, - ctx: &Context, -) -> Option { - match target { - ArrayAccessDispatchTarget::Class(class_name) => ctx - .classes - .get(class_name) - .and_then(|class_info| class_info.methods.get(method)) - .cloned(), - ArrayAccessDispatchTarget::Interface(interface_name) => ctx - .interfaces - .get(interface_name) - .and_then(|interface_info| interface_info.methods.get(method)) - .cloned(), - } -} - -/// Emits an interface ArrayAccess method call with the receiver saved below the evaluated arguments. -/// -/// The receiver was pushed onto the stack before argument evaluation; this function duplicates -/// it above the arguments, then emits the interface method call and discards all saved slots. -/// Returns the declared return type of the interface method. -fn emit_interface_method_call_with_saved_receiver_below_args( - interface_name: &str, - method: &str, - arg_types: &[PhpType], - source_temp_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let arg_temp_bytes = pushed_arg_temp_bytes(arg_types) + source_temp_bytes; - abi::emit_load_temporary_stack_slot( - emitter, - abi::int_result_reg(emitter), - arg_temp_bytes, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // duplicate the saved ArrayAccess receiver above the evaluated arguments - let ret_ty = emit_interface_method_call_with_pushed_args( - interface_name, - method, - arg_types, - source_temp_bytes, - emitter, - ctx, - ); - abi::emit_release_temporary_stack(emitter, 16); // discard the original receiver slot saved below the argument temporaries - ret_ty -} - -/// Emits an interface method call after arguments have been pushed onto the stack. -/// -/// Pops the receiver into the first integer argument register, materializes outgoing -/// arguments per the target ABI, dispatches through the interface vtable, then releases -/// overflow stack arguments and source-order named-argument temporaries. Returns the -/// declared return type of the dispatched interface method. -fn emit_interface_method_call_with_pushed_args( - interface_name: &str, - method: &str, - arg_types: &[PhpType], - source_temp_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, arg_types, 1); - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 0)); // pop the ArrayAccess receiver into the first integer argument register - let overflow_bytes = abi::materialize_outgoing_args(emitter, &assignments); - let ret_ty = crate::codegen::expr::objects::dispatch::emit_dispatch_interface_method( - interface_name, - method, - emitter, - ctx, - ); - abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop spilled stack arguments after the interface method call returns - abi::emit_release_temporary_stack(emitter, source_temp_bytes); // drop source-order named-argument temporaries after dispatch - ret_ty -} - -/// Computes the total temporary stack bytes consumed by pushed arguments for -/// ArrayAccess method calls. Non-Void arguments consume 16 bytes each; Void arguments -/// consume 0 bytes. -fn pushed_arg_temp_bytes(arg_types: &[PhpType]) -> usize { - arg_types - .iter() - .map(|ty| if matches!(ty, PhpType::Void) { 0 } else { 16 }) - .sum() -} diff --git a/src/codegen/expr/arrays/access/string_offset.rs b/src/codegen/expr/arrays/access/string_offset.rs deleted file mode 100644 index 2bc252bea9..0000000000 --- a/src/codegen/expr/arrays/access/string_offset.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Purpose: -//! Lowers PHP string-offset expressions whose source index may be an integer-like string literal. -//! Keeps offset coercion separate from array payload addressing. -//! -//! Called from: -//! - `crate::codegen::expr::arrays::access::indexed::emit_array_access_with_loaded_base()` -//! -//! Key details: -//! - The string-indexing path expects the final offset in the integer result register before bounds checks. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -/// Emits integer offset computation for string index expressions with integer-like string literal support. -pub(super) fn emit_string_offset_index( - index: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if let ExprKind::StringLiteral(value) = &index.kind { - if let Some(offset) = crate::types::parse_php_string_offset_literal(value) { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), offset); - return PhpType::Int; - } - } - - emit_expr(index, emitter, ctx, data) -} diff --git a/src/codegen/expr/arrays/assoc.rs b/src/codegen/expr/arrays/assoc.rs deleted file mode 100644 index c6f0d06954..0000000000 --- a/src/codegen/expr/arrays/assoc.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Purpose: -//! Lowers associative array literals with normalized keys and runtime hash insertion. -//! Builds heap array values and leaves the resulting handle in expression result registers. -//! -//! Called from: -//! - `crate::codegen::expr::arrays` -//! -//! Key details: -//! - Literal emission must evaluate elements in source order and retain heap elements inserted into arrays. - -use super::super::super::context::Context; -use super::super::super::data_section::DataSection; -use super::super::super::emit::Emitter; -use super::super::super::{abi, platform::Arch}; -use super::super::{emit_expr, retain_borrowed_heap_arg, Expr, ExprKind, PhpType}; - -/// Emits an empty associative array literal with the given key/value types. -/// -/// Allocates a hash table with initial capacity 16 via `__rt_hash_new`, using the -/// provided `key_ty` and `value_ty` to set the runtime value tag. Leaves the hash -/// handle in the result register. Returns `PhpType::AssocArray` with the given -/// key and value types boxed. -pub(crate) fn emit_empty_assoc_array_literal( - key_ty: PhpType, - value_ty: PhpType, - emitter: &mut Emitter, -) -> PhpType { - emitter.comment("empty assoc array literal"); - let capacity_reg = abi::int_arg_reg_name(emitter.target, 0); - let value_tag_reg = abi::int_arg_reg_name(emitter.target, 1); - abi::emit_load_int_immediate(emitter, capacity_reg, 16); - abi::emit_load_int_immediate( - emitter, - value_tag_reg, - super::super::super::runtime_value_tag(&value_ty.codegen_repr()) as i64, - ); - abi::emit_call_label(emitter, "__rt_hash_new"); - PhpType::AssocArray { - key: Box::new(key_ty), - value: Box::new(value_ty), - } -} - -/// Rewrites a bare array literal as an `AssocArray` value when the target type is -/// associative (`T[string]:V`). An empty `[]` becomes an empty hash; a positional -/// literal `[a, b, ...]` becomes explicit `0 => a, 1 => b, ...` pairs so the -/// associative emitter normalizes the keys into hash storage. Returns `None` when -/// `value` is not an array literal or `target_ty` is not associative, so the caller -/// falls back to ordinary expression emission. Shared by property assignment and -/// object-allocation default initialization so an `[]` default whose refined type is -/// associative is stored as hash storage rather than an indexed-list array. -pub(crate) fn emit_array_literal_as_assoc_target( - value: &Expr, - target_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let ExprKind::ArrayLiteral(elems) = &value.kind else { - return None; - }; - let PhpType::AssocArray { - key: target_key_ty, - value: target_value_ty, - } = target_ty - else { - return None; - }; - if elems.is_empty() { - return Some(emit_empty_assoc_array_literal( - *target_key_ty.clone(), - *target_value_ty.clone(), - emitter, - )); - } - let pairs: Vec<(Expr, Expr)> = elems - .iter() - .enumerate() - .map(|(idx, elem)| { - ( - Expr::new(ExprKind::IntLiteral(idx as i64), elem.span), - elem.clone(), - ) - }) - .collect(); - Some(emit_assoc_array_literal(&pairs, emitter, ctx, data)) -} - -/// Emits a non-empty associative array literal with key/value expression pairs. -/// -/// Allocates a hash table via `__rt_hash_new`, then inserts each key/value pair -/// in source order via `__rt_hash_set`. Keys are emitted as normalized hash-key -/// payloads before each value expression is evaluated and inserted. Persists the -/// updated hash table pointer after each insertion (allowing the table to grow). -/// Returns `PhpType::AssocArray` with the normalized key type and the merged value -/// type (uses `PhpType::Mixed` when value types differ across pairs). -pub(crate) fn emit_assoc_array_literal( - pairs: &[(Expr, Expr)], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("assoc array literal"); - let result_reg = abi::int_result_reg(emitter); - let stack_reg = match emitter.target.arch { - Arch::AArch64 => "sp", - Arch::X86_64 => "rsp", - }; - let hash_capacity_reg = abi::int_arg_reg_name(emitter.target, 0); - let key_ptr_reg = abi::int_arg_reg_name(emitter.target, 1); - let key_len_reg = abi::int_arg_reg_name(emitter.target, 2); - let value_lo_reg = abi::int_arg_reg_name(emitter.target, 3); - let value_hi_reg = abi::int_arg_reg_name(emitter.target, 4); - let value_tag_reg = abi::int_arg_reg_name(emitter.target, 5); - let tag_reg = abi::int_arg_reg_name(emitter.target, 1); - let float_bits_reg = abi::temp_int_reg(emitter.target); - let zero_reg = match emitter.target.arch { - Arch::AArch64 => "xzr", - Arch::X86_64 => "0", - }; - let (string_ptr_reg, string_len_reg) = abi::string_result_regs(emitter); - - let first_value_ty = super::super::super::functions::infer_contextual_type(&pairs[0].1, ctx); - let header_value_ty = if matches!(first_value_ty, PhpType::Iterable) { - PhpType::Mixed - } else { - first_value_ty - }; - let value_type_tag = super::super::super::runtime_value_tag(&header_value_ty); - - abi::emit_load_int_immediate( - emitter, - hash_capacity_reg, - std::cmp::max(pairs.len() * 2, 16) as i64, - ); - abi::emit_load_int_immediate(emitter, tag_reg, value_type_tag as i64); - abi::emit_call_label(emitter, "__rt_hash_new"); - abi::emit_push_reg(emitter, result_reg); // save the hash table pointer while key/value pairs are inserted - - let mut val_ty = PhpType::Int; - for (i, pair) in pairs.iter().enumerate() { - super::super::super::emit_normalized_hash_key(&pair.0, emitter, ctx, data); - abi::emit_push_reg_pair(emitter, string_ptr_reg, string_len_reg); // save the assoc-array key payload while the value expression is emitted - let mut ty = emit_expr(&pair.1, emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut ty); - if !boxed_iterable { - retain_borrowed_heap_arg(emitter, &pair.1, &ty); - } - if i == 0 { - val_ty = ty.clone(); - } else if ty != val_ty { - val_ty = PhpType::Mixed; - } - let (val_lo, val_hi) = match &ty { - PhpType::Int | PhpType::Bool => (result_reg, zero_reg), - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_str_persist"); // copy the borrowed string result into owned heap storage - (string_ptr_reg, string_len_reg) - } - PhpType::Float => { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("fmov {}, {}", float_bits_reg, abi::float_result_reg(emitter))); // move the float bits into an integer scratch register for hash insertion - } - Arch::X86_64 => { - emitter.instruction(&format!("movq {}, {}", float_bits_reg, abi::float_result_reg(emitter))); // move the float bits into an integer scratch register for hash insertion - } - } - (float_bits_reg, zero_reg) - } - _ => (result_reg, zero_reg), - }; - emitter.instruction(&format!("mov {}, {}", value_lo_reg, val_lo)); // move the low payload word into the hash-set value register - emitter.instruction(&format!("mov {}, {}", value_hi_reg, val_hi)); // move the high payload word into the hash-set value register - abi::emit_load_int_immediate( - emitter, - value_tag_reg, - super::super::super::runtime_value_tag(&ty) as i64, - ); - abi::emit_pop_reg_pair(emitter, key_ptr_reg, key_len_reg); // restore the assoc-array key payload into the hash-set argument registers - abi::emit_load_from_address(emitter, hash_capacity_reg, stack_reg, 0); // reload the current hash table pointer before insertion - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, result_reg, stack_reg, 0); // persist the updated hash table pointer after possible growth - } - - abi::emit_pop_reg(emitter, result_reg); // restore the completed hash table pointer as the expression result - - let mut key_ty = normalized_assoc_key_type(&pairs[0].0, ctx); - for (key, _) in pairs.iter().skip(1) { - let next_ty = normalized_assoc_key_type(key, ctx); - if next_ty != key_ty { - key_ty = PhpType::Mixed; - break; - } - } - - PhpType::AssocArray { - key: Box::new(key_ty), - value: Box::new(val_ty), - } -} - -/// Emits an associative array literal with spread elements from other arrays. -/// -/// Creates an empty hash table with `PhpType::Mixed` keys, then merges each spread -/// operand in source order using `__rt_hash_union` (for assoc arrays) or -/// `__rt_hash_array_union` (for indexed arrays). The merged hash handle is left in -/// the result register. Returns `PhpType::AssocArray` with mixed keys and the -/// inferred value type from the spread elements. -pub(crate) fn emit_array_literal_with_assoc_spread( - elems: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("assoc array literal with spread"); - let result_reg = abi::int_result_reg(emitter); - let value_ty = assoc_spread_literal_value_type(elems, ctx); - emit_empty_assoc_array_literal(PhpType::Mixed, value_ty.clone(), emitter); - abi::emit_push_reg(emitter, result_reg); // save the merged hash while source-order spread operands are evaluated - - for elem in elems { - let elem_ty = match &elem.kind { - ExprKind::Spread(inner) => emit_expr(inner, emitter, ctx, data), - _ => continue, - }; - let helper = match elem_ty { - PhpType::AssocArray { .. } => "__rt_hash_union", - PhpType::Array(_) => "__rt_hash_array_union", - _ => continue, - }; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // pass the next spread array as the right merge operand - abi::emit_pop_reg(emitter, "x0"); // restore the accumulated named-prefix hash as the left operand - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // pass the next spread array as the right merge operand - abi::emit_pop_reg(emitter, "rdi"); // restore the accumulated named-prefix hash as the left operand - } - } - abi::emit_call_label(emitter, helper); // merge this spread operand into the named-prefix hash - abi::emit_push_reg(emitter, result_reg); // keep the updated hash available for the next spread operand - } - - abi::emit_pop_reg(emitter, result_reg); // restore the completed named-prefix hash as the expression result - PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(value_ty), - } -} - -/// Infers the value type for an associative array literal with spread elements. -/// -/// Iterates over spread expressions in `elems`, extracting the value type from -/// each `PhpType::Array` or `PhpType::AssocArray`. Returns the common value type -/// if all spreads agree; otherwise returns `PhpType::Mixed`. Falls back to -/// `PhpType::Mixed` when no spread elements are found. -fn assoc_spread_literal_value_type(elems: &[Expr], ctx: &Context) -> PhpType { - let mut value_ty = PhpType::Never; - for elem in elems { - let ExprKind::Spread(inner) = &elem.kind else { - continue; - }; - let next = match super::super::super::functions::infer_contextual_type(inner, ctx) { - PhpType::Array(elem) => *elem, - PhpType::AssocArray { value, .. } => *value, - _ => PhpType::Mixed, - }; - if matches!(value_ty, PhpType::Never) { - value_ty = next; - } else if value_ty != next { - value_ty = PhpType::Mixed; - } - } - if matches!(value_ty, PhpType::Never) { - PhpType::Mixed - } else { - value_ty - } -} - -/// Normalizes the key expression type for an associative array element. -/// -/// Infers the raw type of `key` from `ctx`, then applies array-key normalization -/// via `crate::types::normalized_array_key_type` to produce a canonical key type. -fn normalized_assoc_key_type(key: &Expr, ctx: &Context) -> PhpType { - let raw_ty = super::super::super::functions::infer_contextual_type(key, ctx); - crate::types::normalized_array_key_type(key, raw_ty) -} diff --git a/src/codegen/expr/arrays/indexed.rs b/src/codegen/expr/arrays/indexed.rs deleted file mode 100644 index 49252ba29f..0000000000 --- a/src/codegen/expr/arrays/indexed.rs +++ /dev/null @@ -1,558 +0,0 @@ -//! Purpose: -//! Lowers indexed array literals, spread elements, and platform-specific allocation paths. -//! Builds heap array values and leaves the resulting handle in expression result registers. -//! -//! Called from: -//! - `crate::codegen::expr::arrays` -//! -//! Key details: -//! - Literal emission must evaluate elements in source order and retain heap elements inserted into arrays. - -use super::super::super::context::Context; -use super::super::super::data_section::DataSection; -use super::super::super::emit::Emitter; -use super::super::super::{abi, platform::Arch}; -use super::super::{emit_expr, retain_borrowed_heap_arg, Expr, ExprKind, PhpType}; - -/// Emits a non-empty indexed array literal with no spread elements. -/// Infers the element type from the first element, picks the allocation strategy -/// (ARM64 vs x86_64, homogeneous vs mixed), and leaves the array pointer in the -/// integer result register. Calls `retain_borrowed_heap_arg` for borrowed values. -pub(crate) fn emit_array_literal( - elems: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let literal_elem_ty = infer_indexed_literal_element_type(elems, ctx); - if matches!(literal_elem_ty, PhpType::Mixed | PhpType::TaggedScalar) - && !elems.iter().any(|e| matches!(e.kind, ExprKind::Spread(_))) - { - return emit_mixed_array_literal(elems, emitter, ctx, data); - } - - if elems.iter().any(|elem| { - matches!( - &elem.kind, - ExprKind::Spread(inner) - if matches!( - super::super::super::functions::infer_contextual_type(inner, ctx), - PhpType::AssocArray { .. } - ) - ) - }) { - return super::assoc::emit_array_literal_with_assoc_spread(elems, emitter, ctx, data); - } - - if emitter.target.arch == Arch::X86_64 - && !elems.iter().any(|e| matches!(e.kind, ExprKind::Spread(_))) - { - return emit_array_literal_linux_x86_64(elems, &literal_elem_ty, emitter, ctx, data); - } - - if elems.is_empty() { - emitter.instruction("mov x0, #4"); // initial capacity: 4 (grows dynamically) - emitter.instruction("mov x1, #16"); // element size: 16 bytes (supports int and string) - emitter.instruction("bl __rt_array_new"); // call runtime to heap-allocate array struct - return PhpType::Array(Box::new(PhpType::Never)); - } - - let has_spread = elems.iter().any(|e| matches!(e.kind, ExprKind::Spread(_))); - if has_spread { - return emit_array_literal_with_spread(elems, emitter, ctx, data); - } - - let es: usize = match &literal_elem_ty { - PhpType::Str => 16, - _ => 8, - }; - - emitter.comment("array literal"); - emitter.instruction(&format!("mov x0, #{}", elems.len())); // capacity: exact element count (grows if needed) - emitter.instruction(&format!("mov x1, #{}", es)); // element size in bytes (8=int/ptr, 16=string) - emitter.instruction("bl __rt_array_new"); // call runtime to heap-allocate array struct - emitter.instruction("str x0, [sp, #-16]!"); // save array pointer on stack while filling - - let mut actual_elem_ty = literal_elem_ty.clone(); - for (i, elem) in elems.iter().enumerate() { - let mut ty = emit_expr(elem, emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut ty); - if i == 0 && actual_elem_ty == PhpType::Int { - actual_elem_ty = ty.clone(); - } - if !boxed_iterable { - retain_borrowed_heap_arg(emitter, elem, &ty); - } - emitter.instruction("ldr x9, [sp]"); // peek array pointer from stack (no pop) - if i == 0 { - emit_array_value_type_stamp(emitter, "x9", &ty); - } - match &ty { - PhpType::Int | PhpType::Bool | PhpType::Callable | PhpType::Resource(_) => { - emitter.instruction(&format!("str x0, [x9, #{}]", 24 + i * 8)); // store int/bool/callable/resource element at data offset - } - PhpType::Float => { - emitter.instruction(&format!("str d0, [x9, #{}]", 24 + i * 8)); // store float element at data offset - } - PhpType::Str => { - emitter.instruction(&format!("str x1, [x9, #{}]", 24 + i * 16)); // store string pointer at data offset - emitter.instruction(&format!("str x2, [x9, #{}]", 24 + i * 16 + 8)); // store string length right after pointer - } - PhpType::Mixed | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { - emitter.instruction(&format!("str x0, [x9, #{}]", 24 + i * 8)); // store array/object pointer at data offset - } - _ => {} - } - emitter.instruction(&format!("mov x10, #{}", i + 1)); // new length after adding this element - emitter.instruction("str x10, [x9]"); // write updated length to array header - } - - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer from stack into x0 - PhpType::Array(Box::new(actual_elem_ty)) -} - -/// x86_64-specific path for non-empty indexed array literals with no spread elements. -/// Uses the System V ABI (rdi=rdi=rsi=elem_size) and returns the array pointer in rax. -/// Falls back to the shared runtime allocator if the literal is empty. -fn emit_array_literal_linux_x86_64( - elems: &[Expr], - literal_elem_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if elems.is_empty() { - abi::emit_load_int_immediate(emitter, "rdi", 4); // initial capacity: four slots for a still-unspecialized empty array - abi::emit_load_int_immediate(emitter, "rsi", 16); // default empty-array slots are wide enough for strings until first write specializes them - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the empty indexed array through the shared runtime helper - return PhpType::Array(Box::new(PhpType::Never)); - } - - let elem_size = match literal_elem_ty { - PhpType::Str => 16, - _ => 8, - }; - let capacity = elems.len().max(4); - - emitter.comment("array literal"); - abi::emit_load_int_immediate(emitter, "rdi", capacity as i64); // choose an indexed-array capacity that matches the x86_64 literal size policy - abi::emit_load_int_immediate(emitter, "rsi", elem_size as i64); // choose the runtime element slot width that matches the inferred literal element family - abi::emit_call_label(emitter, "__rt_array_new"); // allocate a real elephc indexed array so heap headers and runtime metadata stay valid on x86_64 - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save array pointer on stack while filling literal elements - - let mut actual_elem_ty = literal_elem_ty.clone(); - for (i, elem) in elems.iter().enumerate() { - let mut ty = emit_expr(elem, emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut ty); - if i == 0 && actual_elem_ty == PhpType::Int { - actual_elem_ty = ty.clone(); - } - if !boxed_iterable { - retain_borrowed_heap_arg(emitter, elem, &ty); - } - emitter.instruction("mov r11, QWORD PTR [rsp]"); // peek array pointer from the temporary stack slot - if i == 0 { - emit_array_value_type_stamp(emitter, "r11", &ty); // stamp the packed x86_64 array value_type tag once the first literal element fixes the runtime family - } - match &ty { - PhpType::Int | PhpType::Bool | PhpType::Callable | PhpType::Resource(_) => { - abi::emit_store_to_address( - emitter, - abi::int_result_reg(emitter), - "r11", - 24 + i * 8, - ); - } - PhpType::Float => { - abi::emit_store_to_address( - emitter, - abi::float_result_reg(emitter), - "r11", - 24 + i * 8, - ); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_store_to_address(emitter, ptr_reg, "r11", 24 + i * 16); - abi::emit_store_to_address(emitter, len_reg, "r11", 24 + i * 16 + 8); - } - PhpType::Mixed | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { - abi::emit_store_to_address( - emitter, - abi::int_result_reg(emitter), - "r11", - 24 + i * 8, - ); - } - _ => {} - } - abi::emit_load_int_immediate(emitter, "r10", (i + 1) as i64); // materialize the logical indexed-array length after inserting this literal element - abi::emit_store_to_address(emitter, "r10", "r11", 0); // publish the updated indexed-array length in the real array header - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return array pointer in the target integer result register - PhpType::Array(Box::new(actual_elem_ty)) -} - -/// Emits an indexed array literal where all elements have PhpType::Mixed or the -/// inferred element type is PhpType::Mixed. Boxes non-Mixed elements into a PhpValue::Mixed -/// cell before storing. Stamps the array value_type as PhpType::Mixed. Dispatches to -/// the x86_64 path on that architecture. -fn emit_mixed_array_literal( - elems: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if emitter.target.arch == Arch::X86_64 { - return emit_mixed_array_literal_linux_x86_64(elems, emitter, ctx, data); - } - - emitter.comment("mixed array literal"); - emitter.instruction(&format!("mov x0, #{}", elems.len())); // capacity: exact element count for the mixed indexed literal - emitter.instruction("mov x1, #8"); // boxed Mixed slots store one pointer each - emitter.instruction("bl __rt_array_new"); // allocate the indexed array backing storage - emitter.instruction("str x0, [sp, #-16]!"); // save array pointer on stack while filling mixed slots - emitter.instruction("ldr x9, [sp]"); // reload the array pointer for value_type stamping - emit_array_value_type_stamp(emitter, "x9", &PhpType::Mixed); - - for (i, elem) in elems.iter().enumerate() { - let mut ty = emit_expr(elem, emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut ty); - if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, elem, &ty, - ); - } else if !boxed_iterable { - retain_borrowed_heap_arg(emitter, elem, &ty); - } - emitter.instruction("ldr x9, [sp]"); // peek array pointer from stack before storing this Mixed slot - emitter.instruction(&format!("str x0, [x9, #{}]", 24 + i * 8)); // store the boxed Mixed pointer at the indexed slot - emitter.instruction(&format!("mov x10, #{}", i + 1)); // new length after adding this mixed element - emitter.instruction("str x10, [x9]"); // write updated length to array header - } - - emitter.instruction("ldr x0, [sp], #16"); // pop array pointer into the expression result register - PhpType::Array(Box::new(PhpType::Mixed)) -} - -/// x86_64-specific path for mixed array literals. Boxes each element as Mixed, stamps -/// value_type as PhpType::Mixed, returns array pointer in rax. -fn emit_mixed_array_literal_linux_x86_64( - elems: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("mixed array literal"); - abi::emit_load_int_immediate(emitter, "rdi", elems.len() as i64); // choose exact capacity for the mixed indexed literal - abi::emit_load_int_immediate(emitter, "rsi", 8); // boxed Mixed slots store one pointer each - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the indexed array backing storage - abi::emit_push_reg(emitter, "rax"); // save array pointer on stack while filling mixed slots - emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the array pointer for value_type stamping - emit_array_value_type_stamp(emitter, "r11", &PhpType::Mixed); - - for (i, elem) in elems.iter().enumerate() { - let mut ty = emit_expr(elem, emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut ty); - if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, elem, &ty, - ); - } else if !boxed_iterable { - retain_borrowed_heap_arg(emitter, elem, &ty); - } - emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the array pointer before storing this Mixed slot - abi::emit_store_to_address(emitter, "rax", "r11", 24 + i * 8); - abi::emit_load_int_immediate(emitter, "r10", (i + 1) as i64); // materialize the logical length after inserting this mixed element - abi::emit_store_to_address(emitter, "r10", "r11", 0); // publish the updated indexed-array length - } - - abi::emit_pop_reg(emitter, "rax"); // return array pointer in the x86_64 expression result register - PhpType::Array(Box::new(PhpType::Mixed)) -} - -/// Emits an indexed array literal containing one or more spread elements (e.g. `[...$a, 1, ...$b]`). -/// Each spread is merged via `__rt_array_merge_into` or `__rt_array_merge_into_refcounted` -/// depending on whether the source array holds refcounted elements. Non-spread elements are pushed -/// via typed push helpers. The array pointer is left in the integer result register. -pub(crate) fn emit_array_literal_with_spread( - elems: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if emitter.target.arch == Arch::X86_64 { - return emit_array_literal_with_spread_linux_x86_64(elems, emitter, ctx, data); - } - - emitter.comment("array literal with spread"); - emitter.instruction("mov x0, #16"); // initial capacity: 16 elements - emitter.instruction("mov x1, #8"); // element size: 8 bytes (int-sized) - emitter.instruction("bl __rt_array_new"); // allocate destination array - emitter.instruction("str x0, [sp, #-16]!"); // save dest array pointer on stack - - let mut actual_elem_ty = PhpType::Int; - - for (i, elem) in elems.iter().enumerate() { - if let ExprKind::Spread(inner) = &elem.kind { - emitter.comment("spread array into dest"); - let src_ty = emit_expr(inner, emitter, ctx, data); - if (i == 0 || actual_elem_ty == PhpType::Int) - && matches!(&src_ty, PhpType::Array(_)) - { - if let PhpType::Array(inner) = &src_ty { - actual_elem_ty = inner.as_ref().clone(); - } - } - emitter.instruction("mov x1, x0"); // x1 = source array pointer - emitter.instruction("ldr x0, [sp]"); // x0 = dest array pointer (peek) - if matches!(&src_ty, PhpType::Array(inner) if inner.is_refcounted()) { - emitter.instruction("bl __rt_array_merge_into_refcounted"); // append src elements while retaining borrowed heap payloads - } else { - emitter.instruction("bl __rt_array_merge_into"); // append all src elements to dest array - } - emitter.instruction("str x0, [sp]"); // persist the possibly-grown dest array pointer after the spread merge - } else { - let mut ty = emit_expr(elem, emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut ty); - if i == 0 || actual_elem_ty == PhpType::Int { - actual_elem_ty = ty.clone(); - } - if !boxed_iterable { - retain_borrowed_heap_arg(emitter, elem, &ty); - } - emitter.instruction("ldr x9, [sp]"); // peek dest array pointer from stack - match &ty { - PhpType::Int | PhpType::Bool => { - emitter.instruction("mov x1, x0"); // x1 = value to push - emitter.instruction("mov x0, x9"); // x0 = array pointer - emitter.instruction("bl __rt_array_push_int"); // push value onto array - emitter.instruction("str x0, [sp]"); // persist the possibly-grown dest array pointer after the push - } - PhpType::Float => { - emitter.instruction("fmov x1, d0"); // move float bits to int register - emitter.instruction("mov x0, x9"); // x0 = array pointer - emitter.instruction("bl __rt_array_push_int"); // push value onto array - emitter.instruction("str x0, [sp]"); // persist the possibly-grown dest array pointer after the push - } - _ => { - if ty.is_refcounted() { - // The codegen owns one reference to the element here (a fresh literal, - // or a borrowed value retained by `retain_borrowed_heap_arg` above). - // `__rt_array_push_refcounted` retains its own reference for the - // destination array, so the codegen's reference must be released - // afterward or the element leaks. - abi::emit_push_reg(emitter, "x0"); // save the codegen-owned element across the append helper - emitter.instruction("mov x1, x0"); // x1 = value to push - emitter.instruction("mov x0, x9"); // x0 = array pointer - emitter.instruction("bl __rt_array_push_refcounted"); // push retained refcounted payload and stamp array metadata - crate::codegen::emit_release_pushed_refcounted_temp_after_array_push(emitter, &ty); // drop the codegen's owning reference now that the array holds its own - } else { - emitter.instruction("mov x1, x0"); // x1 = value to push - emitter.instruction("mov x0, x9"); // x0 = array pointer - emitter.instruction("bl __rt_array_push_int"); // push value onto array - } - emitter.instruction("str x0, [sp]"); // persist the possibly-grown dest array pointer after the push - } - } - } - } - - emitter.instruction("ldr x0, [sp], #16"); // pop dest array pointer from stack into x0 - PhpType::Array(Box::new(actual_elem_ty)) -} - -/// x86_64-specific path for indexed array literals with spread elements. Uses the System V ABI -/// (rdi=dest_array, rsi=src_array) and returns the array pointer in rax. -fn emit_array_literal_with_spread_linux_x86_64( - elems: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("array literal with spread"); - emitter.instruction("mov rdi, 16"); // seed the destination indexed array with the same fixed initial capacity used by the ARM64 spread helper - emitter.instruction("mov rsi, 8"); // use 8-byte slots because this helper still constructs scalar or pointer packed indexed arrays - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the destination indexed array through the x86_64 runtime constructor - abi::emit_push_reg(emitter, "rax"); // preserve the destination indexed-array pointer on the stack while evaluating spread sources and explicit elements - - let mut actual_elem_ty = PhpType::Int; - - for (i, elem) in elems.iter().enumerate() { - if let ExprKind::Spread(inner) = &elem.kind { - emitter.comment("spread array into dest"); - let src_ty = emit_expr(inner, emitter, ctx, data); - if (i == 0 || actual_elem_ty == PhpType::Int) - && matches!(&src_ty, PhpType::Array(_)) - { - if let PhpType::Array(inner) = &src_ty { - actual_elem_ty = inner.as_ref().clone(); - } - } - emitter.instruction("mov rsi, rax"); // place the source indexed-array pointer in the x86_64 merge helper source register - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the destination indexed-array pointer from the stack without disturbing the literal construction state - if matches!(&src_ty, PhpType::Array(inner) if inner.is_refcounted()) { - abi::emit_call_label(emitter, "__rt_array_merge_into_refcounted"); // append retained child pointers from the source indexed array into the destination - } else { - abi::emit_call_label(emitter, "__rt_array_merge_into"); // append plain scalar payloads from the source indexed array into the destination - } - emitter.instruction("mov QWORD PTR [rsp], rax"); // persist the possibly-grown destination indexed-array pointer after the spread merge - } else { - let mut ty = emit_expr(elem, emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut ty); - if i == 0 || actual_elem_ty == PhpType::Int { - actual_elem_ty = ty.clone(); - } - if !boxed_iterable { - retain_borrowed_heap_arg(emitter, elem, &ty); - } - emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the destination indexed-array pointer from the stack without popping it - match &ty { - PhpType::Int | PhpType::Bool => { - emitter.instruction("mov rsi, rax"); // place the scalar payload in the x86_64 append helper value register - emitter.instruction("mov rdi, r11"); // place the destination indexed-array pointer in the x86_64 append helper receiver register - abi::emit_call_label(emitter, "__rt_array_push_int"); // append the scalar payload into the destination indexed array - emitter.instruction("mov QWORD PTR [rsp], rax"); // persist the possibly-grown destination indexed-array pointer after the append - } - PhpType::Float => { - emitter.instruction("movq rsi, xmm0"); // move the floating-point payload bits into the scalar append helper value register - emitter.instruction("mov rdi, r11"); // place the destination indexed-array pointer in the x86_64 append helper receiver register - abi::emit_call_label(emitter, "__rt_array_push_int"); // append the floating-point payload bits as an 8-byte scalar slot - emitter.instruction("mov QWORD PTR [rsp], rax"); // persist the possibly-grown destination indexed-array pointer after the append - } - PhpType::Str => { - emitter.instruction("mov rsi, rax"); // place the string pointer in the x86_64 string append helper payload register - emitter.instruction("mov rdi, r11"); // place the destination indexed-array pointer in the x86_64 string append helper receiver register - abi::emit_call_label(emitter, "__rt_array_push_str"); // persist and append the string payload into the destination indexed array - emitter.instruction("mov QWORD PTR [rsp], rax"); // persist the possibly-grown destination indexed-array pointer after the append - } - _ => { - if ty.is_refcounted() { - // See the AArch64 arm: the codegen owns one reference to the element, - // and `__rt_array_push_refcounted` retains its own, so the codegen's - // reference must be released afterward to avoid leaking the element. - abi::emit_push_reg(emitter, "rax"); // save the codegen-owned element across the append helper - emitter.instruction("mov rsi, rax"); // place the payload pointer in the shared x86_64 append helper value register - emitter.instruction("mov rdi, r11"); // place the destination indexed-array pointer in the shared x86_64 append helper receiver register - abi::emit_call_label(emitter, "__rt_array_push_refcounted"); // append the retained refcounted payload and stamp the indexed-array value_type metadata - crate::codegen::emit_release_pushed_refcounted_temp_after_array_push(emitter, &ty); // drop the codegen's owning reference now that the array holds its own - } else { - emitter.instruction("mov rsi, rax"); // place the payload bits in the shared x86_64 append helper value register - emitter.instruction("mov rdi, r11"); // place the destination indexed-array pointer in the shared x86_64 append helper receiver register - abi::emit_call_label(emitter, "__rt_array_push_int"); // append the payload bits through the scalar append helper - } - emitter.instruction("mov QWORD PTR [rsp], rax"); // persist the possibly-grown destination indexed-array pointer after the append - } - } - } - } - - abi::emit_pop_reg(emitter, "rax"); // pop the completed destination indexed-array pointer into the standard x86_64 expression result register - PhpType::Array(Box::new(actual_elem_ty)) -} - -/// Writes the runtime value_type tag into the array header's packed kind word. -pub(crate) fn emit_array_value_type_stamp( - emitter: &mut Emitter, - array_reg: &str, - elem_ty: &PhpType, -) { - let value_type_tag = match elem_ty { - PhpType::Float => 2, - PhpType::Bool => 3, - PhpType::Str => 1, - PhpType::Array(_) => 4, - PhpType::AssocArray { .. } => 5, - PhpType::Object(_) => 6, - PhpType::Mixed => 7, - PhpType::Union(_) => 7, - PhpType::Void => 8, - _ => return, - }; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x10, [{}, #-8]", array_reg)); // load the packed array kind word from the heap header - emitter.instruction("mov x12, #0x80ff"); // preserve the indexed-array kind and persistent COW flag - emitter.instruction("and x10, x10, x12"); // keep only the persistent indexed-array metadata bits - emitter.instruction(&format!("mov x11, #{}", value_type_tag)); // materialize the runtime array value_type tag - emitter.instruction("lsl x11, x11, #8"); // move the value_type tag into the packed kind-word byte lane - emitter.instruction("orr x10, x10, x11"); // combine the heap kind with the array value_type tag - emitter.instruction(&format!("str x10, [{}, #-8]", array_reg)); // persist the packed array kind word in the heap header - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "r12"); // preserve the x86_64 nested-call scratch register before reusing it as a temporary array-stamp helper - emitter.instruction(&format!("mov r10, QWORD PTR [{} - 8]", array_reg)); // load the packed array kind word from the heap header - emitter.instruction("mov r12, 0xffffffff000080ff"); // materialize the x86_64 heap-kind preservation mask without clobbering the array base register - emitter.instruction("and r10, r12"); // preserve the x86_64 heap magic marker plus the indexed-array kind and persistent COW flag - emitter.instruction(&format!("mov r12, {}", value_type_tag)); // materialize the runtime array value_type tag in a scratch register that does not alias the array base register - emitter.instruction("shl r12, 8"); // move the value_type tag into the packed kind-word byte lane - emitter.instruction("or r10, r12"); // combine the preserved heap kind with the stamped array value_type tag - emitter.instruction(&format!("mov QWORD PTR [{} - 8], r10", array_reg)); // persist the packed array kind word in the heap header - abi::emit_pop_reg(emitter, "r12"); // restore the x86_64 nested-call scratch register after the array value-type stamp is complete - } - } -} - -/// Infers the homogeneous element type for an indexed array literal by scanning all elements -/// (including Spread nodes). Returns the merged type, or PhpType::Mixed when elements have -/// heterogeneous types. Iterable types are treated as Mixed. -fn infer_indexed_literal_element_type(elems: &[Expr], ctx: &Context) -> PhpType { - let mut elem_ty = PhpType::Never; - for (i, elem) in elems.iter().enumerate() { - let next_ty = match &elem.kind { - ExprKind::Spread(inner) => match crate::codegen::functions::infer_contextual_type(inner, ctx) { - PhpType::Array(inner_ty) => *inner_ty, - _ => PhpType::Mixed, - }, - _ => crate::codegen::functions::infer_contextual_type(elem, ctx), - }; - let next_ty = if matches!(next_ty, PhpType::Iterable) { - PhpType::Mixed - } else { - next_ty - }; - if i == 0 { - elem_ty = next_ty; - } else { - elem_ty = merge_indexed_literal_element_type(&elem_ty, &next_ty, ctx); - } - } - elem_ty -} - -/// Merges two element types from consecutive positions in an indexed array literal. -/// Returns the broader type when types differ (e.g. int + float → Mixed), or the common -/// type when they match. Object types are resolved via `ctx.common_object_type`. -fn merge_indexed_literal_element_type( - existing: &PhpType, - next: &PhpType, - ctx: &Context, -) -> PhpType { - if existing == next { - return existing.clone(); - } - if matches!(existing, PhpType::Never) { - return next.clone(); - } - if matches!(next, PhpType::Never) { - return existing.clone(); - } - if matches!(existing, PhpType::Mixed | PhpType::Union(_) | PhpType::TaggedScalar) - || matches!(next, PhpType::Mixed | PhpType::Union(_) | PhpType::TaggedScalar) - { - return PhpType::Mixed; - } - if let (PhpType::Object(left), PhpType::Object(right)) = (existing, next) { - return ctx - .common_object_type(left, right) - .unwrap_or(PhpType::Mixed); - } - PhpType::Mixed -} diff --git a/src/codegen/expr/assignment.rs b/src/codegen/expr/assignment.rs deleted file mode 100644 index d184e48ad0..0000000000 --- a/src/codegen/expr/assignment.rs +++ /dev/null @@ -1,250 +0,0 @@ -//! Purpose: -//! Lowers assignment expressions that appear where an expression result is required. -//! Bridges statement assignment machinery with expression result preservation. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Writes must happen once and the assigned value must remain available in the expected result registers. - -use super::super::context::Context; -use super::super::data_section::DataSection; -use super::super::emit::Emitter; -use crate::parser::ast::{Expr, ExprKind, Stmt, StmtKind}; -use crate::types::PhpType; - -/// Emits an assignment expression that also serves as an expression result. -/// -/// For simple local variable targets, emits the assignment statement then emits -/// the result target (or the variable itself if no result target is specified). -/// For non-local targets (array access, property access), delegates to the -/// non-local machinery which writes first then evaluates the result expression. -/// -/// The `prelude` contains any leading assignment statements (e.g., from spread -/// argument preprocessing). The `conditional_value_temp` is set when a null -/// coalescing assignment has a non-null current value that must be preserved -/// across the default branch. -/// -/// Returns the PHP type of the result expression. -pub(super) fn emit_assignment_expr( - target: &Expr, - value: &Expr, - result_target: Option<&Expr>, - prelude: &[Stmt], - conditional_value_temp: Option<&str>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_assignment_prelude(prelude, emitter, ctx, data); - - if let Some(temp_name) = conditional_value_temp { - if let Some(ty) = emit_conditional_non_local_null_coalesce_assignment( - temp_name, - target, - value, - result_target, - emitter, - ctx, - data, - ) { - return ty; - } - } - - let ExprKind::Variable(name) = &target.kind else { - return emit_non_local_assignment_expr(target, value, result_target, emitter, ctx, data); - }; - - super::super::stmt::emit_assign_stmt(name, value, emitter, ctx, data); - match result_target { - Some(other) if !is_same_local(other, name) => { - super::emit_expr(other, emitter, ctx, data) - } - _ => super::variables::emit_variable(name, emitter, ctx), - } -} - -/// Returns true if `expr` is a Variable node with the same name as `name`. -fn is_same_local(expr: &Expr, name: &str) -> bool { - matches!(&expr.kind, ExprKind::Variable(other) if other == name) -} - -/// Emits an assignment expression with a non-local target (array access, property, etc.). -/// -/// Writes the value to the target first, then evaluates and returns the result expression -/// (or the target itself if no result target is given). Unlike local variable assignment, -/// the write must occur before the result is computed because the target may involve -/// intermediate expressions or memory that would be clobbered by result evaluation. -pub(super) fn emit_non_local_assignment_expr( - target: &Expr, - value: &Expr, - result_target: Option<&Expr>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_non_local_assignment_write(target, value, emitter, ctx, data); - - super::emit_expr(result_target.unwrap_or(target), emitter, ctx, data) -} - -/// Emits a null-coalescing assignment expression with a non-local target. -/// -/// Handles the case where the right-hand side is a NullCoalesce node, and the current -/// value may be a Mixed or Union type requiring special handling. If the current value -/// is non-null, emits the result target (preserving the current value via a temporary -/// on the stack for Mixed/Union types) and jumps to done. If the current value is null, -/// evaluates the default, assigns it to `temp_name`, writes it to the non-local target, -/// and uses the default as the result. -/// -/// Returns `None` if the value is not a NullCoalesce expression (caller should fall back -/// to a regular non-local assignment). Returns `Some(PhpType)` with the widened result type. -fn emit_conditional_non_local_null_coalesce_assignment( - temp_name: &str, - target: &Expr, - value: &Expr, - result_target: Option<&Expr>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let ExprKind::NullCoalesce { - value: current, - default, - } = &value.kind - else { - return None; - }; - - if matches!(default.kind, ExprKind::Null) { - return Some(super::emit_expr(result_target.unwrap_or(target), emitter, ctx, data)); - } - - let current_ty = super::emit_expr(current, emitter, ctx, data); - let keep_label = (current_ty != PhpType::Void) - .then(|| ctx.next_label("nca_expr_keep")); - let done_label = keep_label - .as_ref() - .map(|_| ctx.next_label("nca_expr_done")); - let saved_current_bytes = if matches!(current_ty, PhpType::Mixed | PhpType::Union(_)) { - crate::codegen::abi::emit_push_reg( - emitter, - crate::codegen::abi::int_result_reg(emitter), - ); // preserve the boxed current value while the null check unboxes its tag - 16 - } else { - 0 - }; - if let Some(label) = &keep_label { - super::super::stmt::emit_branch_if_result_non_null(¤t_ty, label, emitter); - } - super::super::stmt::emit_assign_stmt(temp_name, default, emitter, ctx, data); - let temp_value = Expr::new(ExprKind::Variable(temp_name.to_string()), default.span); - emit_non_local_assignment_write(target, &temp_value, emitter, ctx, data); - let default_ty = super::emit_expr(&temp_value, emitter, ctx, data); - let result_ty = super::widen_codegen_type(¤t_ty, &default_ty); - super::coerce_result_to_type(emitter, ctx, data, &default_ty, &result_ty); - if saved_current_bytes != 0 { - crate::codegen::abi::emit_release_temporary_stack(emitter, saved_current_bytes); // discard the saved null value on the default-assignment path - } - if let Some(label) = &done_label { - crate::codegen::abi::emit_jump(emitter, label); // keep the just-assigned default value as the expression result - } - if let Some(label) = &keep_label { - emitter.label(label); - if saved_current_bytes != 0 { - crate::codegen::abi::emit_pop_reg( - emitter, - crate::codegen::abi::int_result_reg(emitter), - ); // restore the original boxed current value for the keep-existing path - } - super::coerce_result_to_type(emitter, ctx, data, ¤t_ty, &result_ty); - } - if let Some(label) = &done_label { - emitter.label(label); - } - - Some(result_ty) -} - -/// Emits the write half of a non-local assignment expression. -/// -/// Dispatches to the appropriate statement emitter based on the target expression kind: -/// - ArrayAccess on a Variable: `emit_array_assign_stmt` -/// - ArrayAccess on a PropertyAccess: `emit_property_array_assign_stmt` -/// - ArrayAccess on a StaticPropertyAccess: `emit_static_property_array_assign_stmt` -/// - ArrayAccess on a nested expression: `emit_nested_array_assign_stmt` -/// - PropertyAccess: `emit_property_assign_stmt` -/// - StaticPropertyAccess: `emit_static_property_assign_stmt` -/// Falls through to a warning comment for unsupported targets. -fn emit_non_local_assignment_write( - target: &Expr, - value: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - match &target.kind { - ExprKind::ArrayAccess { array, index } => match &array.kind { - ExprKind::Variable(array) => { - super::super::stmt::emit_array_assign_stmt(array, index, value, emitter, ctx, data); - } - ExprKind::PropertyAccess { object, property } => { - super::super::stmt::emit_property_array_assign_stmt( - object, property, index, value, emitter, ctx, data, - ); - } - ExprKind::StaticPropertyAccess { receiver, property } => { - super::super::stmt::emit_static_property_array_assign_stmt( - receiver, property, index, value, emitter, ctx, data, - ); - } - _ => { - super::super::stmt::emit_nested_array_assign_stmt( - target, value, emitter, ctx, data, - ); - } - }, - ExprKind::PropertyAccess { object, property } => { - super::super::stmt::emit_property_assign_stmt( - object, property, value, emitter, ctx, data, - ); - } - ExprKind::StaticPropertyAccess { receiver, property } => { - super::super::stmt::emit_static_property_assign_stmt( - receiver, property, value, emitter, ctx, data, - ); - } - _ => { - emitter.comment("WARNING: assignment expression target is not supported in codegen"); - } - } -} - -/// Emits any leading statements that precede the assignment expression. -/// -/// Iterates over `prelude` statements and emits each one. Assign statements are emitted -/// via `emit_assign_stmt` directly. Synthetic statements are handled recursively. -/// All other statement kinds are emitted via the standard statement emitter. -fn emit_assignment_prelude( - prelude: &[Stmt], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - for stmt in prelude { - match &stmt.kind { - StmtKind::Assign { name, value } => { - super::super::stmt::emit_assign_stmt(name, value, emitter, ctx, data); - } - StmtKind::Synthetic(stmts) => { - emit_assignment_prelude(stmts, emitter, ctx, data); - } - _ => { - super::super::stmt::emit_stmt(stmt, emitter, ctx, data); - } - } - } -} diff --git a/src/codegen/expr/binops/arithmetic.rs b/src/codegen/expr/binops/arithmetic.rs deleted file mode 100644 index 539195c6da..0000000000 --- a/src/codegen/expr/binops/arithmetic.rs +++ /dev/null @@ -1,570 +0,0 @@ -//! Purpose: -//! Lowers numeric arithmetic and modulo operators with PHP-compatible coercions. -//! Keeps operator-specific conversions and result register setup out of the dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::binops` -//! -//! Key details: -//! - Runtime calls and target instructions must preserve left/right evaluation order and scratch register assumptions. - -use super::super::super::context::Context; -use super::super::super::data_section::DataSection; -use super::super::super::emit::Emitter; -use super::super::super::{abi, platform::Arch}; -use super::target::{ - emit_float_binop, emit_promote_int_to_float, emit_set_bool_from_flags, -}; -use super::super::{ - coerce_null_to_zero, coerce_to_int, coerce_to_string_releasing_owned, coerce_to_truthiness, - emit_expr, - expr_result_heap_ownership, string_result_is_owned_call_temp, - string_result_uses_transient_concat_buffer, BinOp, Expr, PhpType, -}; -use crate::codegen::context::HeapOwnership; - -/// Lowers &&, ||, xor logical operators. -pub(super) fn emit_logical_binop( - left: &Expr, - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - match op { - BinOp::And => { - let end_label = ctx.next_label("and_end"); - let left_ty = emit_expr(left, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &left_ty); - abi::emit_branch_if_int_result_zero(emitter, &end_label); - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &right_ty); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("cmp x0, #0"), // test whether right-operand truthiness is zero (false) - Arch::X86_64 => emitter.instruction("test rax, rax"), // test whether right-operand truthiness is zero (false) - } - emit_set_bool_from_flags(emitter, "ne"); - emitter.label(&end_label); - PhpType::Bool - } - BinOp::Or => { - let end_label = ctx.next_label("or_end"); - let left_ty = emit_expr(left, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &left_ty); - abi::emit_branch_if_int_result_nonzero(emitter, &end_label); - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &right_ty); - emitter.label(&end_label); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("cmp x0, #0"), // test whether right-operand truthiness is zero (false) - Arch::X86_64 => emitter.instruction("test rax, rax"), // test whether right-operand truthiness is zero (false) - } - emit_set_bool_from_flags(emitter, "ne"); - PhpType::Bool - } - BinOp::Xor => { - let left_ty = emit_expr(left, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &left_ty); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &right_ty); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x9"); - emitter.instruction("eor x0, x9, x0"); // true when exactly one operand is truthy - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "r10"); - emitter.instruction("xor rax, r10"); // true when exactly one operand is truthy - } - } - PhpType::Bool - } - _ => unreachable!(), - } -} - -/// Lowers the ** exponentiation operator using libc pow. -pub(super) fn emit_pow_binop( - left: &Expr, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let left_ty = emit_expr(left, emitter, ctx, data); - coerce_to_int(emitter, &left_ty); - if left_ty != PhpType::Float { - emit_promote_int_to_float( - emitter, - abi::float_result_reg(emitter), - abi::int_result_reg(emitter), - ); - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_int(emitter, &right_ty); - if right_ty != PhpType::Float { - emit_promote_int_to_float( - emitter, - abi::float_result_reg(emitter), - abi::int_result_reg(emitter), - ); - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fmov d1, d0"); // move right-operand float into d1 (second pow argument) - abi::emit_pop_float_reg(emitter, "d0"); - emitter.bl_c("pow"); - } - Arch::X86_64 => { - abi::emit_pop_float_reg(emitter, "xmm1"); - emitter.instruction("movapd xmm2, xmm0"); // stash right-operand float before shuffling pow argument registers - emitter.instruction("movapd xmm0, xmm1"); // place left-operand float into xmm0 (first pow argument) - emitter.instruction("movapd xmm1, xmm2"); // place right-operand float into xmm1 (second pow argument) - emitter.instruction("call pow"); // invoke libc pow(xmm0, xmm1); result returned in xmm0 - } - } - PhpType::Float -} - -/// Lowers +, -, *, /, % operators with PHP-compatible numeric coercion. -pub(super) fn emit_numeric_binop( - left: &Expr, - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let left_ty = emit_expr(left, emitter, ctx, data); - let dynamic_candidate = matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul); - let left_stack_ty = if dynamic_candidate && left_ty == PhpType::Void { - coerce_null_to_zero(emitter, &left_ty); - PhpType::Int - } else if left_ty == PhpType::TaggedScalar { - // narrow a tagged scalar (null -> 0) before the operand is spilled as one word - coerce_null_to_zero(emitter, &left_ty); - PhpType::Int - } else if dynamic_candidate { - left_ty.clone() - } else { - coerce_to_int(emitter, &left_ty); - left_ty.clone() - }; - let use_float = left_stack_ty == PhpType::Float; - if use_float { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - } else { - abi::emit_push_result_value(emitter, &left_stack_ty); - } - let right_ty = emit_expr(right, emitter, ctx, data); - - if should_emit_mixed_numeric_binop(op, &left_stack_ty, &right_ty) { - return emit_mixed_numeric_binop( - left, - op, - &left_stack_ty, - right, - &right_ty, - emitter, - ); - } - - coerce_to_int(emitter, &right_ty); - - if left_stack_ty == PhpType::Float || right_ty == PhpType::Float || *op == BinOp::Div { - if right_ty != PhpType::Float { - emit_promote_int_to_float( - emitter, - abi::float_result_reg(emitter), - abi::int_result_reg(emitter), - ); - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - if left_stack_ty == PhpType::Float { - let left_float_reg = match emitter.target.arch { - Arch::AArch64 => "d1", - Arch::X86_64 => "xmm1", - }; - abi::emit_load_temporary_stack_slot(emitter, left_float_reg, 16); - } else { - let left_int_reg = abi::symbol_scratch_reg(emitter); - let left_float_reg = match emitter.target.arch { - Arch::AArch64 => "d1", - Arch::X86_64 => "xmm1", - }; - abi::emit_load_temporary_stack_slot(emitter, left_int_reg, 16); - emit_promote_int_to_float(emitter, left_float_reg, left_int_reg); - } - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); - emit_float_binop(emitter, op); - abi::emit_release_temporary_stack(emitter, 16); - PhpType::Float - } else { - let left_reg = match emitter.target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "r10", - }; - let result_reg = abi::int_result_reg(emitter); - abi::emit_pop_reg(emitter, left_reg); - match op { - BinOp::Add => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("add x0, x1, x0"), // x0 = left (x1) + right (x0) - Arch::X86_64 => { - emitter.instruction(&format!("add {}, {}", left_reg, result_reg)); // left_reg += result_reg (right operand) - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the sum back into the result register - } - }, - BinOp::Sub => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("sub x0, x1, x0"), // x0 = left (x1) - right (x0) - Arch::X86_64 => { - emitter.instruction(&format!("sub {}, {}", left_reg, result_reg)); // left_reg -= result_reg (right operand) - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the difference back into the result register - } - }, - BinOp::Mul => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("mul x0, x1, x0"), // x0 = left (x1) * right (x0) - Arch::X86_64 => { - emitter.instruction(&format!("imul {}, {}", left_reg, result_reg)); // left_reg *= result_reg (right operand) - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the product back into the result register - } - }, - BinOp::Div => { - emitter.instruction("sdiv x0, x1, x0"); // x0 = left (x1) / right (x0) (signed division) - } - BinOp::Mod => emit_int_mod(emitter, ctx, left_reg, result_reg), - _ => unreachable!(), - } - PhpType::Int - } -} - -/// Returns true when the given operator and operand types require mixed-numeric binop emission. -/// Only Add, Sub, and Mul can operate on Mixed/Union types; integerish pairs also use this path -/// to bypass normal int/float coercions when both operands fit in integers. -fn should_emit_mixed_numeric_binop(op: &BinOp, left_ty: &PhpType, right_ty: &PhpType) -> bool { - if !matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul) { - return false; - } - if matches!(left_ty, PhpType::Mixed | PhpType::Union(_)) - || matches!(right_ty, PhpType::Mixed | PhpType::Union(_)) - { - return true; - } - is_integerish_numeric(left_ty) && is_integerish_numeric(right_ty) -} - -/// Returns true for PhpType values that represent integer-compatible PHP types: -/// Int, Bool (coerced to int), and Void (null coerced to zero). -fn is_integerish_numeric(ty: &PhpType) -> bool { - matches!(ty, PhpType::Int | PhpType::Bool | PhpType::Void) -} - -/// Emits a mixed-numeric add/sub/mul operation. -/// At least one operand is `PhpType::Mixed` or `PhpType::Union`; the other may be a concrete -/// integer type that was previously on the expression stack. Non-Mixed operands are boxed as -/// Mixed before the runtime helper is called. The result type is always `PhpType::Mixed`. -/// Ownership: owned operands are released after the call; borrowed or static operands are not. -fn emit_mixed_numeric_binop( - left: &Expr, - op: &BinOp, - left_stack_ty: &PhpType, - right: &Expr, - right_ty: &PhpType, - emitter: &mut Emitter, -) -> PhpType { - let right_was_boxed = !matches!(right_ty, PhpType::Mixed | PhpType::Union(_)); - let left_was_boxed = !matches!(left_stack_ty, PhpType::Mixed | PhpType::Union(_)); - let release_left_operand = - left_was_boxed || mixed_numeric_operand_is_owned(left, left_stack_ty); - let release_right_operand = - right_was_boxed || mixed_numeric_operand_is_owned(right, right_ty); - if right_was_boxed { - crate::codegen::emit_box_current_value_as_mixed(emitter, right_ty); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - load_saved_numeric_operand(emitter, left_stack_ty, 16); - if left_was_boxed { - crate::codegen::emit_box_current_value_as_mixed(emitter, left_stack_ty); - } - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(emitter, "x1"); - } - Arch::X86_64 => { - abi::emit_pop_reg(emitter, "rdi"); - } - } - abi::emit_release_temporary_stack(emitter, 16); - let helper = match op { - BinOp::Add => "__rt_mixed_numeric_add", - BinOp::Sub => "__rt_mixed_numeric_sub", - BinOp::Mul => "__rt_mixed_numeric_mul", - _ => unreachable!(), - }; - if release_left_operand { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - if release_right_operand { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, "x1"); - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rdi"); - } - } - } - abi::emit_call_label(emitter, helper); - release_temporary_numeric_operands(emitter, release_left_operand, release_right_operand); - PhpType::Mixed -} - -/// Returns true when a Mixed or Union typed expression result is heap-allocated and owned -/// (not borrowed, not static, not a call temporary). Used to decide whether the operand -/// needs a reference-count release after the runtime numeric helper completes. -fn mixed_numeric_operand_is_owned(expr: &Expr, ty: &PhpType) -> bool { - matches!(ty, PhpType::Mixed | PhpType::Union(_)) - && expr_result_heap_ownership(expr) == HeapOwnership::Owned -} - -/// Decrements refcounts for owned left/right operands that were pushed onto the temporary -/// stack before calling a mixed-numeric runtime helper. Skips decrement for non-owned operands. -/// The result from the helper is preserved on the stack during cleanup to avoid clobbering. -fn release_temporary_numeric_operands( - emitter: &mut Emitter, - release_left_operand: bool, - release_right_operand: bool, -) { - if !release_left_operand && !release_right_operand { - return; - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - let mut offset = 16; - if release_right_operand { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), offset); - abi::emit_decref_if_refcounted(emitter, &PhpType::Mixed); - offset += 16; - } - if release_left_operand { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), offset); - abi::emit_decref_if_refcounted(emitter, &PhpType::Mixed); - } - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - let operand_stack_bytes = - 16 * usize::from(release_left_operand) + 16 * usize::from(release_right_operand); - abi::emit_release_temporary_stack(emitter, operand_stack_bytes); -} - -/// Loads a previously saved numeric operand from a temporary stack slot at the given offset. -/// Handles Float (fp register), Str (ptr+len register pair), and integer types (single register). -/// Void/Never types (null) are a no-op since null contributes zero to numeric operations. -fn load_saved_numeric_operand(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_load_temporary_stack_slot( - emitter, - abi::float_result_reg(emitter), - offset, - ); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, offset); - abi::emit_load_temporary_stack_slot(emitter, len_reg, offset + 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), offset); - } - } -} - -/// Lowers the . concatenation operator using __rt_concat. -pub(super) fn emit_concat_binop( - left: &Expr, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let left_ty = emit_expr(left, emitter, ctx, data); - coerce_to_string_releasing_owned( - emitter, - ctx, - data, - &left_ty, - expr_result_heap_ownership(left) == HeapOwnership::Owned, - ); - let persisted_left = expr_result_heap_ownership(left) == HeapOwnership::NonHeap - || string_result_uses_transient_concat_buffer(left); - let release_left = persisted_left || string_result_is_owned_call_temp(left, ctx); - if persisted_left { - abi::emit_call_label(emitter, "__rt_str_persist"); - } - let (left_ptr_reg, left_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, left_ptr_reg, left_len_reg); - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_string_releasing_owned( - emitter, - ctx, - data, - &right_ty, - expr_result_heap_ownership(right) == HeapOwnership::Owned, - ); - let release_right = string_result_is_owned_call_temp(right, ctx); - let mut cleanup_operands = 0usize; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, x1"); // save right-operand string pointer into x3 - emitter.instruction("mov x4, x2"); // save right-operand string length into x4 - abi::emit_pop_reg_pair(emitter, "x1", "x2"); - if release_right { - abi::emit_push_reg_pair(emitter, "x3", "x4"); - cleanup_operands += 1; - } - if release_left { - abi::emit_push_reg_pair(emitter, "x1", "x2"); - cleanup_operands += 1; - } - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // save right-operand string pointer into rdi - emitter.instruction("mov rsi, rdx"); // save right-operand string length into rsi - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); - if release_right { - abi::emit_push_reg_pair(emitter, "rdi", "rsi"); - cleanup_operands += 1; - } - if release_left { - abi::emit_push_reg_pair(emitter, "rax", "rdx"); - cleanup_operands += 1; - } - } - } - abi::emit_call_label(emitter, "__rt_concat"); - if cleanup_operands > 0 { - emit_release_preserved_concat_operands(emitter, cleanup_operands); - } - PhpType::Str -} - -/// Cleans up persisted (non-heap) concat operands that were preserved on the temporary stack. -/// Called after `__rt_concat` when the result has already copied the string data; the result -/// is kept alive while each operand is freed via `__rt_heap_free_safe`. The result pointer/length -/// pair is also restored to the return registers after cleanup. -fn emit_release_preserved_concat_operands(emitter: &mut Emitter, count: usize) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - - // Keep the concat result live while freeing persisted operands that - // __rt_concat has already copied into the result buffer. - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); - for idx in 0..count { - abi::emit_load_temporary_stack_slot( - emitter, - abi::int_result_reg(emitter), - 16 + idx * 16, - ); - abi::emit_call_label(emitter, "__rt_heap_free_safe"); - } - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - abi::emit_release_temporary_stack(emitter, count * 16); -} - -/// Lowers &, |, ^, <<, >> bitwise operators. -pub(super) fn emit_bitwise_binop( - left: &Expr, - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let left_ty = emit_expr(left, emitter, ctx, data); - coerce_to_int(emitter, &left_ty); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_int(emitter, &right_ty); - let left_reg = match emitter.target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "r10", - }; - let result_reg = abi::int_result_reg(emitter); - abi::emit_pop_reg(emitter, left_reg); - match op { - BinOp::BitAnd => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("and x0, x1, x0"), // x0 = left (x1) & right (x0) - Arch::X86_64 => { - emitter.instruction(&format!("and {}, {}", left_reg, result_reg)); // left_reg &= result_reg (right operand) - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the AND result back into the result register - } - }, - BinOp::BitOr => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("orr x0, x1, x0"), // x0 = left (x1) | right (x0) - Arch::X86_64 => { - emitter.instruction(&format!("or {}, {}", left_reg, result_reg)); // left_reg |= result_reg (right operand) - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the OR result back into the result register - } - }, - BinOp::BitXor => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("eor x0, x1, x0"), // x0 = left (x1) ^ right (x0) - Arch::X86_64 => { - emitter.instruction(&format!("xor {}, {}", left_reg, result_reg)); // left_reg ^= result_reg (right operand) - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the XOR result back into the result register - } - }, - BinOp::ShiftLeft => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("lsl x0, x1, x0"), // x0 = left (x1) << right (x0) - Arch::X86_64 => { - emitter.instruction("mov rcx, rax"); // x86 shifts require count in cl -- move right operand into rcx - emitter.instruction(&format!("shl {}, cl", left_reg)); // left_reg <<= cl (logical shift left) - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the shifted value back into the result register - } - }, - BinOp::ShiftRight => match emitter.target.arch { - Arch::AArch64 => emitter.instruction("asr x0, x1, x0"), // x0 = left (x1) >> right (x0) (arithmetic shift right) - Arch::X86_64 => { - emitter.instruction("mov rcx, rax"); // x86 shifts require count in cl -- move right operand into rcx - emitter.instruction(&format!("sar {}, cl", left_reg)); // left_reg >>= cl (arithmetic shift right) - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the shifted value back into the result register - } - }, - _ => unreachable!(), - } - PhpType::Int -} - -/// Emits signed integer modulo (left % right) with a divisor-is-zero guard. -/// On ARM64 uses sdiv/msub; on x86_64 uses idiv. When the divisor is zero, PHP semantics -/// mandate returning zero rather than triggering a divide-by-zero trap. The left_reg holds -/// the left operand and result_reg (x0/rax) holds the right operand at entry. -fn emit_int_mod(emitter: &mut Emitter, ctx: &mut Context, left_reg: &str, result_reg: &str) { - let skip = ctx.next_label("mod_ok"); - let zero = ctx.next_label("mod_zero"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz x0, {zero}")); // branch to zero-divisor guard when right operand is zero - emitter.instruction("sdiv x2, x1, x0"); // x2 = left / right (quotient for modulo) - emitter.instruction("msub x0, x2, x0, x1"); // x0 = left - quotient*right (the remainder) - emitter.instruction(&format!("b {skip}")); // skip the divisor-zero case - emitter.label(&zero); - emitter.instruction("mov x0, #0"); // return 0 when the divisor was zero (PHP semantics) - emitter.label(&skip); - } - Arch::X86_64 => { - emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // test whether divisor is zero - emitter.instruction(&format!("je {}", zero)); // jump to divisor-zero case when flag set - emitter.instruction(&format!("mov r11, {}", result_reg)); // stash divisor in r11 before overwriting rax with the dividend - emitter.instruction(&format!("mov {}, {}", result_reg, left_reg)); // move the dividend (left operand) into rax for idiv - emitter.instruction("cqo"); // sign-extend rax into rdx:rax (required by idiv) - emitter.instruction("idiv r11"); // signed divide -- quotient in rax, remainder in rdx - emitter.instruction(&format!("mov {}, rdx", result_reg)); // return the remainder in the result register - emitter.instruction(&format!("jmp {}", skip)); // skip the divisor-zero case - emitter.label(&zero); - emitter.instruction(&format!("mov {}, 0", result_reg)); // return 0 when the divisor was zero (PHP semantics) - emitter.label(&skip); - } - } -} diff --git a/src/codegen/expr/binops/array_union.rs b/src/codegen/expr/binops/array_union.rs deleted file mode 100644 index 6f50429702..0000000000 --- a/src/codegen/expr/binops/array_union.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! Purpose: -//! Lowers PHP array union expressions and optimized empty-array cases. -//! Keeps operator-specific conversions and result register setup out of the dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::binops` -//! -//! Key details: -//! - Runtime calls and target instructions must preserve left/right evaluation order and scratch register assumptions. - -use super::super::super::context::Context; -use super::super::super::data_section::DataSection; -use super::super::super::emit::Emitter; -use super::super::super::{abi, platform::Arch}; -use super::super::{emit_expr, Expr, ExprKind, PhpType}; - -/// Returns true if both operands are array-like types that benefit from the -/// specialized array-union codepath (rather than the general binary operator dispatch). -/// -/// Matches `PhpType::Array` and `PhpType::AssocArray` in all four pairwise combinations. -/// The `ctx` parameter provides contextual type information for both operands. -pub(super) fn is_array_union_candidate(left: &Expr, right: &Expr, ctx: &Context) -> bool { - matches!( - ( - super::super::super::functions::infer_contextual_type(left, ctx), - super::super::super::functions::infer_contextual_type(right, ctx), - ), - (PhpType::Array(_), PhpType::Array(_)) - | (PhpType::AssocArray { .. }, PhpType::AssocArray { .. }) - | (PhpType::Array(_), PhpType::AssocArray { .. }) - | (PhpType::AssocArray { .. }, PhpType::Array(_)) - ) -} - -/// Lowers the `+` array union operator. -/// -/// Saves the left array pointer before evaluating the right operand, then restores -/// it as the first argument to the runtime helper. The runtime helper receives arguments -/// in platform ABI order (x0/x1 on ARM64, rdi/rsi on x86_64) and returns the union result -/// in the integer result register. -/// -/// # Arguments -/// * `left` - Left operand expression (evaluated first) -/// * `right` - Right operand expression (evaluated second) -/// * `emitter` - Code emitter -/// * `ctx` - Codegen context (carries variable layout, class metadata) -/// * `data` - Read-only data section for constants -/// -/// # Returns -/// The `PhpType` of the union result, derived from the static types of both operands. -pub(super) fn emit_array_union_binop( - left: &Expr, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let left_static_ty = emit_expr(left, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the left array pointer while evaluating the right operand - let right_static_ty = emit_expr(right, emitter, ctx, data); - let result_ty = array_union_result_type(left, &left_static_ty, right, &right_static_ty); - let runtime_helper = match (&left_static_ty, &right_static_ty) { - (PhpType::Array(_), PhpType::Array(_)) => "__rt_array_union", - (PhpType::AssocArray { .. }, PhpType::AssocArray { .. }) => "__rt_hash_union", - (PhpType::Array(_), PhpType::AssocArray { .. }) => "__rt_array_hash_union", - (PhpType::AssocArray { .. }, PhpType::Array(_)) => "__rt_hash_array_union", - _ => "__rt_array_union", - }; - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // pass the right array pointer as the second runtime argument - abi::emit_pop_reg(emitter, "x0"); // restore the left array pointer as the first runtime argument - } - Arch::X86_64 => { - emitter.instruction("mov rsi, rax"); // pass the right array pointer as the second runtime argument - abi::emit_pop_reg(emitter, "rdi"); // restore the left array pointer as the first runtime argument - } - } - - abi::emit_call_label(emitter, runtime_helper); // compute PHP array union with left-key precedence for the active storage pair - - result_ty -} - -/// Determines the `PhpType` of an array union result from the static types of both operands. -/// -/// This function applies PHP's type inference rules for the `+` operator: -/// - Empty array operand is discarded (left or right) -/// - Matching element types are preserved when both are homogeneous indexed arrays -/// - Mixed indexed/associative unions produce `AssocArray` with merged key/value types -/// - Dissimilar value types collapse to `Mixed` -/// -/// # Arguments -/// * `left_expr` - Left operand expression (used only to detect empty array literals) -/// * `left` - Static `PhpType` of the left operand -/// * `right_expr` - Right operand expression (used only to detect empty array literals) -/// * `right` - Static `PhpType` of the right operand -/// -/// # Returns -/// The inferred `PhpType` for the union expression. -fn array_union_result_type( - left_expr: &Expr, - left: &PhpType, - right_expr: &Expr, - right: &PhpType, -) -> PhpType { - match (left, right) { - (PhpType::Array(_), PhpType::Array(_)) if is_empty_indexed_array_literal(left_expr) => { - right.clone() - } - (PhpType::Array(_), PhpType::Array(_)) if is_empty_indexed_array_literal(right_expr) => { - left.clone() - } - (PhpType::Array(left_elem), PhpType::Array(right_elem)) if left_elem == right_elem => { - PhpType::Array(left_elem.clone()) - } - (PhpType::Array(left_elem), PhpType::Array(_)) => PhpType::Array(left_elem.clone()), - ( - PhpType::AssocArray { - key: left_key, - value: left_value, - }, - PhpType::AssocArray { - key: right_key, - value: right_value, - }, - ) => { - let key = if left_key == right_key { - left_key.clone() - } else { - Box::new(PhpType::Mixed) - }; - let value = if left_value == right_value { - left_value.clone() - } else { - Box::new(PhpType::Mixed) - }; - PhpType::AssocArray { key, value } - } - (PhpType::Array(left_elem), PhpType::AssocArray { key, value }) => PhpType::AssocArray { - key: Box::new(merge_array_union_key_with_indexed(key)), - value: Box::new(merge_array_union_value_types(left_elem, value)), - }, - (PhpType::AssocArray { key, value }, PhpType::Array(right_elem)) => PhpType::AssocArray { - key: Box::new(merge_array_union_key_with_indexed(key)), - value: Box::new(merge_array_union_value_types(value, right_elem)), - }, - _ => left.clone(), - } -} - -/// Returns true if the expression is an empty indexed array literal (`[]`). -/// -/// Used by `array_union_result_type` to apply the empty-array optimization where the -/// non-empty operand's type becomes the result type. -fn is_empty_indexed_array_literal(expr: &Expr) -> bool { - matches!(&expr.kind, ExprKind::ArrayLiteral(elems) if elems.is_empty()) -} - -/// Merges the key type of an associative array with an indexed array in a union. -/// -/// In PHP array union, indexed arrays use integer keys. When an `AssocArray` with a -/// known `Int` key type is unioned with an indexed array, the key type remains `Int`; -/// otherwise it becomes `Mixed` since the union result could have non-integer keys. -fn merge_array_union_key_with_indexed(key: &PhpType) -> PhpType { - if matches!(key, PhpType::Int) { - PhpType::Int - } else { - PhpType::Mixed - } -} - -/// Merges the value types of two array operands in a union. -/// -/// Returns the left type if both types match. If one side is `Never` (unreachable), -/// returns the other type. Otherwise collapses to `Mixed` since PHP arrays are -/// heterogeneous and a union can introduce values of different types. -fn merge_array_union_value_types(left: &PhpType, right: &PhpType) -> PhpType { - if left == right { - left.clone() - } else if matches!(left, PhpType::Never) { - right.clone() - } else if matches!(right, PhpType::Never) { - left.clone() - } else { - PhpType::Mixed - } -} diff --git a/src/codegen/expr/binops/comparison.rs b/src/codegen/expr/binops/comparison.rs deleted file mode 100644 index 80d926402f..0000000000 --- a/src/codegen/expr/binops/comparison.rs +++ /dev/null @@ -1,613 +0,0 @@ -//! Purpose: -//! Lowers loose equality, ordering, and spaceship operators. -//! Keeps operator-specific conversions and result register setup out of the dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::binops` -//! -//! Key details: -//! - Runtime calls and target instructions must preserve left/right evaluation order and scratch register assumptions. - -use super::super::super::context::Context; -use super::super::super::data_section::DataSection; -use super::super::super::emit::Emitter; -use super::super::super::{abi, platform::Arch}; -use super::target::{ - emit_float_compare, emit_pop_left_float_for_comparison, emit_promote_int_to_float, - emit_set_bool_from_flags, emit_set_float_bool_from_flags, -}; -use super::super::{ - coerce_null_to_zero, coerce_to_int, coerce_to_truthiness, emit_expr, BinOp, Expr, PhpType, -}; - -/// Converts `ty` to integer for loose comparison. -/// Handles null, bool, int, float, str, and Mixed/Union types. -/// Emits the integer result into `abi::int_result_reg(emitter)`. -/// Float values are truncated via `fcvtzs`. Strings call `__rt_atoi`. -fn coerce_to_int_for_loose_cmp(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Void => { - emitter.instruction("mov x0, #0"); // coerce null into integer 0 for loose comparison - } - PhpType::Bool => {} - PhpType::Int => { - super::super::coerce_null_to_zero(emitter, ty); - } - PhpType::Float => { - emitter.instruction("fcvtzs x0, d0"); // truncate the float in d0 to signed int for loose comparison - } - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_atoi"); - } - PhpType::Mixed | PhpType::Union(_) => { - abi::emit_call_label(emitter, "__rt_mixed_cast_int"); - } - _ => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - } -} - -/// Emits loose equality when the left operand is bool. -/// Coerces both sides to truthiness, then compares saved left truthiness against current right truthiness. -/// Uses a 16-byte temporary stack slot to preserve the left bool during right evaluation. -fn emit_bool_left_loose_equality( - _left: &Expr, - op: &BinOp, - right: &Expr, - left_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - coerce_to_truthiness(emitter, ctx, left_ty); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &right_ty); - compare_saved_truthiness_with_current(op, emitter); - PhpType::Bool -} - -/// Emits loose equality when the left operand is string. -/// Pushes the left string onto the temporary stack, emits the right expression, -/// then dispatches on the right type to handle bool, void, string, numeric, and other cases. -/// Returns PhpType::Bool. -fn emit_string_left_loose_equality( - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let (left_ptr, left_len) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, left_ptr, left_len); - let right_ty = emit_expr(right, emitter, ctx, data); - match right_ty { - PhpType::Bool => { - coerce_to_truthiness(emitter, ctx, &right_ty); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - load_saved_left_string(emitter, 16); - coerce_to_truthiness(emitter, ctx, &PhpType::Str); - compare_saved_right_truthiness_with_current_left(op, emitter); - abi::emit_release_temporary_stack(emitter, 16); - } - PhpType::Void => { - pop_saved_left_string(emitter); - emit_compare_current_string_length_to_zero(op, emitter); - } - PhpType::Str => { - call_str_loose_eq_with_saved_left(op, emitter); - } - PhpType::Int | PhpType::Float => { - push_current_number_as_float(emitter, &right_ty); - load_saved_left_string(emitter, 16); - abi::emit_call_label(emitter, "__rt_str_to_number"); - compare_parsed_string_with_saved_float(op, emitter, ctx); - abi::emit_release_temporary_stack(emitter, 16); - } - _ => { - pop_saved_left_string(emitter); - emit_set_loose_bool_literal(op, false, emitter); - } - } - PhpType::Bool -} - -/// Emits loose equality when the right operand is bool but left is not. -/// Left value is already on the temporary stack (numeric). Coerces right to truthiness, -/// pops left and coerces it to truthiness, then compares saved right truthiness against current left truthiness. -fn emit_bool_right_loose_equality( - op: &BinOp, - left_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - coerce_to_truthiness(emitter, ctx, &PhpType::Bool); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - pop_saved_left_for_truthiness(emitter, left_ty); - coerce_to_truthiness(emitter, ctx, left_ty); - compare_saved_right_truthiness_with_current_left(op, emitter); - PhpType::Bool -} - -/// Emits loose equality when the right operand is string. -/// The left value (numeric) is on the temporary stack. Dispatches based on left type: -/// - void: compares current string length to zero -/// - int/float: converts string to number and compares -/// - other: discards left and returns false -fn emit_right_string_loose_equality( - op: &BinOp, - left_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - if *left_ty == PhpType::Void { - discard_saved_left_numeric(emitter, left_ty); - emit_compare_current_string_length_to_zero(op, emitter); - } else if matches!(left_ty, PhpType::Int | PhpType::Float) { - abi::emit_call_label(emitter, "__rt_str_to_number"); - compare_parsed_string_with_saved_left_number(op, left_ty, emitter, ctx); - } else { - discard_saved_left_numeric(emitter, left_ty); - emit_set_loose_bool_literal(op, false, emitter); - } - PhpType::Bool -} - -/// Pops the saved left truthiness value into a scratch register and compares it -/// against the current right truthiness in `abi::int_result_reg(emitter)`. -/// Sets the boolean result from flags using `loose_equality_condition(op)`. -fn compare_saved_truthiness_with_current(op: &BinOp, emitter: &mut Emitter) { - let left_reg = match emitter.target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "r10", - }; - abi::emit_pop_reg(emitter, left_reg); - emitter.instruction(&format!("cmp {}, {}", left_reg, abi::int_result_reg(emitter))); // compare left truthiness against right truthiness - emit_set_bool_from_flags(emitter, loose_equality_condition(op)); -} - -/// Pops the saved right truthiness value into a scratch register and compares it -/// against the current left truthiness in `abi::int_result_reg(emitter)`. -/// Sets the boolean result from flags using `loose_equality_condition(op)`. -/// The comparison order is reversed relative to `compare_saved_truthiness_with_current`. -fn compare_saved_right_truthiness_with_current_left(op: &BinOp, emitter: &mut Emitter) { - let right_reg = match emitter.target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "r10", - }; - abi::emit_pop_reg(emitter, right_reg); - emitter.instruction(&format!("cmp {}, {}", abi::int_result_reg(emitter), right_reg)); // compare left truthiness against right truthiness - emit_set_bool_from_flags(emitter, loose_equality_condition(op)); -} - -/// Arranges arguments on ARM64 or x86_64 ABI registers and calls `__rt_str_loose_eq` -/// with the saved left string (popped from temp stack) and current right string. -/// Inverts the result for `!=` via `invert_loose_bool_if_needed`. -fn call_str_loose_eq_with_saved_left(op: &BinOp, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, x1"); // move the right string pointer into the loose string helper argument - emitter.instruction("mov x4, x2"); // move the right string length into the loose string helper argument - abi::emit_pop_reg_pair(emitter, "x1", "x2"); - } - Arch::X86_64 => { - emitter.instruction("mov r10, rax"); // preserve the right string pointer while arranging helper arguments - emitter.instruction("mov rcx, rdx"); // move the right string length into the fourth helper argument - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); - emitter.instruction("mov rdx, r10"); // move the right string pointer into the third helper argument - } - } - abi::emit_call_label(emitter, "__rt_str_loose_eq"); - invert_loose_bool_if_needed(op, emitter); -} - -/// Loads the saved left string from the temporary stack slot at `offset`. -/// Pointer lands in `abi::string_result_regs(emitter).0`, length in `.1`. -fn load_saved_left_string(emitter: &mut Emitter, offset: usize) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, offset); - abi::emit_load_temporary_stack_slot(emitter, len_reg, offset + 8); -} - -/// Pops the saved left string from the temporary stack into `abi::string_result_regs(emitter)`. -fn pop_saved_left_string(emitter: &mut Emitter) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); -} - -/// Compares the current string's length register against zero. -/// Sets the boolean result via `loose_equality_condition(op)`. -fn emit_compare_current_string_length_to_zero(op: &BinOp, emitter: &mut Emitter) { - let (_, len_reg) = abi::string_result_regs(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #0", len_reg)); // compare string length against the empty string for null loose equality - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, 0", len_reg)); // compare string length against the empty string for null loose equality - } - } - emit_set_bool_from_flags(emitter, loose_equality_condition(op)); -} - -/// Promotes the current integer result to float if needed, then pushes the float -/// onto the temporary stack for numeric string comparison. -fn push_current_number_as_float(emitter: &mut Emitter, ty: &PhpType) { - if *ty != PhpType::Float { - emit_promote_int_to_float( - emitter, - abi::float_result_reg(emitter), - abi::int_result_reg(emitter), - ); - } - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); -} - -/// Compares a numeric string (parsed into x0/d0 by `__rt_str_to_number`) against a float -/// that was saved on the temporary stack. On success (x0 != 0), pops the saved float and -/// compares it with the parsed value. On parsing failure, jumps to `false_label` and -/// sets the result to false. Uses `done_label` to skip the false branch on success. -fn compare_parsed_string_with_saved_float( - op: &BinOp, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let false_label = ctx.next_label("loose_numeric_string_false"); - let done_label = ctx.next_label("loose_numeric_string_done"); - let saved_float_reg = match emitter.target.arch { - Arch::AArch64 => "d1", - Arch::X86_64 => "xmm1", - }; - abi::emit_pop_float_reg(emitter, saved_float_reg); - emit_branch_if_current_flag_false(emitter, &false_label); - emit_compare_saved_float_with_parsed_string(emitter); - emit_set_float_bool_from_flags(emitter, loose_equality_condition(op)); - abi::emit_jump(emitter, &done_label); // skip the non-numeric-string false branch - emitter.label(&false_label); - emit_set_loose_bool_literal(op, false, emitter); - emitter.label(&done_label); -} - -/// Compares a numeric string (parsed into x0/d0) against a number that was saved on -/// the temporary stack. If left was int, it is first promoted to float. On success -/// (x0 != 0), pops the saved number and compares it with the parsed value. -/// On parsing failure, jumps to `false_label` and sets result to false. -fn compare_parsed_string_with_saved_left_number( - op: &BinOp, - left_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let false_label = ctx.next_label("loose_numeric_string_false"); - let done_label = ctx.next_label("loose_numeric_string_done"); - let saved_float_reg = match emitter.target.arch { - Arch::AArch64 => "d1", - Arch::X86_64 => "xmm1", - }; - if *left_ty == PhpType::Float { - abi::emit_pop_float_reg(emitter, saved_float_reg); - } else { - let left_int_reg = match emitter.target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "r10", - }; - abi::emit_pop_reg(emitter, left_int_reg); - emit_promote_int_to_float(emitter, saved_float_reg, left_int_reg); - } - emit_branch_if_current_flag_false(emitter, &false_label); - emit_compare_saved_float_with_parsed_string(emitter); - emit_set_float_bool_from_flags(emitter, loose_equality_condition(op)); - abi::emit_jump(emitter, &done_label); // skip the non-numeric-string false branch - emitter.label(&false_label); - emit_set_loose_bool_literal(op, false, emitter); - emitter.label(&done_label); -} - -/// Tests whether the string-to-number parsing result in x0/rax is zero (failure). -/// Branches to `label` when parsing failed (non-numeric string). -fn emit_branch_if_current_flag_false(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // test whether string-to-number parsing failed - emitter.instruction(&format!("b.eq {}", label)); // branch when the string was not numeric - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // test whether string-to-number parsing failed - emitter.instruction(&format!("je {}", label)); // branch when the string was not numeric - } - } -} - -/// Issues the target-specific float comparison instruction between the saved float -/// (d1/xmm1) and the parsed numeric string result (d0/xmm0). -fn emit_compare_saved_float_with_parsed_string(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fcmp d1, d0"); // compare numeric operand against parsed numeric string - } - Arch::X86_64 => { - emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric operand against parsed numeric string - } - } -} - -/// Pops the saved left operand into the appropriate register for truthiness coercion. -/// Float values go to `float_result_reg`, strings are popped as a pair, other types -/// go to `int_result_reg`. -fn pop_saved_left_for_truthiness(emitter: &mut Emitter, left_ty: &PhpType) { - match left_ty { - PhpType::Float => { - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); - } - PhpType::Str => { - pop_saved_left_string(emitter); - } - _ => { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - } - } -} - -/// Discards the saved left numeric operand from the temporary stack without using it. -/// Float values pop to `float_result_reg`, integers to `int_result_reg`. -fn discard_saved_left_numeric(emitter: &mut Emitter, left_ty: &PhpType) { - if *left_ty == PhpType::Float { - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); - } else { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - } -} - -/// Inverts the normalized equality result (0 or 1) in x0/rax when the operator is `!=`. -fn invert_loose_bool_if_needed(op: &BinOp, emitter: &mut Emitter) { - if *op == BinOp::NotEq { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("eor x0, x0, #1"); // invert normalized loose equality for != - } - Arch::X86_64 => { - emitter.instruction("xor rax, 1"); // invert normalized loose equality for != - } - } - } -} - -/// Emits a boolean literal result for loose equality operators. -/// For `==`: emits `equality_value` directly; for `!=`: emits `!equality_value`. -/// Result lands in `abi::int_result_reg(emitter)`. -fn emit_set_loose_bool_literal(op: &BinOp, equality_value: bool, emitter: &mut Emitter) { - let result = match op { - BinOp::Eq => equality_value, - BinOp::NotEq => !equality_value, - _ => unreachable!(), - }; - abi::emit_load_int_immediate( - emitter, - abi::int_result_reg(emitter), - if result { 1 } else { 0 }, - ); -} - -/// Returns the target condition name for loose equality operators. -/// `"eq"` for `==`, `"ne"` for `!=`. Panics for other operators. -fn loose_equality_condition(op: &BinOp) -> &'static str { - match op { - BinOp::Eq => "eq", - BinOp::NotEq => "ne", - _ => unreachable!(), - } -} - -/// Emits `==` and `!=` loose equality with full PHP type coercion rules. -/// Dispatches on the left type: bool-left and string-left have specialized paths. -/// For other types, emits left, pushes it, emits right, then compares as int or float. -/// Returns PhpType::Bool with result in `abi::int_result_reg(emitter)`. -pub(super) fn emit_loose_equality_binop( - left: &Expr, - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let left_ty = emit_expr(left, emitter, ctx, data); - if left_ty == PhpType::Bool { - return emit_bool_left_loose_equality(left, op, right, &left_ty, emitter, ctx, data); - } - if left_ty == PhpType::Str { - return emit_string_left_loose_equality(op, right, emitter, ctx, data); - } - let left_numeric = matches!( - left_ty, - PhpType::Int | PhpType::Float | PhpType::Bool | PhpType::Void - ); - coerce_null_to_zero(emitter, &left_ty); - let use_float = left_ty == PhpType::Float; - if use_float { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - } else { - if !left_numeric { - coerce_to_int_for_loose_cmp(emitter, &left_ty); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - let right_ty = emit_expr(right, emitter, ctx, data); - let right_numeric = matches!( - right_ty, - PhpType::Int | PhpType::Float | PhpType::Bool | PhpType::Void - ); - coerce_null_to_zero(emitter, &right_ty); - - if right_ty == PhpType::Bool && matches!(left_ty, PhpType::Int | PhpType::Float | PhpType::Void) { - return emit_bool_right_loose_equality(op, &left_ty, emitter, ctx); - } - if right_ty == PhpType::Str { - return emit_right_string_loose_equality(op, &left_ty, emitter, ctx); - } - - if left_numeric && right_numeric && (left_ty == PhpType::Float || right_ty == PhpType::Float) { - if right_ty != PhpType::Float { - emit_promote_int_to_float( - emitter, - abi::float_result_reg(emitter), - abi::int_result_reg(emitter), - ); - } - emit_pop_left_float_for_comparison(emitter, &left_ty); - emit_float_compare(emitter); - } else { - if !right_numeric { - coerce_to_int_for_loose_cmp(emitter, &right_ty); - } - let left_reg = match emitter.target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "r10", - }; - abi::emit_pop_reg(emitter, left_reg); - emitter.instruction(&format!("cmp {}, {}", left_reg, abi::int_result_reg(emitter))); // compare left against right in integer registers - } - let cond = match op { - BinOp::Eq => "eq", - BinOp::NotEq => "ne", - _ => unreachable!(), - }; - if left_numeric && right_numeric && (left_ty == PhpType::Float || right_ty == PhpType::Float) { - emit_set_float_bool_from_flags(emitter, cond); - } else { - emit_set_bool_from_flags(emitter, cond); - } - PhpType::Bool -} - -/// Lowers <, >, <=, >= ordering comparisons with float/int dispatch. -pub(super) fn emit_order_compare_binop( - left: &Expr, - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let left_ty = emit_expr(left, emitter, ctx, data); - coerce_to_int(emitter, &left_ty); - let use_float = left_ty == PhpType::Float; - if use_float { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - } else { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_int(emitter, &right_ty); - - if left_ty == PhpType::Float || right_ty == PhpType::Float { - if right_ty != PhpType::Float { - emit_promote_int_to_float( - emitter, - abi::float_result_reg(emitter), - abi::int_result_reg(emitter), - ); - } - emit_pop_left_float_for_comparison(emitter, &left_ty); - emit_float_compare(emitter); - } else { - let left_reg = match emitter.target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "r10", - }; - abi::emit_pop_reg(emitter, left_reg); - emitter.instruction(&format!("cmp {}, {}", left_reg, abi::int_result_reg(emitter))); // compare left against right in integer registers - } - let cond = match op { - BinOp::Lt => "lt", - BinOp::Gt => "gt", - BinOp::LtEq => "le", - BinOp::GtEq => "ge", - _ => unreachable!(), - }; - if left_ty == PhpType::Float || right_ty == PhpType::Float { - emit_set_float_bool_from_flags(emitter, cond); - } else { - emit_set_bool_from_flags(emitter, cond); - } - PhpType::Bool -} - -/// Lowers the <=> spaceship operator returning -1, 0, or 1. -pub(super) fn emit_spaceship_binop( - left: &Expr, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let left_ty = emit_expr(left, emitter, ctx, data); - coerce_to_int(emitter, &left_ty); - let use_float = left_ty == PhpType::Float; - if use_float { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - } else { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - let right_ty = emit_expr(right, emitter, ctx, data); - coerce_to_int(emitter, &right_ty); - - if left_ty == PhpType::Float || right_ty == PhpType::Float { - if right_ty != PhpType::Float { - emit_promote_int_to_float( - emitter, - abi::float_result_reg(emitter), - abi::int_result_reg(emitter), - ); - } - emit_pop_left_float_for_comparison(emitter, &left_ty); - emit_float_compare(emitter); - } else { - let left_reg = match emitter.target.arch { - Arch::AArch64 => "x1", - Arch::X86_64 => "r10", - }; - abi::emit_pop_reg(emitter, left_reg); - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("cmp x1, x0"), // compare left (x1) against right (x0) before computing the spaceship result - Arch::X86_64 => emitter.instruction(&format!( // compare left against right in integer registers - "cmp {}, {}", - left_reg, - abi::int_result_reg(emitter) - )), - } - } - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cset x0, gt"); // set x0 to 1 when left > right, else 0 - emitter.instruction("csinv x0, x0, xzr, ge"); // keep 1 when left >= right, invert to -1 when left < right - if left_ty == PhpType::Float || right_ty == PhpType::Float { - emitter.instruction("mov w1, #1"); // candidate spaceship result for an unordered (NaN) comparison - emitter.instruction("csel x0, x1, x0, vs"); // PHP: NaN <=> x is 1 — pick 1 when fcmp was unordered - } - } - Arch::X86_64 => { - let greater_label = ctx.next_label("spaceship_gt"); - let less_label = ctx.next_label("spaceship_lt"); - let done_label = ctx.next_label("spaceship_done"); - if left_ty == PhpType::Float || right_ty == PhpType::Float { - emitter.instruction(&format!("jp {}", greater_label)); // PHP: NaN <=> x is 1 — route unordered (parity) to the greater (1) branch - emitter.instruction(&format!("ja {}", greater_label)); // floats: jump to greater branch when unordered-above - emitter.instruction(&format!("jb {}", less_label)); // floats: jump to less branch when unordered-below - } else { - emitter.instruction(&format!("jg {}", greater_label)); // ints: jump to greater branch when signed greater - emitter.instruction(&format!("jl {}", less_label)); // ints: jump to less branch when signed less - } - emitter.instruction("mov rax, 0"); // equal case: spaceship result is 0 - emitter.instruction(&format!("jmp {}", done_label)); // skip the greater/less branches - emitter.label(&greater_label); - emitter.instruction("mov rax, 1"); // greater branch: spaceship result is 1 - emitter.instruction(&format!("jmp {}", done_label)); // skip the less branch - emitter.label(&less_label); - emitter.instruction("mov rax, -1"); // less branch: spaceship result is -1 - emitter.label(&done_label); - } - } - PhpType::Int -} diff --git a/src/codegen/expr/binops/mod.rs b/src/codegen/expr/binops/mod.rs deleted file mode 100644 index 49d6c2fbd7..0000000000 --- a/src/codegen/expr/binops/mod.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Dispatches binary operator lowering to arithmetic, comparison, array-union, and target helper modules. -//! Selects PHP-compatible result types and special operator paths before instruction emission. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Operator precedence is resolved by the parser; this layer preserves PHP value semantics and result registers. - -use super::super::context::Context; -use super::super::data_section::DataSection; -use super::super::emit::Emitter; -use super::compare::{emit_null_coalesce, emit_strict_compare}; -use super::{BinOp, Expr, PhpType}; - -mod arithmetic; -mod array_union; -mod comparison; -mod target; - -use arithmetic::{ - emit_concat_binop, emit_logical_binop, emit_numeric_binop, emit_pow_binop, -}; -use array_union::{emit_array_union_binop, is_array_union_candidate}; -use comparison::{emit_loose_equality_binop, emit_order_compare_binop, emit_spaceship_binop}; - -/// Dispatches a binary operator to the appropriate specialized emitter. -/// Handles arithmetic, logical, comparison, concat, bitwise, and array-union operators -/// with PHP-compatible value semantics. -pub(super) fn emit_binop( - left: &Expr, - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - match op { - BinOp::And | BinOp::Or | BinOp::Xor => { - emit_logical_binop(left, op, right, emitter, ctx, data) - } - BinOp::Pow => emit_pow_binop(left, right, emitter, ctx, data), - BinOp::Add if is_array_union_candidate(left, right, ctx) => { - emit_array_union_binop(left, right, emitter, ctx, data) - } - BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod => { - emit_numeric_binop(left, op, right, emitter, ctx, data) - } - BinOp::Eq | BinOp::NotEq => emit_loose_equality_binop(left, op, right, emitter, ctx, data), - BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq => { - emit_order_compare_binop(left, op, right, emitter, ctx, data) - } - BinOp::StrictEq | BinOp::StrictNotEq => { - emit_strict_compare(left, op, right, emitter, ctx, data) - } - BinOp::Concat => emit_concat_binop(left, right, emitter, ctx, data), - BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::ShiftLeft | BinOp::ShiftRight => { - arithmetic::emit_bitwise_binop(left, op, right, emitter, ctx, data) - } - BinOp::Spaceship => emit_spaceship_binop(left, right, emitter, ctx, data), - BinOp::NullCoalesce => emit_null_coalesce(left, right, emitter, ctx, data), - } -} diff --git a/src/codegen/expr/binops/target.rs b/src/codegen/expr/binops/target.rs deleted file mode 100644 index 0e1086b088..0000000000 --- a/src/codegen/expr/binops/target.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! Purpose: -//! Lowers target-specific instruction snippets shared by binary operators. -//! Keeps operator-specific conversions and result register setup out of the dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::binops` -//! -//! Key details: -//! - Runtime calls and target instructions must preserve left/right evaluation order and scratch register assumptions. - -use crate::codegen::{abi, emit::Emitter, platform::Arch}; -use crate::parser::ast::BinOp; -use crate::types::PhpType; - -/// Sets integer result (x0/rax) to 1 or 0 based on a condition code from flags. -pub(super) fn emit_set_bool_from_flags(emitter: &mut Emitter, cond: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cset x0, {}", cond)); // set integer result to 1 if the comparison condition matched - } - Arch::X86_64 => { - let setcc = match cond { - "eq" => "sete", - "ne" => "setne", - "lt" => "setl", - "gt" => "setg", - "le" => "setle", - "ge" => "setge", - _ => unreachable!("unsupported comparison condition {cond}"), - }; - emitter.instruction(&format!("{} al", setcc)); // set the low result byte when the comparison condition matched - emitter.instruction("movzx rax, al"); // zero-extend the boolean byte into the integer result register - } - } -} - -/// Sets integer result (x0/rax) from float comparison flags. -/// -/// Applies PHP's NaN (unordered) rule: every ordering/equality comparison against NaN is false -/// except `!=`, which is true. After the conditional set, the unordered case (ARM64 `V`, x86_64 -/// parity flag) is forced to 0 for `==`/`<`/`>`/`<=`/`>=` and to 1 for `!=`. -pub(super) fn emit_set_float_bool_from_flags(emitter: &mut Emitter, cond: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cset x0, {}", cond)); // set integer result to 1 if the float comparison condition matched - if cond != "ne" { - emitter.instruction("csel x0, xzr, x0, vs"); // PHP: an unordered (NaN) ==, <, >, <=, >= is false — force 0 when fcmp was unordered - } - } - Arch::X86_64 => { - let setcc = match cond { - "eq" => "sete", - "ne" => "setne", - "lt" => "setb", - "gt" => "seta", - "le" => "setbe", - "ge" => "setae", - _ => unreachable!("unsupported float comparison condition {cond}"), - }; - emitter.instruction(&format!("{} al", setcc)); // set the low result byte when the float comparison condition matched - if cond == "ne" { - emitter.instruction("setp cl"); // cl = 1 when ucomisd was unordered (a NaN operand) - emitter.instruction("or al, cl"); // PHP: NaN != x is true — OR the unordered case into the not-equal result - } else { - emitter.instruction("setnp cl"); // cl = 1 only when the comparison was ordered (no NaN operand) - emitter.instruction("and al, cl"); // PHP: an unordered (NaN) comparison is false — mask out the unordered case - } - emitter.instruction("movzx rax, al"); // zero-extend the boolean byte into the integer result register - } - } -} - -/// Promotes an integer operand to a float register (scvtf/cvtsi2sd). -pub(super) fn emit_promote_int_to_float(emitter: &mut Emitter, float_reg: &str, int_reg: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("scvtf {}, {}", float_reg, int_reg)); // promote the integer operand into a floating-point register - } - Arch::X86_64 => { - emitter.instruction(&format!("cvtsi2sd {}, {}", float_reg, int_reg)); // promote the integer operand into a floating-point register - } - } -} - -/// Pops the left float (or promoted int) into the comparison scratch register. -pub(super) fn emit_pop_left_float_for_comparison(emitter: &mut Emitter, left_ty: &PhpType) { - let left_float_reg = match emitter.target.arch { - Arch::AArch64 => "d1", - Arch::X86_64 => "xmm1", - }; - if *left_ty == PhpType::Float { - abi::emit_pop_float_reg(emitter, left_float_reg); // pop left float operand into the comparison scratch register - } else { - let left_int_reg = abi::symbol_scratch_reg(emitter); - abi::emit_pop_reg(emitter, left_int_reg); // pop left integer operand before float promotion - emit_promote_int_to_float(emitter, left_float_reg, left_int_reg); - } -} - -/// Emits a double-precision comparison (fcmp/ucomisd) setting NZCV/flags. -pub(super) fn emit_float_compare(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fcmp d1, d0"); // compare two doubles, setting NZCV flags - } - Arch::X86_64 => { - emitter.instruction("ucomisd xmm1, xmm0"); // compare two doubles, setting x86_64 condition flags - } - } -} - -/// Emits a float binop (+, -, *, /, %) using target instructions. -pub(super) fn emit_float_binop(emitter: &mut Emitter, op: &BinOp) { - match emitter.target.arch { - Arch::AArch64 => { - match op { - BinOp::Add => { - emitter.instruction("fadd d0, d1, d0"); // float addition: left + right - } - BinOp::Sub => { - emitter.instruction("fsub d0, d1, d0"); // float subtraction: left - right - } - BinOp::Mul => { - emitter.instruction("fmul d0, d1, d0"); // float multiplication: left * right - } - BinOp::Div => { - emitter.instruction("fdiv d0, d1, d0"); // float division: left / right - } - BinOp::Mod => { - // -- float modulo: a - trunc(a/b) * b (C/PHP truncated mod) -- - emitter.instruction("fdiv d2, d1, d0"); // d2 = left / right - emitter.instruction("frintz d2, d2"); // d2 = trunc(left / right) toward zero - emitter.instruction("fmsub d0, d2, d0, d1"); // d0 = left - trunc(l/r)*right - } - _ => unreachable!(), - } - } - Arch::X86_64 => { - match op { - BinOp::Add => { - emitter.instruction("addsd xmm1, xmm0"); // float addition: left + right - emitter.instruction("movsd xmm0, xmm1"); // move the sum back to the floating-point result register - } - BinOp::Sub => { - emitter.instruction("subsd xmm1, xmm0"); // float subtraction: left - right - emitter.instruction("movsd xmm0, xmm1"); // move the difference back to the floating-point result register - } - BinOp::Mul => { - emitter.instruction("mulsd xmm1, xmm0"); // float multiplication: left * right - emitter.instruction("movsd xmm0, xmm1"); // move the product back to the floating-point result register - } - BinOp::Div => { - emitter.instruction("divsd xmm1, xmm0"); // float division: left / right - emitter.instruction("movsd xmm0, xmm1"); // move the quotient back to the floating-point result register - } - BinOp::Mod => { - // -- float modulo: a - trunc(a/b) * b (C/PHP truncated mod) -- - emitter.instruction("movsd xmm2, xmm1"); // copy the left operand before quotient calculation - emitter.instruction("divsd xmm2, xmm0"); // xmm2 = left / right - emitter.instruction("roundsd xmm2, xmm2, 3"); // xmm2 = trunc(left / right) toward zero - emitter.instruction("mulsd xmm2, xmm0"); // xmm2 = trunc(left / right) * right - emitter.instruction("subsd xmm1, xmm2"); // xmm1 = left - trunc(left/right)*right - emitter.instruction("movsd xmm0, xmm1"); // move the modulo result back to the floating-point result register - } - _ => unreachable!(), - } - } - } -} diff --git a/src/codegen/expr/calls.rs b/src/codegen/expr/calls.rs deleted file mode 100644 index dd90849ebf..0000000000 --- a/src/codegen/expr/calls.rs +++ /dev/null @@ -1,487 +0,0 @@ -//! Purpose: -//! Dispatches function-like expression calls including direct, indirect, closure, method-adjacent, and first-class forms. -//! Coordinates call signatures, argument lowering, and result typing for expression consumers. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Argument evaluation must preserve PHP source order before ABI materialization happens in call-argument helpers. - -pub(crate) mod args; -mod callable_array_runtime; -mod closure; -mod descriptor_invoker_args; -mod descriptor_value; -mod first_class; -mod function; -mod indirect; -mod pipe; - -use super::super::context::Context; -use super::super::data_section::DataSection; -use super::super::emit::Emitter; -use super::Expr; -use crate::names::{php_symbol_key, Name}; -use crate::parser::ast::{CallableTarget, ExprKind, StaticReceiver, TypeExpr}; -use crate::span::Span; -use crate::types::PhpType; - -/// Emits a direct or namespaced function call by name. -pub(super) fn emit_function_call( - name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - function::emit_function_call(name, args, emitter, ctx, data) -} - -/// Emits a closure (anonymous function) definition with captures. -pub(super) fn emit_closure( - params: &[(String, Option, Option, bool)], - variadic: &Option, - return_type: &Option, - body: &[crate::parser::ast::Stmt], - captures: &[String], - capture_refs: &[String], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - closure::emit_closure( - params, - variadic, - return_type, - body, - captures, - capture_refs, - emitter, - ctx, - data, - ) -} - -/// Emits a closure call expression (e.g., `$closure(...)`). -pub(super) fn emit_closure_call( - var: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - closure::emit_closure_call(var, args, emitter, ctx, data) -} - -/// Emits an indirect call where the callee is a runtime-loaded expression. -pub(super) fn emit_loaded_expr_call( - callee: &Expr, - args: &[Expr], - loaded_callee_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - indirect::emit_loaded_expr_call(callee, args, loaded_callee_ty, emitter, ctx, data) -} - -/// Emits a call where the already-loaded callee result is a runtime string callback name. -pub(super) fn emit_loaded_runtime_string_call( - args: &[Expr], - span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("call runtime string callable"); - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::save_concat_offset_before_nested_call(emitter, ctx); - } - - let (ptr_reg, len_reg) = crate::codegen::abi::string_result_regs(emitter); - crate::codegen::abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime string callback name while building descriptor arguments - let arr_ty = descriptor_invoker_args::emit_descriptor_invoker_arg_array( - args, - None, - span, - emitter, - ctx, - data, - ); - let call_reg = crate::codegen::abi::nested_call_reg(emitter); - let ret_ty = - crate::codegen::builtins::arrays::call_user_func_array::emit_loaded_array_string_callback_call( - crate::codegen::builtins::arrays::call_user_func_array::LoadedArraySource::Result, - &arr_ty, - 0, - 8, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - crate::codegen::abi::emit_release_temporary_stack(emitter, 16); // discard the preserved runtime string callback name - ret_ty -} - -/// Emits `([$object, "method"])(...)` or `([ClassName::class, "method"])(...)`. -pub(super) fn emit_callable_array_literal_call( - callee: &Expr, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - if let Some((receiver, method)) = callable_array_parts(callee) { - if let Some(receiver) = static_callable_receiver(receiver, ctx) { - return emit_static_callable_array_descriptor_call( - &receiver, - method, - args, - emitter, - ctx, - data, - ); - } - if let Some(ret_ty) = - emit_instance_callable_array_descriptor_call(receiver, method, args, emitter, ctx, data) - { - return Some(ret_ty); - } - } - callable_array_runtime::emit_literal_call(callee, args, emitter, ctx, data) -} - -/// Emits a runtime-selected callable-array invocation for builtin callback paths. -pub(crate) fn emit_runtime_callable_array_call( - callee: &Expr, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - if let ExprKind::Variable(var) = &callee.kind { - if let Some(ret_ty) = - callable_array_runtime::emit_variable_call(var, args, emitter, ctx, data) - { - return Some(ret_ty); - } - } - callable_array_runtime::emit_literal_call(callee, args, emitter, ctx, data) -} - -/// Emits a direct `$callback(...)` call when `$callback` stores a PHP callable array. -pub(super) fn emit_callable_array_variable_call( - var: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let Some(target) = ctx.callable_array_targets.get(var).cloned() else { - return callable_array_runtime::emit_variable_call(var, args, emitter, ctx, data); - }; - match target { - CallableTarget::Method { object, method } => emit_instance_callable_array_variable_call( - var, &object, &method, args, emitter, ctx, data, - ), - CallableTarget::StaticMethod { receiver, method } => { - emit_static_callable_array_variable_call(&receiver, &method, args, emitter, ctx, data) - } - CallableTarget::Function(_) => None, - } -} - -/// Emits a descriptor invocation for a local object variable with public `__invoke`. -pub(super) fn emit_invokable_object_variable_call( - var: &str, - class_name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let case = crate::codegen::callable_dispatch::runtime_instance_method_case( - ctx, - data, - class_name, - "__invoke", - crate::codegen::callable_dispatch::RuntimeInstanceCallableShape::ObjectInvoke, - )?; - if !case.has_invoker { - return None; - } - let mut descriptor_args = Vec::with_capacity(args.len() + 1); - descriptor_args.push(Expr::new( - ExprKind::Variable(var.to_string()), - Span::dummy(), - )); - descriptor_args.extend(args.iter().cloned()); - emit_callable_array_descriptor_case_call( - &case.descriptor_label, - &case.sig, - &descriptor_args, - emitter, - ctx, - data, - ) -} - -/// Emits a descriptor invocation for a stored instance-method callable array. -fn emit_instance_callable_array_variable_call( - var: &str, - object: &Expr, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let receiver_ty = crate::codegen::functions::infer_contextual_type(object, ctx); - let class_name = crate::codegen::functions::singular_object_class(&receiver_ty)?; - let case = crate::codegen::callable_dispatch::runtime_instance_method_case( - ctx, - data, - class_name, - method, - crate::codegen::callable_dispatch::RuntimeInstanceCallableShape::InstanceMethod, - )?; - if !case.has_invoker { - return None; - } - let receiver = callable_array_receiver_slot_expr(var); - let mut descriptor_args = Vec::with_capacity(args.len() + 1); - descriptor_args.push(receiver); - descriptor_args.extend(args.iter().cloned()); - emit_callable_array_descriptor_case_call( - &case.descriptor_label, - &case.sig, - &descriptor_args, - emitter, - ctx, - data, - ) -} - -/// Emits a descriptor invocation for a literal instance-method callable array. -fn emit_instance_callable_array_descriptor_call( - receiver: &Expr, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let receiver_ty = crate::codegen::functions::infer_contextual_type(receiver, ctx); - let class_name = crate::codegen::functions::singular_object_class(&receiver_ty)?; - let case = crate::codegen::callable_dispatch::runtime_instance_method_case( - ctx, - data, - class_name, - method, - crate::codegen::callable_dispatch::RuntimeInstanceCallableShape::InstanceMethod, - )?; - if !case.has_invoker { - return None; - } - let mut descriptor_args = Vec::with_capacity(args.len() + 1); - descriptor_args.push(receiver.clone()); - descriptor_args.extend(args.iter().cloned()); - emit_callable_array_descriptor_case_call( - &case.descriptor_label, - &case.sig, - &descriptor_args, - emitter, - ctx, - data, - ) -} - -/// Emits a descriptor invocation for a stored static-method callable array. -fn emit_static_callable_array_variable_call( - receiver: &StaticReceiver, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emit_static_callable_array_descriptor_call(receiver, method, args, emitter, ctx, data) -} - -/// Emits a descriptor invocation for a static-method callable array. -fn emit_static_callable_array_descriptor_call( - receiver: &StaticReceiver, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let StaticReceiver::Named(class_name) = receiver else { - return None; - }; - let case = crate::codegen::callable_dispatch::runtime_static_method_case( - ctx, - data, - class_name.as_str(), - method, - )?; - if !case.has_invoker { - return None; - } - emit_callable_array_descriptor_case_call( - &case.descriptor_label, - &case.sig, - args, - emitter, - ctx, - data, - ) -} - -/// Calls a callable-array descriptor case with direct callable-array arguments. -fn emit_callable_array_descriptor_case_call( - descriptor_label: &str, - sig: &crate::types::FunctionSig, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - emitter.comment("call callable-array descriptor"); - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::save_concat_offset_before_nested_call(emitter, ctx); - } - let arr_ty = descriptor_invoker_args::emit_descriptor_invoker_arg_array( - args, - Some(sig), - Span::dummy(), - emitter, - ctx, - data, - ); - let call_reg = crate::codegen::abi::nested_call_reg(emitter); - crate::codegen::abi::emit_symbol_address(emitter, call_reg, descriptor_label); - crate::codegen::builtins::arrays::call_user_func_array::emit_call_descriptor_array_invoker( - crate::codegen::builtins::arrays::call_user_func_array::LoadedArraySource::Result, - &arr_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - Some(PhpType::Mixed) -} - -/// Builds `$callback[0]`, the receiver slot stored inside a callable-array value. -fn callable_array_receiver_slot_expr(var: &str) -> Expr { - callable_array_slot_expr(var, 0) -} - -/// Builds `$callback[$index]`, a positional slot stored inside a callable-array value. -fn callable_array_slot_expr(var: &str, index: i64) -> Expr { - Expr::new( - ExprKind::ArrayAccess { - array: Box::new(Expr::new(ExprKind::Variable(var.to_string()), Span::dummy())), - index: Box::new(Expr::new(ExprKind::IntLiteral(index), Span::dummy())), - }, - Span::dummy(), - ) -} - -/// Returns receiver and method from a two-element PHP callable array literal. -fn callable_array_parts(callee: &Expr) -> Option<(&Expr, &str)> { - let elems = match &callee.kind { - ExprKind::ArrayLiteral(elems) => elems, - _ => return None, - }; - if elems.len() != 2 { - return None; - } - let ExprKind::StringLiteral(method) = &elems[1].kind else { - return None; - }; - Some((&elems[0], method.as_str())) -} - -/// Resolves a callable-array receiver expression to a static class receiver. -fn static_callable_receiver(receiver: &Expr, ctx: &Context) -> Option { - let class_name = match &receiver.kind { - ExprKind::StringLiteral(class_name) => { - resolve_class_name(ctx, class_name).map(str::to_string) - } - ExprKind::ClassConstant { receiver } => resolve_static_receiver_class(receiver, ctx), - _ => None, - }?; - Some(StaticReceiver::Named(Name::from(class_name))) -} - -/// Resolves `self`, `parent`, `static`, and named static receivers to concrete class names. -fn resolve_static_receiver_class(receiver: &StaticReceiver, ctx: &Context) -> Option { - match receiver { - StaticReceiver::Named(name) => resolve_class_name(ctx, name.as_str()).map(str::to_string), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.parent.clone()), - } -} - -/// Resolves a class name case-insensitively against the known codegen class table. -fn resolve_class_name<'a>(ctx: &'a Context, class_name: &str) -> Option<&'a str> { - let class_key = php_symbol_key(class_name.trim_start_matches('\\')); - ctx.classes - .keys() - .find(|existing| php_symbol_key(existing) == class_key) - .map(String::as_str) -} - -/// Emits a first-class callable expression (e.g., `$fn(...)()`). -pub(super) fn emit_first_class_callable( - target: &crate::parser::ast::CallableTarget, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - first_class::emit_first_class_callable(target, emitter, ctx, data) -} - -/// Returns the function signature for a first-class callable target. -pub(crate) fn first_class_callable_sig( - target: &crate::parser::ast::CallableTarget, - ctx: &Context, -) -> Option { - first_class::first_class_callable_sig(target, ctx) -} - -/// Generates a unique temp name for the receiver of an inline first-class callable. -pub(crate) fn first_class_method_receiver_temp_name(span: Span) -> String { - first_class::method_receiver_temp_name(span) -} - -/// Generates a unique temp name for the pipe value in an arrow-function pipeline. -pub(crate) fn pipe_value_temp_name(span: Span) -> String { - format!("__elephc_pipe_value_{}_{}", span.line, span.col) -} - -/// Emits a pipe expression (first-class callable pipeline). -pub(super) fn emit_pipe( - value: &Expr, - callable: &Expr, - span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - pipe::emit_pipe(value, callable, span, emitter, ctx, data) -} diff --git a/src/codegen/expr/calls/args/array_elements.rs b/src/codegen/expr/calls/args/array_elements.rs deleted file mode 100644 index db0c8b19e7..0000000000 --- a/src/codegen/expr/calls/args/array_elements.rs +++ /dev/null @@ -1,489 +0,0 @@ -//! Purpose: -//! Lowers argument values sourced from spread array elements. -//! Converts evaluated PHP argument expressions into temporary values ready for ABI assignment. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args` -//! -//! Key details: -//! - Argument checks must happen at PHP-observable points without skipping later side effects. - -use crate::codegen::builtins::arrays::call_user_func_array::INVOKER_ARG_REF_CELL_TAG; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, context::Context, data_section::DataSection, platform::Arch}; -use crate::types::PhpType; - -use super::common::{ - coerce_current_value_to_target, push_arg_value, push_current_result_ref_arg_address, - release_preserved_mixed_after_arg_coercion, -}; - -/// Loads a spread/callback array element into the appropriate result register based on `source_elem_ty`. -/// For `Float`, loads into `float_result_reg`; for `Str`, loads pointer and length into `string_result_regs`; -/// for `Void`, emits nothing; otherwise loads scalar or pointer into `int_result_reg`. -/// `data_base_reg` points to the spread/callback array payload; `byte_offset` is the element's offset within that payload. -pub(crate) fn load_array_element_to_result( - emitter: &mut Emitter, - source_elem_ty: &PhpType, - data_base_reg: &str, - byte_offset: usize, -) { - match source_elem_ty.codegen_repr() { - PhpType::Float => { - abi::emit_load_from_address(emitter, abi::float_result_reg(emitter), data_base_reg, byte_offset); // load float element from the spread/callback array payload - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_from_address(emitter, ptr_reg, data_base_reg, byte_offset); // load string pointer from the spread/callback array payload - abi::emit_load_from_address(emitter, len_reg, data_base_reg, byte_offset + 8); // load string length from the spread/callback array payload - } - PhpType::Void => {} - _ => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), data_base_reg, byte_offset); // load scalar or boxed pointer element from the spread/callback array payload - } - } -} - -/// Returns the byte stride of a spread array element based on its PHP type. -/// `Str` elements occupy 16 bytes (8-byte pointer + 8-byte length), `Void` occupies 0 bytes, -/// and all other types occupy 8 bytes (a single machine word or pointer). -pub(crate) fn array_element_stride(source_elem_ty: &PhpType) -> usize { - match source_elem_ty.codegen_repr() { - PhpType::Str => 16, - PhpType::Void => 0, - _ => 8, - } -} - -/// Coerces a spread array element to the target type and pushes it as a call argument. -/// First applies `coerce_current_value_to_target` using `source_elem_ty` and `target_ty`. -/// Increments the refcount if the pushed value is refcounted but not boxed to `Mixed`. -/// Returns the post-coercion `PhpType` that was pushed. -pub(crate) fn push_loaded_array_element_arg( - source_elem_ty: &PhpType, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let (pushed_ty, boxed_to_mixed) = - coerce_current_value_to_target(emitter, ctx, data, source_elem_ty, target_ty); - if !boxed_to_mixed { - abi::emit_incref_if_refcounted(emitter, &pushed_ty); - } - push_arg_value(emitter, &pushed_ty); - pushed_ty -} - -/// Emits a hash lookup for a named or numeric key in a spread/callback array argument. -/// Sets up `x0`/`rdi` with the hash base register and `x1`/`edi` with the key pointer/index, -/// `x2`/`esi` with the key length, then calls `__rt_hash_get`. -/// If `param_name` is provided, performs a named-key lookup first and branches to `found_label` -/// when the key is present before falling through to the numeric-key lookup. -pub(crate) fn emit_hash_lookup_for_param_or_index( - hash_base_reg: &str, - param_name: Option<&str>, - numeric_idx: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let key_ptr_reg = abi::int_arg_reg_name(emitter.target, 1); - let key_len_reg = abi::int_arg_reg_name(emitter.target, 2); - let found_label = param_name.map(|_| ctx.next_label("assoc_spread_key_found")); - - if let Some(name) = param_name { - let (key_label, key_len) = data.add_string(name.as_bytes()); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("mov x0, {}", hash_base_reg)); // pass the associative spread hash to the named-key lookup - abi::emit_symbol_address(emitter, key_ptr_reg, &key_label); - abi::emit_load_int_immediate(emitter, key_len_reg, key_len as i64); - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, {}", hash_base_reg)); // pass the associative spread hash to the named-key lookup - abi::emit_symbol_address(emitter, key_ptr_reg, &key_label); - abi::emit_load_int_immediate(emitter, key_len_reg, key_len as i64); - } - } - abi::emit_call_label(emitter, "__rt_hash_get"); - if let Some(found_label) = &found_label { - abi::emit_branch_if_int_result_nonzero(emitter, found_label); - } - } - - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("mov x0, {}", hash_base_reg)); // pass the associative spread hash to the numeric-key lookup - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, {}", hash_base_reg)); // pass the associative spread hash to the numeric-key lookup - } - } - abi::emit_load_int_immediate(emitter, key_ptr_reg, numeric_idx as i64); - abi::emit_load_int_immediate(emitter, key_len_reg, -1); - abi::emit_call_label(emitter, "__rt_hash_get"); - - if let Some(found_label) = found_label { - emitter.label(&found_label); - } -} - -/// Materializes a hash lookup result and pushes it as a call argument, handling Mixed boxing. -/// Calls `materialize_hash_value_to_result` to move the hash lookup output into the standard result registers. -/// For `Mixed` or `Union` source types that must coerce to a narrower target type, preserves the boxed payload -/// on the stack during coercion then releases it afterward via `release_preserved_mixed_after_arg_coercion`. -/// Returns the post-coercion `PhpType` that was pushed. -pub(crate) fn push_loaded_hash_value_arg( - source_elem_ty: &PhpType, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if matches!(source_elem_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - return push_loaded_mixed_hash_value_arg(target_ty, emitter, ctx, data); - } - - materialize_hash_value_to_result(emitter, source_elem_ty); - push_loaded_array_element_arg(source_elem_ty, target_ty, emitter, ctx, data) -} - -/// Pushes a loaded Mixed hash value, dereferencing invoker ref-cell markers when needed. -fn push_loaded_mixed_hash_value_arg( - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let direct_marker_label = ctx.next_label("hash_invoker_ref_value_direct"); - let nested_probe_label = ctx.next_label("hash_invoker_ref_value_probe"); - let nested_marker_label = ctx.next_label("hash_invoker_ref_value_nested"); - let boxed_mixed_label = ctx.next_label("hash_invoker_ref_value_boxed_mixed"); - let ordinary_label = ctx.next_label("hash_invoker_ref_value_ordinary"); - let done_label = ctx.next_label("hash_invoker_ref_value_done"); - let (raw_lo_reg, raw_hi_reg, raw_tag_reg) = raw_hash_value_regs(emitter); - - emit_branch_if_invoker_ref_cell_tag(raw_tag_reg, &direct_marker_label, emitter); - emit_branch_if_hash_value_tag( - raw_tag_reg, - crate::codegen::runtime_value_tag(&PhpType::Mixed), - &nested_probe_label, - emitter, - ); - abi::emit_jump(emitter, &ordinary_label); - - emitter.label(&nested_probe_label); - emit_branch_if_boxed_hash_value_is_invoker_ref_cell( - raw_lo_reg, - &nested_marker_label, - emitter, - ); - abi::emit_jump(emitter, &boxed_mixed_label); - - emitter.label(&ordinary_label); - materialize_hash_value_to_result(emitter, &PhpType::Mixed); - let ordinary_ty = push_materialized_mixed_hash_value_arg(target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - - emitter.label(&boxed_mixed_label); - move_raw_hash_value_lo_to_result(emitter); - abi::emit_incref_if_refcounted(emitter, &PhpType::Mixed); - let boxed_ty = push_materialized_mixed_hash_value_arg(target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - - emitter.label(&direct_marker_label); - let direct_ty = push_raw_invoker_ref_cell_value_arg( - raw_lo_reg, - raw_hi_reg, - target_ty, - emitter, - ctx, - data, - ); - abi::emit_jump(emitter, &done_label); - - emitter.label(&nested_marker_label); - let ref_cell_reg = abi::symbol_scratch_reg(emitter); - let source_tag_reg = abi::secondary_scratch_reg(emitter); - abi::emit_load_from_address(emitter, ref_cell_reg, raw_lo_reg, 8); - abi::emit_load_from_address(emitter, source_tag_reg, raw_lo_reg, 16); - let nested_ty = push_raw_invoker_ref_cell_value_arg( - ref_cell_reg, - source_tag_reg, - target_ty, - emitter, - ctx, - data, - ); - - emitter.label(&done_label); - widen_loaded_arg_type( - &widen_loaded_arg_type(&ordinary_ty, &boxed_ty), - &widen_loaded_arg_type(&direct_ty, &nested_ty), - ) -} - -/// Coerces and pushes a materialized boxed Mixed hash value. -fn push_materialized_mixed_hash_value_arg( - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let release_mixed_after_coerce = target_ty.is_some_and(|target_ty| { - !matches!(target_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) - && super::super::super::can_coerce_result_to_type(&PhpType::Mixed, target_ty) - }); - if release_mixed_after_coerce { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed hash payload while coercing it for the call - } - let (pushed_ty, _boxed_to_mixed) = - coerce_current_value_to_target(emitter, ctx, data, &PhpType::Mixed, target_ty); - if release_mixed_after_coerce { - release_preserved_mixed_after_arg_coercion(emitter, &pushed_ty); - } - push_arg_value(emitter, &pushed_ty); - pushed_ty -} - -/// Pushes the current value inside an invoker reference-cell marker for a non-ref parameter. -fn push_raw_invoker_ref_cell_value_arg( - ref_cell_reg: &str, - source_tag_reg: &str, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emit_box_raw_invoker_ref_cell_value_as_mixed(ref_cell_reg, source_tag_reg, emitter, ctx); - push_materialized_mixed_hash_value_arg(target_ty, emitter, ctx, data) -} - -/// Pushes loaded hash value ref arg onto the temporary call stack or synthetic metadata list. -pub(crate) fn push_loaded_hash_value_ref_arg( - source_elem_ty: &PhpType, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if matches!(source_elem_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - return push_loaded_mixed_hash_value_ref_arg(target_ty, emitter, ctx, data); - } - - materialize_hash_value_to_result(emitter, source_elem_ty); - push_current_result_ref_arg_address(source_elem_ty, target_ty, emitter, ctx, data) -} - -/// Pushes a loaded Mixed hash value as a by-reference argument. -fn push_loaded_mixed_hash_value_ref_arg( - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let direct_marker_label = ctx.next_label("hash_invoker_ref_direct"); - let nested_probe_label = ctx.next_label("hash_invoker_ref_probe"); - let nested_marker_label = ctx.next_label("hash_invoker_ref_nested"); - let ordinary_label = ctx.next_label("hash_invoker_ref_ordinary"); - let done_label = ctx.next_label("hash_invoker_ref_done"); - let (raw_lo_reg, _raw_hi_reg, raw_tag_reg) = raw_hash_value_regs(emitter); - - emit_branch_if_invoker_ref_cell_tag(raw_tag_reg, &direct_marker_label, emitter); - emit_branch_if_hash_value_tag( - raw_tag_reg, - crate::codegen::runtime_value_tag(&PhpType::Mixed), - &nested_probe_label, - emitter, - ); - abi::emit_jump(emitter, &ordinary_label); - - emitter.label(&nested_probe_label); - emit_branch_if_boxed_hash_value_is_invoker_ref_cell( - raw_lo_reg, - &nested_marker_label, - emitter, - ); - abi::emit_jump(emitter, &ordinary_label); - - emitter.label(&direct_marker_label); - move_raw_hash_value_lo_to_result(emitter); - push_arg_value(emitter, &PhpType::Int); - abi::emit_jump(emitter, &done_label); - - emitter.label(&nested_marker_label); - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), raw_lo_reg, 8); - push_arg_value(emitter, &PhpType::Int); - abi::emit_jump(emitter, &done_label); - - emitter.label(&ordinary_label); - materialize_hash_value_to_result(emitter, &PhpType::Mixed); - push_current_result_ref_arg_address(&PhpType::Mixed, target_ty, emitter, ctx, data); - - emitter.label(&done_label); - PhpType::Int -} - -/// Returns the raw value registers produced by `__rt_hash_get`. -fn raw_hash_value_regs(emitter: &Emitter) -> (&'static str, &'static str, &'static str) { - match emitter.target.arch { - Arch::AArch64 => ("x1", "x2", "x3"), - Arch::X86_64 => ("rdi", "rsi", "rcx"), - } -} - -/// Moves the raw hash lookup low payload into the standard integer result register. -fn move_raw_hash_value_lo_to_result(emitter: &mut Emitter) { - let (raw_lo_reg, _, _) = raw_hash_value_regs(emitter); - let result_reg = abi::int_result_reg(emitter); - emitter.instruction(&format!("mov {}, {}", result_reg, raw_lo_reg)); // move the invoker reference-cell address into the standard result register -} - -/// Branches to `label` when a raw hash value tag equals `expected_tag`. -fn emit_branch_if_hash_value_tag( - tag_reg: &str, - expected_tag: u8, - label: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", tag_reg, expected_tag)); // compare the raw hash value tag with the expected runtime tag - emitter.instruction(&format!("b.eq {}", label)); // dispatch this hash value shape when the tag matches - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", tag_reg, expected_tag)); // compare the raw hash value tag with the expected runtime tag - emitter.instruction(&format!("je {}", label)); // dispatch this hash value shape when the tag matches - } - } -} - -/// Branches to `label` when a raw value tag is the invoker reference-cell marker. -fn emit_branch_if_invoker_ref_cell_tag(tag_reg: &str, label: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", tag_reg, INVOKER_ARG_REF_CELL_TAG)); // check for an invoker-only by-reference argument marker - emitter.instruction(&format!("b.eq {}", label)); // use the original caller storage when this hash value is a marker - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", tag_reg, INVOKER_ARG_REF_CELL_TAG)); // check for an invoker-only by-reference argument marker - emitter.instruction(&format!("je {}", label)); // use the original caller storage when this hash value is a marker - } - } -} - -/// Branches to `label` when a boxed Mixed hash value contains an invoker ref-cell marker. -fn emit_branch_if_boxed_hash_value_is_invoker_ref_cell( - mixed_reg: &str, - label: &str, - emitter: &mut Emitter, -) { - let inner_tag_reg = abi::secondary_scratch_reg(emitter); - abi::emit_load_from_address(emitter, inner_tag_reg, mixed_reg, 0); - emit_branch_if_invoker_ref_cell_tag(inner_tag_reg, label, emitter); -} - -/// Boxes the current value referenced by an invoker ref-cell marker into an owned Mixed cell. -fn emit_box_raw_invoker_ref_cell_value_as_mixed( - ref_cell_reg: &str, - source_tag_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let ref_cell_scratch = abi::symbol_scratch_reg(emitter); - let tag_scratch = abi::secondary_scratch_reg(emitter); - let lo_reg = abi::tertiary_scratch_reg(emitter); - let hi_reg = match emitter.target.arch { - Arch::AArch64 => "x12", - Arch::X86_64 => "rdx", - }; - let string_hi_label = ctx.next_label("hash_invoker_ref_string_hi"); - let box_label = ctx.next_label("hash_invoker_ref_box"); - - emitter.instruction(&format!("mov {}, {}", ref_cell_scratch, ref_cell_reg)); // preserve the source variable cell before loading its current value - emitter.instruction(&format!("mov {}, {}", tag_scratch, source_tag_reg)); // preserve the source variable runtime tag before boxing - abi::emit_load_from_address(emitter, lo_reg, ref_cell_scratch, 0); - abi::emit_load_int_immediate(emitter, hi_reg, 0); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #1", tag_scratch)); // does the referenced value use a two-word string slot? - emitter.instruction(&format!("b.eq {}", string_hi_label)); // load the string length only for string reference cells - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, 1", tag_scratch)); // does the referenced value use a two-word string slot? - emitter.instruction(&format!("je {}", string_hi_label)); // load the string length only for string reference cells - } - } - abi::emit_jump(emitter, &box_label); - - emitter.label(&string_hi_label); - abi::emit_load_from_address(emitter, hi_reg, ref_cell_scratch, 8); - - emitter.label(&box_label); - crate::codegen::emit_box_runtime_payload_as_mixed(emitter, tag_scratch, lo_reg, hi_reg); -} - -/// Returns a conservative type for a runtime branch that can push different argument types. -fn widen_loaded_arg_type(left: &PhpType, right: &PhpType) -> PhpType { - if left == right { - left.clone() - } else { - PhpType::Mixed - } -} - -/// Moves the hash lookup result (delivered in architecture-specific register pairs: x1/x2 on ARM64, rdi/rsi on x86_64) -/// into the standard result registers (`x0`/`d0`/`string_result_regs`) based on `source_elem_ty`. -/// For `Int`/`Bool`, moves the scalar; for `Str`, moves pointer and length; for `Float`, moves bits via `fmov`/`movq`; -/// for `Mixed`/`Union`, boxes the runtime payload as `Mixed` using `emit_box_runtime_payload_as_mixed`. -fn materialize_hash_value_to_result(emitter: &mut Emitter, source_elem_ty: &PhpType) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => match source_elem_ty.codegen_repr() { - PhpType::Int | PhpType::Bool => { - emitter.instruction("mov x0, x1"); // move the hash scalar payload into the standard result register - } - PhpType::Str => {} - PhpType::Float => { - emitter.instruction("fmov d0, x1"); // move the hash float bits into the standard result register - } - PhpType::Mixed | PhpType::Union(_) => { - crate::codegen::emit_box_runtime_payload_as_mixed(emitter, "x3", "x1", "x2"); - } - _ => { - emitter.instruction("mov x0, x1"); // move the hash pointer payload into the standard result register - } - }, - crate::codegen::platform::Arch::X86_64 => match source_elem_ty.codegen_repr() { - PhpType::Int | PhpType::Bool => { - emitter.instruction("mov rax, rdi"); // move the hash scalar payload into the standard result register - } - PhpType::Str => { - emitter.instruction("mov rax, rdi"); // move the hash string pointer into the standard result register - emitter.instruction("mov rdx, rsi"); // move the hash string length into the paired result register - } - PhpType::Float => { - emitter.instruction("movq xmm0, rdi"); // move the hash float bits into the standard result register - } - PhpType::Mixed | PhpType::Union(_) => { - crate::codegen::emit_box_runtime_payload_as_mixed(emitter, "rcx", "rdi", "rsi"); - } - _ => { - emitter.instruction("mov rax, rdi"); // move the hash pointer payload into the standard result register - } - }, - } -} - -/// Returns the element type for a spread source based on the container PHP type. -/// For `PhpType::Array` and `PhpType::AssocArray`, returns the inner element type. -/// Runtime `Iterable` values are type-erased and therefore expose `Mixed` elements. -/// For all other types, defaults to `PhpType::Int`. -pub(super) fn spread_source_elem_ty(spread_ty: &PhpType) -> PhpType { - match spread_ty { - PhpType::Array(elem) => (**elem).clone(), - PhpType::AssocArray { value, .. } => (**value).clone(), - PhpType::Iterable => PhpType::Mixed, - _ => PhpType::Int, - } -} diff --git a/src/codegen/expr/calls/args/assoc_variadic.rs b/src/codegen/expr/calls/args/assoc_variadic.rs deleted file mode 100644 index 463308c378..0000000000 --- a/src/codegen/expr/calls/args/assoc_variadic.rs +++ /dev/null @@ -1,366 +0,0 @@ -//! Purpose: -//! Builds associative variadic argument containers from runtime hash sources. -//! Shares keyed `...$rest` construction between call_user_func_array() and spread lowering. -//! -//! Called from: -//! - `crate::codegen::builtins::arrays::call_user_func_array` -//! - `crate::codegen::expr::calls::args::spread` -//! -//! Key details: -//! - Numeric keys consumed by fixed parameters are skipped, while unknown string keys -//! remain in the variadic hash for user-defined callable targets. - -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::codegen::{abi, context::Context}; -use crate::types::{FunctionSig, PhpType}; - -/// Emits assembly for a loaded associative variadic array argument. -#[allow(clippy::too_many_arguments)] -pub(crate) fn emit_loaded_assoc_variadic_array_arg( - source_hash_reg: &str, - elem_ty: &PhpType, - sig: &FunctionSig, - skip_numeric_before: usize, - skip_param_names_before: usize, - context_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let visible_param_count = sig.params.len(); - let variadic_elem_ty = sig - .params - .get(visible_param_count.saturating_sub(1)) - .and_then(|(_, ty)| match ty { - PhpType::Array(elem) => Some((**elem).clone()), - PhpType::Iterable => Some(PhpType::Mixed), - _ => None, - }) - .unwrap_or_else(|| elem_ty.clone()); - let variadic_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(variadic_elem_ty.clone()), - }; - let capacity_reg = abi::int_arg_reg_name(emitter.target, 0); - let tag_reg = abi::int_arg_reg_name(emitter.target, 1); - - emitter.comment(context_label); - abi::emit_load_int_immediate(emitter, capacity_reg, 16); - abi::emit_load_int_immediate( - emitter, - tag_reg, - crate::codegen::runtime_value_tag(&variadic_elem_ty.codegen_repr()) as i64, - ); - abi::emit_call_label(emitter, "__rt_hash_new"); - abi::emit_push_result_value(emitter, &variadic_ty); - - emit_loaded_assoc_variadic_entries( - source_hash_reg, - sig, - skip_numeric_before, - skip_param_names_before, - emitter, - ctx, - data, - ); - - variadic_ty -} - -/// Emits assembly for copying loaded associative source entries into the variadic hash. -fn emit_loaded_assoc_variadic_entries( - source_hash_reg: &str, - sig: &FunctionSig, - skip_numeric_before: usize, - skip_param_names_before: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - const SCRATCH_BYTES: usize = 96; - const CURSOR_OFF: usize = 0; - const SOURCE_HASH_OFF: usize = 8; - const KEY_PTR_OFF: usize = 16; - const KEY_LEN_OFF: usize = 24; - const VALUE_LO_OFF: usize = 32; - const VALUE_HI_OFF: usize = 40; - const VALUE_TAG_OFF: usize = 48; - const NUMERIC_KEY_OFF: usize = 56; - - let loop_label = ctx.next_label("assoc_variadic_loop"); - let done_label = ctx.next_label("assoc_variadic_done"); - let skip_label = ctx.next_label("assoc_variadic_skip"); - let numeric_key_label = ctx.next_label("assoc_variadic_numeric_key"); - let string_key_label = ctx.next_label("assoc_variadic_string_key"); - let insert_label = ctx.next_label("assoc_variadic_insert"); - let value_string_label = ctx.next_label("assoc_variadic_value_string"); - let value_ref_label = ctx.next_label("assoc_variadic_value_ref"); - let value_scalar_label = ctx.next_label("assoc_variadic_value_scalar"); - let insert_call_label = ctx.next_label("assoc_variadic_insert_call"); - - abi::emit_reserve_temporary_stack(emitter, SCRATCH_BYTES); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("str {}, [sp, #{}]", source_hash_reg, SOURCE_HASH_OFF)); // save the source hash for the variadic scan - emitter.instruction(&format!("str xzr, [sp, #{}]", CURSOR_OFF)); // start hash iteration from the insertion-order head - emitter.instruction(&format!("str xzr, [sp, #{}]", NUMERIC_KEY_OFF)); // start numeric variadic keys from zero - } - Arch::X86_64 => { - emitter.instruction(&format!("mov QWORD PTR [rsp + {}], {}", SOURCE_HASH_OFF, source_hash_reg)); // save the source hash for the variadic scan - emitter.instruction(&format!("mov QWORD PTR [rsp + {}], 0", CURSOR_OFF)); // start hash iteration from the insertion-order head - emitter.instruction(&format!("mov QWORD PTR [rsp + {}], 0", NUMERIC_KEY_OFF)); // start numeric variadic keys from zero - } - } - - emitter.label(&loop_label); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x0", SOURCE_HASH_OFF); - abi::emit_load_temporary_stack_slot(emitter, "x1", CURSOR_OFF); - abi::emit_call_label(emitter, "__rt_hash_iter_next"); - emitter.instruction("cmn x0, #1"); // has the associative argument scan reached the terminal cursor? - emitter.instruction(&format!("b.eq {}", done_label)); // finish the variadic hash once every source entry was visited - abi::emit_store_to_address(emitter, "x0", "sp", CURSOR_OFF); - abi::emit_store_to_address(emitter, "x1", "sp", KEY_PTR_OFF); - abi::emit_store_to_address(emitter, "x2", "sp", KEY_LEN_OFF); - abi::emit_store_to_address(emitter, "x3", "sp", VALUE_LO_OFF); - abi::emit_store_to_address(emitter, "x4", "sp", VALUE_HI_OFF); - abi::emit_store_to_address(emitter, "x5", "sp", VALUE_TAG_OFF); - emitter.instruction("cmn x2, #1"); // is the current source key numeric? - emitter.instruction(&format!("b.eq {}", numeric_key_label)); // numeric keys are positional and may belong to ...$rest - emitter.instruction(&format!("b {}", string_key_label)); // string keys must be filtered by regular parameter names - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rdi", SOURCE_HASH_OFF); - abi::emit_load_temporary_stack_slot(emitter, "rsi", CURSOR_OFF); - abi::emit_call_label(emitter, "__rt_hash_iter_next"); - emitter.instruction("cmp rax, -1"); // has the associative argument scan reached the terminal cursor? - emitter.instruction(&format!("je {}", done_label)); // finish the variadic hash once every source entry was visited - abi::emit_store_to_address(emitter, "rax", "rsp", CURSOR_OFF); - abi::emit_store_to_address(emitter, "rdi", "rsp", KEY_PTR_OFF); - abi::emit_store_to_address(emitter, "rdx", "rsp", KEY_LEN_OFF); - abi::emit_store_to_address(emitter, "rcx", "rsp", VALUE_LO_OFF); - abi::emit_store_to_address(emitter, "r8", "rsp", VALUE_HI_OFF); - abi::emit_store_to_address(emitter, "r9", "rsp", VALUE_TAG_OFF); - emitter.instruction("cmp rdx, -1"); // is the current source key numeric? - emitter.instruction(&format!("je {}", numeric_key_label)); // numeric keys are positional and may belong to ...$rest - emitter.instruction(&format!("jmp {}", string_key_label)); // string keys must be filtered by regular parameter names - } - } - - emitter.label(&numeric_key_label); - emit_skip_if_consumed_numeric_key(skip_numeric_before, &skip_label, emitter); - emit_use_next_variadic_numeric_key( - KEY_PTR_OFF, - KEY_LEN_OFF, - NUMERIC_KEY_OFF, - emitter, - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("b {}", insert_label)); // insert the numeric-keyed extra argument into ...$rest - } - Arch::X86_64 => { - emitter.instruction(&format!("jmp {}", insert_label)); // insert the numeric-keyed extra argument into ...$rest - } - } - - emitter.label(&string_key_label); - for (param_name, _) in sig.params.iter().take(skip_param_names_before) { - emit_skip_if_key_matches_param(param_name, &skip_label, emitter, data); - } - - emitter.label(&insert_label); - emit_prepare_and_insert_assoc_variadic_entry( - SCRATCH_BYTES, - KEY_PTR_OFF, - KEY_LEN_OFF, - VALUE_LO_OFF, - VALUE_HI_OFF, - VALUE_TAG_OFF, - &value_string_label, - &value_ref_label, - &value_scalar_label, - &insert_call_label, - &loop_label, - emitter, - ); - - emitter.label(&skip_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("b {}", loop_label)); // continue scanning source entries after skipping a consumed key - } - Arch::X86_64 => { - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning source entries after skipping a consumed key - } - } - - emitter.label(&done_label); - abi::emit_release_temporary_stack(emitter, SCRATCH_BYTES); -} - -/// Emits assembly that skips a numeric source key already consumed by regular parameters. -fn emit_skip_if_consumed_numeric_key( - skip_numeric_before: usize, - skip_label: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x8", 16); - abi::emit_load_int_immediate(emitter, "x9", skip_numeric_before as i64); - emitter.instruction("cmp x8, x9"); // has this numeric key already filled a regular callback parameter? - emitter.instruction(&format!("b.lt {}", skip_label)); // skip numeric keys consumed by the fixed callback prefix - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r10", 16); - abi::emit_load_int_immediate(emitter, "r11", skip_numeric_before as i64); - emitter.instruction("cmp r10, r11"); // has this numeric key already filled a regular callback parameter? - emitter.instruction(&format!("jl {}", skip_label)); // skip numeric keys consumed by the fixed callback prefix - } - } -} - -/// Emits assembly that rewrites an accepted numeric tail key to the next compact variadic key. -fn emit_use_next_variadic_numeric_key( - key_ptr_off: usize, - key_len_off: usize, - numeric_key_off: usize, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x8", numeric_key_off); - abi::emit_store_to_address(emitter, "x8", "sp", key_ptr_off); - abi::emit_load_int_immediate(emitter, "x9", -1); - abi::emit_store_to_address(emitter, "x9", "sp", key_len_off); - emitter.instruction("add x8, x8, #1"); // advance the next numeric variadic key after accepting this positional extra - abi::emit_store_to_address(emitter, "x8", "sp", numeric_key_off); - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r10", numeric_key_off); - abi::emit_store_to_address(emitter, "r10", "rsp", key_ptr_off); - abi::emit_load_int_immediate(emitter, "r11", -1); - abi::emit_store_to_address(emitter, "r11", "rsp", key_len_off); - emitter.instruction("add r10, 1"); // advance the next numeric variadic key after accepting this positional extra - abi::emit_store_to_address(emitter, "r10", "rsp", numeric_key_off); - } - } -} - -/// Emits assembly that skips a string key matching an already-bound regular parameter. -fn emit_skip_if_key_matches_param( - param_name: &str, - skip_label: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (key_label, key_len) = data.add_string(param_name.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x1", 16); - abi::emit_load_temporary_stack_slot(emitter, "x2", 24); - abi::emit_symbol_address(emitter, "x3", &key_label); - abi::emit_load_int_immediate(emitter, "x4", key_len as i64); - abi::emit_call_label(emitter, "__rt_hash_key_eq"); - emitter.instruction("cmp x0, #0"); // did this source key already bind a fixed callback parameter? - emitter.instruction(&format!("b.ne {}", skip_label)); // do not copy consumed named parameters into ...$rest - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rdi", 16); - abi::emit_load_temporary_stack_slot(emitter, "rsi", 24); - abi::emit_symbol_address(emitter, "rdx", &key_label); - abi::emit_load_int_immediate(emitter, "rcx", key_len as i64); - abi::emit_call_label(emitter, "__rt_hash_key_eq"); - emitter.instruction("test rax, rax"); // did this source key already bind a fixed callback parameter? - emitter.instruction(&format!("jne {}", skip_label)); // do not copy consumed named parameters into ...$rest - } - } -} - -/// Emits assembly that prepares a hash entry payload and inserts it into the variadic hash. -#[allow(clippy::too_many_arguments)] -fn emit_prepare_and_insert_assoc_variadic_entry( - hash_slot_off: usize, - key_ptr_off: usize, - key_len_off: usize, - value_lo_off: usize, - value_hi_off: usize, - value_tag_off: usize, - value_string_label: &str, - value_ref_label: &str, - value_scalar_label: &str, - insert_call_label: &str, - loop_label: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x5", value_tag_off); - emitter.instruction("cmp x5, #1"); // does the variadic hash value contain a string payload? - emitter.instruction(&format!("b.eq {}", value_string_label)); // string payloads must be duplicated for the rest hash owner - emitter.instruction("cmp x5, #4"); // is the value in the heap-backed runtime tag range? - emitter.instruction(&format!("b.lo {}", value_scalar_label)); // scalar values can be copied directly into the rest hash - emitter.instruction("cmp x5, #7"); // is the heap-backed tag one of the supported refcounted payloads? - emitter.instruction(&format!("b.hi {}", value_scalar_label)); // unknown high tags fall back to scalar copying - emitter.instruction(&format!("b {}", value_ref_label)); // retain refcounted payloads before insertion - emitter.label(value_string_label); - abi::emit_load_temporary_stack_slot(emitter, "x1", value_lo_off); - abi::emit_load_temporary_stack_slot(emitter, "x2", value_hi_off); - abi::emit_call_label(emitter, "__rt_str_persist"); - emitter.instruction("mov x3, x1"); // pass the owned string pointer as the hash value low word - emitter.instruction("mov x4, x2"); // pass the owned string length as the hash value high word - abi::emit_load_temporary_stack_slot(emitter, "x5", value_tag_off); - emitter.instruction(&format!("b {}", insert_call_label)); // insert the persisted string without reloading the borrowed payload - emitter.label(value_ref_label); - abi::emit_load_temporary_stack_slot(emitter, "x0", value_lo_off); - abi::emit_call_label(emitter, "__rt_incref"); - emitter.label(value_scalar_label); - abi::emit_load_temporary_stack_slot(emitter, "x3", value_lo_off); - abi::emit_load_temporary_stack_slot(emitter, "x4", value_hi_off); - abi::emit_load_temporary_stack_slot(emitter, "x5", value_tag_off); - emitter.label(insert_call_label); - abi::emit_load_temporary_stack_slot(emitter, "x0", hash_slot_off); - abi::emit_load_temporary_stack_slot(emitter, "x1", key_ptr_off); - abi::emit_load_temporary_stack_slot(emitter, "x2", key_len_off); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, "x0", "sp", hash_slot_off); - emitter.instruction(&format!("b {}", loop_label)); // continue scanning source entries after inserting a variadic value - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r9", value_tag_off); - emitter.instruction("cmp r9, 1"); // does the variadic hash value contain a string payload? - emitter.instruction(&format!("je {}", value_string_label)); // string payloads must be duplicated for the rest hash owner - emitter.instruction("cmp r9, 4"); // is the value in the heap-backed runtime tag range? - emitter.instruction(&format!("jb {}", value_scalar_label)); // scalar values can be copied directly into the rest hash - emitter.instruction("cmp r9, 7"); // is the heap-backed tag one of the supported refcounted payloads? - emitter.instruction(&format!("ja {}", value_scalar_label)); // unknown high tags fall back to scalar copying - emitter.instruction(&format!("jmp {}", value_ref_label)); // retain refcounted payloads before insertion - emitter.label(value_string_label); - abi::emit_load_temporary_stack_slot(emitter, "rax", value_lo_off); - abi::emit_load_temporary_stack_slot(emitter, "rdx", value_hi_off); - abi::emit_call_label(emitter, "__rt_str_persist"); - emitter.instruction("mov rcx, rax"); // pass the owned string pointer as the hash value low word - emitter.instruction("mov r8, rdx"); // pass the owned string length as the hash value high word - abi::emit_load_temporary_stack_slot(emitter, "r9", value_tag_off); - emitter.instruction(&format!("jmp {}", insert_call_label)); // insert the persisted string without reloading the borrowed payload - emitter.label(value_ref_label); - abi::emit_load_temporary_stack_slot(emitter, "rax", value_lo_off); - abi::emit_call_label(emitter, "__rt_incref"); - emitter.label(value_scalar_label); - abi::emit_load_temporary_stack_slot(emitter, "rcx", value_lo_off); - abi::emit_load_temporary_stack_slot(emitter, "r8", value_hi_off); - abi::emit_load_temporary_stack_slot(emitter, "r9", value_tag_off); - emitter.label(insert_call_label); - abi::emit_load_temporary_stack_slot(emitter, "rdi", hash_slot_off); - abi::emit_load_temporary_stack_slot(emitter, "rsi", key_ptr_off); - abi::emit_load_temporary_stack_slot(emitter, "rdx", key_len_off); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, "rax", "rsp", hash_slot_off); - emitter.instruction(&format!("jmp {}", loop_label)); // continue scanning source entries after inserting a variadic value - } - } -} diff --git a/src/codegen/expr/calls/args/common.rs b/src/codegen/expr/calls/args/common.rs deleted file mode 100644 index 28aa609c44..0000000000 --- a/src/codegen/expr/calls/args/common.rs +++ /dev/null @@ -1,341 +0,0 @@ -//! Purpose: -//! Lowers shared call-argument coercion, push, and by-reference helpers. -//! Converts evaluated PHP argument expressions into temporary values ready for ABI assignment. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args` -//! -//! Key details: -//! - Argument checks must happen at PHP-observable points without skipping later side effects. - -use crate::codegen::emit::Emitter; -use crate::codegen::{ - abi, - context::{Context, HeapOwnership}, - data_section::DataSection, -}; -use crate::parser::ast::{BinOp, Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; - -/// Returns the declared target PHP type for a parameter, considering explicit type annotations. -pub(crate) fn declared_target_ty<'a>( - sig: Option<&'a FunctionSig>, - param_idx: usize, -) -> Option<&'a PhpType> { - sig.and_then(|sig| { - let target_ty = sig.params.get(param_idx).map(|(_, ty)| ty)?; - if sig - .declared_params - .get(param_idx) - .copied() - .unwrap_or(false) - || matches!(target_ty.codegen_repr(), PhpType::Mixed) - { - Some(target_ty) - } else { - None - } - }) -} - -/// Returns the effective call-target PHP type for a parameter, optionally including inferred types. -pub(crate) fn call_target_ty<'a>( - sig: Option<&'a FunctionSig>, - param_idx: usize, - include_inferred: bool, -) -> Option<&'a PhpType> { - if include_inferred { - sig.and_then(|sig| sig.params.get(param_idx).map(|(_, ty)| ty)) - } else { - declared_target_ty(sig, param_idx) - } -} - -/// Pushes the current value in the result register onto the argument stack for the ABI. -pub(crate) fn push_arg_value(emitter: &mut Emitter, ty: &PhpType) { - abi::emit_push_result_value(emitter, ty); -} - -/// Emits the address of a variable for a by-reference argument and returns whether the variable is valid. -pub(crate) fn emit_ref_arg_variable_address( - var_name: &str, - context_label: &str, - emitter: &mut Emitter, - ctx: &Context, -) -> bool { - if ctx.global_vars.contains(var_name) { - let label = format!("_gvar_{}", var_name); - emitter.comment(&format!("{}: address of global ${}", context_label, var_name)); - abi::emit_symbol_address(emitter, abi::int_result_reg(emitter), &label); - true - } else if ctx.ref_params.contains(var_name) { - let Some(var) = ctx.variables.get(var_name) else { - emitter.comment(&format!("WARNING: undefined ref variable ${}", var_name)); - return false; - }; - emitter.comment(&format!( - "{}: forward underlying reference for ${}", - context_label, var_name - )); - abi::load_at_offset(emitter, abi::int_result_reg(emitter), var.stack_offset); // load the existing by-reference pointer from the current frame slot - true - } else { - let Some(var) = ctx.variables.get(var_name) else { - emitter.comment(&format!("WARNING: undefined variable ${}", var_name)); - return false; - }; - emitter.comment(&format!("{}: address of ${}", context_label, var_name)); - abi::emit_frame_slot_address(emitter, abi::int_result_reg(emitter), var.stack_offset); // compute the local variable's frame-slot address through the ABI helper - true - } -} - -/// Coerces the current value to the target PHP type, returning the pushed type and whether boxing occurred. -pub(crate) fn coerce_current_value_to_target( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - source_ty: &PhpType, - target_ty: Option<&PhpType>, -) -> (PhpType, bool) { - let source_repr = source_ty.codegen_repr(); - let pushed_ty = target_ty - .filter(|target_ty| { - super::super::super::can_coerce_result_to_type(source_ty, target_ty) - }) - .map(PhpType::codegen_repr) - .or_else(|| { - if matches!(source_repr, PhpType::Void) { - Some(PhpType::Int) - } else { - None - } - }) - .unwrap_or_else(|| source_repr.clone()); - let boxed_to_mixed = matches!(pushed_ty, PhpType::Mixed) && !matches!(source_repr, PhpType::Mixed); - - if source_repr != pushed_ty { - let coerce_source_ty = if matches!(pushed_ty, PhpType::Mixed) { - source_ty - } else { - &source_repr - }; - super::super::super::coerce_result_to_type(emitter, ctx, data, coerce_source_ty, &pushed_ty); - } - - (pushed_ty, boxed_to_mixed) -} - -/// Evaluates an argument expression, coerces it to the target type, and pushes it as a call argument. -pub(crate) fn push_expr_arg( - arg: &Expr, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let source_ty = super::super::super::emit_expr(arg, emitter, ctx, data); - let source_repr = source_ty.codegen_repr(); - if target_ty - .is_some_and(|target_ty| matches!(target_ty.codegen_repr(), PhpType::Mixed)) - && !matches!(source_repr, PhpType::Mixed) - { - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, arg, &source_ty, - ); - push_arg_value(emitter, &PhpType::Mixed); - return PhpType::Mixed; - } - let release_mixed_after_coerce = - should_release_owned_mixed_after_arg_coerce(arg, &source_ty, target_ty); - if release_mixed_after_coerce { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - let (pushed_ty, boxed_to_mixed) = - coerce_current_value_to_target(emitter, ctx, data, &source_ty, target_ty); - if release_mixed_after_coerce { - release_preserved_mixed_after_arg_coercion(emitter, &pushed_ty); - } - if !boxed_to_mixed && source_ty.codegen_repr() == pushed_ty { - super::super::super::retain_borrowed_heap_arg(emitter, arg, &source_ty); - } - push_arg_value(emitter, &pushed_ty); - pushed_ty -} - -/// Allocates a by-reference cell for a non-variable argument and pushes its address. -pub(crate) fn push_non_variable_ref_arg_address( - arg: &Expr, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let pushed_ty = push_expr_arg(arg, target_ty, emitter, ctx, data); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 16); - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate a stable 16-byte by-reference cell for a default or temporary argument - let cell_reg = abi::symbol_scratch_reg(emitter); - emitter.instruction(&format!("mov {}, {}", cell_reg, abi::int_result_reg(emitter))); // keep the allocated reference cell address while storing the initial value - store_pushed_value_to_ref_cell(emitter, cell_reg, &pushed_ty); - abi::emit_push_reg(emitter, cell_reg); - PhpType::Int -} - -/// Pushes current result ref arg address onto the temporary call stack or synthetic metadata list. -pub(crate) fn push_current_result_ref_arg_address( - source_ty: &PhpType, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let source_repr = source_ty.codegen_repr(); - let (pushed_ty, boxed_to_mixed) = - coerce_current_value_to_target(emitter, ctx, data, source_ty, target_ty); - if !boxed_to_mixed { - abi::emit_incref_if_refcounted(emitter, &source_repr); - } - push_arg_value(emitter, &pushed_ty); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 16); - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate a stable 16-byte by-reference cell for a dynamic callback argument - let cell_reg = abi::symbol_scratch_reg(emitter); - emitter.instruction(&format!("mov {}, {}", cell_reg, abi::int_result_reg(emitter))); // keep the allocated callback reference cell while storing the loaded argument - store_pushed_value_to_ref_cell(emitter, cell_reg, &pushed_ty); - abi::emit_push_reg(emitter, cell_reg); - PhpType::Int -} - -/// Stores the value currently on the ABI result register into a by-reference heap cell. -/// The cell is organized as: [value_pointer, type_tag] with tag values matching PhpType -/// variants (e.g., 4=Array, 6=Object, 7=Mixed/Union/Iterable, 9=Resource). -/// Takes ownership of the value on the result register. -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); - match val_ty.codegen_repr() { - PhpType::Bool - | PhpType::Int - | PhpType::Callable - | PhpType::Pointer(_) - | PhpType::Buffer(_) - | PhpType::Packed(_) => { - abi::emit_pop_reg(emitter, temp_reg); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); - abi::emit_store_zero_to_address(emitter, cell_reg, 8); - } - PhpType::Resource(_) => { - abi::emit_pop_reg(emitter, temp_reg); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 9); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); - } - PhpType::TaggedScalar => { - let tag_temp_reg = abi::tertiary_scratch_reg(emitter); - abi::emit_pop_reg_pair(emitter, temp_reg, tag_temp_reg); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); - abi::emit_store_to_address(emitter, tag_temp_reg, cell_reg, 8); - } - PhpType::Mixed | PhpType::Union(_) | PhpType::Iterable => { - abi::emit_pop_reg(emitter, temp_reg); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 7); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); - } - PhpType::Array(_) => { - abi::emit_pop_reg(emitter, temp_reg); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 4); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); - } - PhpType::AssocArray { .. } => { - abi::emit_pop_reg(emitter, temp_reg); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 5); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); - } - PhpType::Object(_) => { - abi::emit_pop_reg(emitter, temp_reg); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); - abi::emit_load_int_immediate(emitter, temp_reg, 6); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 8); - } - PhpType::Float => { - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), cell_reg, 0); - abi::emit_store_zero_to_address(emitter, cell_reg, 8); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - abi::emit_push_reg(emitter, cell_reg); - abi::emit_call_label(emitter, "__rt_str_persist"); // detach temporary string storage before putting it in the reference cell - abi::emit_pop_reg(emitter, cell_reg); - abi::emit_store_to_address(emitter, ptr_reg, cell_reg, 0); - abi::emit_store_to_address(emitter, len_reg, cell_reg, 8); - } - PhpType::Void | PhpType::Never => { - abi::emit_store_zero_to_address(emitter, cell_reg, 0); - abi::emit_store_zero_to_address(emitter, cell_reg, 8); - } - } -} - -/// Determines whether an owned Mixed or Union value must be preserved on the temporary -/// stack and released after argument coercion rather than immediately released. -/// Returns true when the source is heap-owned Mixed/Union, the target is a concrete non-Mixed -/// type, and coercion is applicable. Arithmetic binary ops are treated as heap-owned to -/// handle their intermediate results correctly. -fn should_release_owned_mixed_after_arg_coerce( - arg: &Expr, - source_ty: &PhpType, - target_ty: Option<&PhpType>, -) -> bool { - let source_repr = source_ty.codegen_repr(); - let Some(target_repr) = target_ty.map(PhpType::codegen_repr) else { - return false; - }; - matches!(source_repr, PhpType::Mixed | PhpType::Union(_)) - && !matches!(target_repr, PhpType::Mixed | PhpType::Union(_)) - && target_ty.is_some_and(|target_ty| { - super::super::super::can_coerce_result_to_type(source_ty, target_ty) - }) - && (super::super::super::expr_result_heap_ownership(arg) == HeapOwnership::Owned - || matches!( - arg.kind, - ExprKind::BinaryOp { - op: BinOp::Add | BinOp::Sub | BinOp::Mul, - .. - } - )) -} - -/// Releases a preserved Mixed value after coercion when the target type is not Mixed. -pub(crate) fn release_preserved_mixed_after_arg_coercion( - emitter: &mut Emitter, - target_ty: &PhpType, -) { - match target_ty.codegen_repr() { - PhpType::Float => { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, &PhpType::Mixed); - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); - abi::emit_release_temporary_stack(emitter, 16); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_call_label(emitter, "__rt_str_persist"); // detach string casts from the mixed cell before releasing the boxed owner - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, &PhpType::Mixed); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - abi::emit_release_temporary_stack(emitter, 16); - } - _ => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, &PhpType::Mixed); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - abi::emit_release_temporary_stack(emitter, 16); - } - } -} diff --git a/src/codegen/expr/calls/args/emit.rs b/src/codegen/expr/calls/args/emit.rs deleted file mode 100644 index 62ef9df140..0000000000 --- a/src/codegen/expr/calls/args/emit.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Purpose: -//! Lowers top-level call-argument emission from prepared semantic plans. -//! Converts evaluated PHP argument expressions into temporary values ready for ABI assignment. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args` -//! -//! Key details: -//! - Argument checks must happen at PHP-observable points without skipping later side effects. - -use crate::codegen::emit::Emitter; -use crate::codegen::{context::Context, data_section::DataSection}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; - -use super::common::{ - call_target_ty, emit_ref_arg_variable_address, push_arg_value, - push_expr_arg, push_non_variable_ref_arg_address, -}; -use super::named; -use super::normalize::{has_named_args, prepare_call_args}; -use super::spread::{emit_spread_into_named_params, emit_spread_tail_variadic_array_arg}; -use super::variadic::{emit_empty_variadic_array_arg, emit_variadic_array_arg_from_exprs}; -use super::EmittedCallArgs; - -/// Emits all call arguments from an expression list, handling named args, spreads, and variadic params. -/// -/// Dispatches to `named::emit_source_order_named_call_args` when named arguments are present and a -/// signature is available. Otherwise normalizes arguments via `prepare_call_args` and emits them -/// as regular positional arguments, handling by-ref parameters and building variadic arrays as needed. -/// -/// Returns `EmittedCallArgs` containing the collected argument types. The `source_temp_bytes` field -/// is always zero here; it is populated by the caller for source-level temp tracking. -/// -/// # Parameters -/// - `args_exprs`: Raw argument expressions from the PHP call site. -/// - `sig`: Function signature when known; `None` forces positional-only path. -/// - `regular_param_count`: Number of caller-visible regular (non-variadic) parameters. -/// - `ref_arg_context_label`: Label for ref-arg address emission diagnostics. -/// - `retain_non_variable_ref_args`: Whether to retain addresses for non-variable ref args. -/// - `coerce_inferred_params`: Whether to coerce arguments to inferred parameter types. -/// - `emitter`/`ctx`/`data`: Codegen state passed through to sub-emitters. -pub(crate) fn emit_pushed_call_args( - args_exprs: &[Expr], - sig: Option<&FunctionSig>, - regular_param_count: usize, - ref_arg_context_label: &str, - retain_non_variable_ref_args: bool, - coerce_inferred_params: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> EmittedCallArgs { - let has_named = has_named_args(args_exprs); - - if let Some(sig) = sig { - if has_named { - return named::emit_source_order_named_call_args( - args_exprs, - sig, - regular_param_count, - ref_arg_context_label, - retain_non_variable_ref_args, - emitter, - ctx, - data, - ); - } - } - - debug_assert!( - sig.is_some() || !has_named, - "codegen reached named-arg call without a known signature; checker should have rejected this" - ); - - let prepared = prepare_call_args(sig, args_exprs, regular_param_count); - let mut arg_types = emit_pushed_non_variadic_args( - &prepared.all_args, - sig, - ref_arg_context_label, - retain_non_variable_ref_args, - coerce_inferred_params, - emitter, - ctx, - data, - ); - - if prepared.spread_into_named { - if let Some(spread_expr) = prepared.spread_arg.as_ref() { - emit_spread_into_named_params( - spread_expr, - sig, - prepared.spread_at_index, - prepared.regular_param_count, - "named params", - emitter, - ctx, - data, - &mut arg_types, - ); - } - } - - if prepared.is_variadic { - if let Some(spread_expr) = prepared.spread_arg.as_ref() { - let tail_start = prepared - .regular_param_count - .saturating_sub(prepared.spread_at_index); - let variadic_ty = emit_spread_tail_variadic_array_arg( - spread_expr, - sig, - tail_start, - prepared.regular_param_count, - "spread tail as variadic param", - emitter, - ctx, - data, - ); - arg_types.push(variadic_ty); - } else if prepared.variadic_args.is_empty() { - arg_types.push(emit_empty_variadic_array_arg("empty variadic array", emitter)); - } else { - let variadic_ty = emit_variadic_array_arg_from_exprs( - &prepared.variadic_args, - "build variadic array", - true, - true, - emitter, - ctx, - data, - ); - arg_types.push(variadic_ty); - } - } - - EmittedCallArgs { - arg_types, - source_temp_bytes: 0, - } -} - -/// Emits regular (non-variadic) call arguments from a prepared argument list. -/// -/// Iterates over `all_args` and emits each argument according to its role: -/// - **By-ref parameters** (`is_ref=true`): emits the variable's address (for `Variable` expressions) -/// or the address of a temporary (for non-variable expressions), then pushes `PhpType::Int`. -/// - **Regular parameters**: delegates to `push_expr_arg` which evaluates, materializes, and returns -/// the runtime type for each argument. -/// -/// By-ref emission uses `emit_ref_arg_variable_address` for simple variable references, falling back -/// to `push_non_variable_ref_arg_address` for expressions that require a temporary address. -/// -/// # Parameters -/// - `all_args`: Prepared argument expressions to emit. -/// - `sig`: Function signature providing `ref_params` and target-type information. -/// - `ref_arg_context_label`: Diagnostic label passed through to ref-arg emitters. -/// - `_retain_non_variable_ref_args`: Currently unused; retained for API compatibility. -/// - `coerce_inferred_params`: Passed to `call_target_ty` to control type coercion behavior. -/// - `emitter`/`ctx`/`data`: Codegen state passed through to sub-emitters. -/// -/// # Returns -/// A `Vec` listing the runtime type of each emitted argument, in argument order. -pub(crate) fn emit_pushed_non_variadic_args( - all_args: &[Expr], - sig: Option<&FunctionSig>, - ref_arg_context_label: &str, - _retain_non_variable_ref_args: bool, - coerce_inferred_params: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Vec { - let mut arg_types = Vec::new(); - - for (idx, arg) in all_args.iter().enumerate() { - let is_ref = sig - .and_then(|sig| sig.ref_params.get(idx)) - .copied() - .unwrap_or(false); - let target_ty = call_target_ty(sig, idx, coerce_inferred_params); - - if is_ref { - if let ExprKind::Variable(var_name) = &arg.kind { - if !emit_ref_arg_variable_address(var_name, ref_arg_context_label, emitter, ctx) { - continue; - } - push_arg_value(emitter, &PhpType::Int); - } else { - push_non_variable_ref_arg_address(arg, target_ty, emitter, ctx, data); - } - arg_types.push(PhpType::Int); - } else { - let pushed_ty = push_expr_arg(arg, target_ty, emitter, ctx, data); - arg_types.push(pushed_ty); - } - } - - arg_types -} diff --git a/src/codegen/expr/calls/args/mod.rs b/src/codegen/expr/calls/args/mod.rs deleted file mode 100644 index dff51657af..0000000000 --- a/src/codegen/expr/calls/args/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Purpose: -//! Coordinates call-argument lowering from semantic call plans into ABI-ready temporary values. -//! Re-exports helpers for named, spread, variadic, and array-element argument paths. -//! -//! Called from: -//! - `crate::codegen::expr::calls` -//! -//! Key details: -//! - Source-order side effects and ABI-order materialization are deliberately separated across this module tree. - -mod assoc_variadic; -mod array_elements; -mod common; -mod emit; -mod named; -mod normalize; -mod spread; -mod spread_checks; -mod variadic; - -use crate::parser::ast::Expr; -use crate::types::call_args::SpreadBoundsCheck; -use crate::types::PhpType; - -pub(crate) use assoc_variadic::emit_loaded_assoc_variadic_array_arg; -pub(crate) use array_elements::{ - array_element_stride, emit_hash_lookup_for_param_or_index, load_array_element_to_result, - push_loaded_array_element_arg, push_loaded_hash_value_arg, push_loaded_hash_value_ref_arg, -}; -pub(crate) use common::{ - coerce_current_value_to_target, declared_target_ty, emit_ref_arg_variable_address, - push_arg_value, push_current_result_ref_arg_address, push_expr_arg, - push_non_variable_ref_arg_address, release_preserved_mixed_after_arg_coercion, -}; -pub(crate) use emit::emit_pushed_call_args; -pub(crate) use named::pushed_temp_bytes; -pub(crate) use normalize::{ - has_named_args, named_call_arg_temp_name, named_call_prefix_temp_name, - normalize_builtin_call_args_with_checks, normalize_named_call_args_with_checks, - preevaluate_named_call_args_to_temps, regular_param_count, -}; -pub(crate) use spread_checks::emit_spread_length_checks; -use array_elements::spread_source_elem_ty; -use spread_checks::{ - emit_array_length_bounds_check, emit_named_spread_duplicate_abort, - emit_named_spread_length_abort, -}; -use variadic::{store_current_array_element, variadic_container_elem_ty}; -pub(crate) use variadic::emit_empty_variadic_array_arg; - -/// Holds normalized argument expressions paired with their required spread-length validation checks. -/// Produced by `normalize_named_call_args_with_checks` and `normalize_builtin_call_args_with_checks`. -pub(crate) struct NormalizedCallArgs { - pub(crate) args: Vec, - pub(crate) spread_length_checks: Vec, -} - -/// Holds the decomposed call-argument state for positional (non-named) calls. -/// Tracks regular arguments, variadic arguments, spread arguments, and metadata needed for ABI materialization. -/// Produced by `prepare_call_args`. -pub(crate) struct PreparedCallArgs { - pub(crate) all_args: Vec, - pub(crate) variadic_args: Vec, - pub(crate) spread_arg: Option, - pub(crate) spread_at_index: usize, - pub(crate) regular_param_count: usize, - pub(crate) is_variadic: bool, - pub(crate) spread_into_named: bool, -} - -/// Holds the emitted call-argument state after ABI materialization. -/// `arg_types` lists the runtime PHP type of each pushed argument in order. -/// `source_temp_bytes` tracks total stack bytes used for source temporaries (populated by named-arg lowering, zero elsewhere). -pub(crate) struct EmittedCallArgs { - pub(crate) arg_types: Vec, - pub(crate) source_temp_bytes: usize, -} diff --git a/src/codegen/expr/calls/args/named/final_args.rs b/src/codegen/expr/calls/args/named/final_args.rs deleted file mode 100644 index f1b073f686..0000000000 --- a/src/codegen/expr/calls/args/named/final_args.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Purpose: -//! Lowers final ABI argument pushes from named-source descriptors. -//! Works with the shared call-argument plan to preserve PHP named-argument semantics. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args::named` -//! -//! Key details: -//! - Side effects occur in source order, while final argument materialization follows parameter and ABI order. - -use crate::codegen::emit::Emitter; -use crate::codegen::{context::Context, data_section::DataSection}; -use crate::types::{FunctionSig, PhpType}; - -use super::prefix::push_prefix_array_element_arg; -use super::temps::{push_saved_source_temp_arg, pushed_temp_bytes, temp_slot_size}; -use super::variadic::emit_variadic_array_arg_from_sources; -use super::{FinalArgSource, PrefixVariadicTail, VariadicArgSource}; -use super::super::{declared_target_ty, emit_empty_variadic_array_arg, push_expr_arg, EmittedCallArgs}; - -/// Materializes final ABI arguments from named-source descriptors in parameter order. - /// - /// Side effects from prefix-element evaluation and source-temp reads occur in source order - /// (as planned by the shared call-argument planner), while the final ABI push order follows - /// parameter/ABI order. This function consumes `slot_sources` and `variadic_sources`, emitting - /// each argument via the helper chain: `push_saved_source_temp_arg`, `push_prefix_array_element_arg`, - /// `push_expr_arg`, or `emit_variadic_array_arg_from_sources` depending on the source variant. - /// - /// # Parameters - /// - `slot_sources`: per-slot sources for regular parameters (indexed by parameter position). - /// - `variadic_sources`: sources for individual variadic arguments. - /// - `prefix_variadic_tail`: optional prefix-array tail for variadic expansion. - /// - `sig`: callee function signature used to resolve parameter names for named-key preference. - /// - `regular_param_count`: number of caller-visible regular (non-variadic) parameters. - /// - `source_temp_types`: PHP types of saved source temporaries for slot-size calculation. - /// - /// # Returns - /// `EmittedCallArgs` containing the types of all arguments pushed and the total byte size of - /// source temporaries (used by the caller for temp cleanup/frame accounting). -pub(super) fn push_final_call_args_from_sources( - slot_sources: Vec>, - variadic_sources: Vec, - prefix_variadic_tail: Option, - sig: &FunctionSig, - regular_param_count: usize, - source_temp_types: &[PhpType], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> EmittedCallArgs { - let source_temp_bytes = pushed_temp_bytes(source_temp_types); - let mut arg_types = Vec::new(); - let mut final_pushed_bytes = 0usize; - - for (idx, source) in slot_sources.into_iter().enumerate().take(regular_param_count) { - let target_ty = declared_target_ty(Some(sig), idx); - let pushed_ty = match source { - Some(FinalArgSource::SourceTemp(temp_idx)) => { - push_saved_source_temp_arg( - temp_idx, - source_temp_types, - final_pushed_bytes, - emitter, - ) - } - Some(FinalArgSource::PrefixElement { - prefix_temp_idx, - element_idx, - prefer_named_key, - default, - }) => push_prefix_array_element_arg( - prefix_temp_idx, - element_idx, - prefer_named_key - .then(|| sig.params.get(idx).map(|(name, _)| name.as_str())) - .flatten(), - default.as_ref(), - target_ty, - source_temp_types, - final_pushed_bytes, - emitter, - ctx, - data, - ), - Some(FinalArgSource::Default(default)) => { - push_expr_arg(&default, target_ty, emitter, ctx, data) - } - None => continue, - }; - final_pushed_bytes += temp_slot_size(&pushed_ty); - arg_types.push(pushed_ty); - } - - if sig.variadic.is_some() { - let variadic_ty = if variadic_sources.is_empty() && prefix_variadic_tail.is_none() { - emit_empty_variadic_array_arg("empty variadic array", emitter) - } else { - emit_variadic_array_arg_from_sources( - &variadic_sources, - prefix_variadic_tail.as_ref(), - source_temp_types, - final_pushed_bytes, - emitter, - ctx, - data, - ) - }; - arg_types.push(variadic_ty); - } - - EmittedCallArgs { - arg_types, - source_temp_bytes, - } -} diff --git a/src/codegen/expr/calls/args/named/mod.rs b/src/codegen/expr/calls/args/named/mod.rs deleted file mode 100644 index f6e6d7fa10..0000000000 --- a/src/codegen/expr/calls/args/named/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Purpose: -//! Defines shared data structures for named-argument source tracking and final argument sources. -//! Connects source-order evaluation, temporary storage, prefix spreads, and variadic construction. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args` -//! -//! Key details: -//! - Named argument lowering must consume the shared semantic plan instead of rebuilding matching rules. - -mod final_args; -mod prefix; -mod source_order; -mod temps; -mod variadic; - -pub(super) use source_order::emit_source_order_named_call_args; -pub(crate) use temps::pushed_temp_bytes; - -use crate::parser::ast::Expr; - -/// Describes the origin of a final call argument. -/// -/// Variants cover: a previously-evaluated source temp, a prefix-array element -/// (with optional named-key preference and default fallback), or a plain default expression. -#[derive(Clone)] -enum FinalArgSource { - SourceTemp(usize), - PrefixElement { - prefix_temp_idx: usize, - element_idx: usize, - prefer_named_key: bool, - default: Option, - }, - Default(Expr), -} - -/// Tracks one individual variadic argument's key and source. -/// -/// The key is `None` for positional variadic elements and `Some(String)` for -/// named variadic arguments. The source follows the same taxonomy as regular -/// slot arguments. -#[derive(Clone)] -struct VariadicArgSource { - key: Option, - source: FinalArgSource, -} - -/// Records the source prefix array and the starting index for a variadic tail. -/// -/// When a spread prefix array supplies elements beyond the regular parameter -/// count, this struct captures which prefix temp to read from and the first -/// variadic index in that prefix. Used to lazily splice prefix tail elements -/// into the variadic array during final argument materialization. -#[derive(Clone)] -struct PrefixVariadicTail { - prefix_temp_idx: usize, - start_idx: usize, -} diff --git a/src/codegen/expr/calls/args/named/prefix.rs b/src/codegen/expr/calls/args/named/prefix.rs deleted file mode 100644 index b4e9dcaa73..0000000000 --- a/src/codegen/expr/calls/args/named/prefix.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! Purpose: -//! Lowers prefix positional elements produced by spread arrays before named arguments. -//! Works with the shared call-argument plan to preserve PHP named-argument semantics. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args::named` -//! -//! Key details: -//! - Side effects occur in source order, while final argument materialization follows parameter and ABI order. - -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, context::Context, data_section::DataSection}; -use crate::parser::ast::Expr; -use crate::types::{PhpType}; - -use super::temps::source_temp_offset; -use super::super::{ - array_element_stride, emit_array_length_bounds_check, emit_hash_lookup_for_param_or_index, - emit_named_spread_duplicate_abort, emit_named_spread_length_abort, - load_array_element_to_result, push_expr_arg, push_loaded_array_element_arg, - push_loaded_hash_value_arg, spread_source_elem_ty, -}; - -/// Emits a bounds check for the positional prefix length before named spread args. -pub(super) fn emit_prefix_array_length_check( - prefix_temp_idx: usize, - source_temp_types: &[PhpType], - min_len: usize, - max_len: Option, - max_len_param_name: Option<&str>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let ok_label = ctx.next_label("named_prefix_len_ok"); - let underflow_label = ctx.next_label("named_prefix_len_underflow"); - let overflow_label = ctx.next_label("named_prefix_len_overflow"); - emitter.comment("validate named-argument positional prefix length"); - let prefix_offset = source_temp_offset(source_temp_types, prefix_temp_idx, 0); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x8", prefix_offset); - emitter.instruction("ldr x9, [x8]"); // load the evaluated positional-prefix array length - emit_array_length_bounds_check( - "x9", - min_len, - max_len, - &underflow_label, - &overflow_label, - &ok_label, - emitter, - ); - } - crate::codegen::platform::Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r8", prefix_offset); - emitter.instruction("mov r10, QWORD PTR [r8]"); // load the evaluated positional-prefix array length - emit_array_length_bounds_check( - "r10", - min_len, - max_len, - &underflow_label, - &overflow_label, - &ok_label, - emitter, - ); - } - } - emitter.label(&underflow_label); - emit_named_spread_length_abort(emitter, data); - emitter.label(&overflow_label); - if let Some(param_name) = max_len_param_name { - emit_named_spread_duplicate_abort(emitter, data, param_name); - } else { - emit_named_spread_length_abort(emitter, data); - } - emitter.label(&ok_label); -} - -/// Checks dynamic associative spread prefixes for numeric keys that would fill -/// parameters later assigned by explicit named arguments. -pub(super) fn emit_prefix_duplicate_named_checks( - prefix_temp_idx: usize, - source_temp_types: &[PhpType], - duplicate_params: &[(usize, &str)], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if duplicate_params.is_empty() - || !matches!(source_temp_types[prefix_temp_idx], PhpType::AssocArray { .. }) - { - return; - } - - let prefix_offset = source_temp_offset(source_temp_types, prefix_temp_idx, 0); - for (param_idx, param_name) in duplicate_params { - let ok_label = ctx.next_label("named_prefix_duplicate_ok"); - let fail_label = ctx.next_label("named_prefix_duplicate_fail"); - emitter.comment("validate named-argument prefix duplicate"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x8", prefix_offset); - emitter.instruction("mov x0, x8"); // pass the associative prefix hash to the numeric-key duplicate probe - abi::emit_load_int_immediate(emitter, "x1", *param_idx as i64); - abi::emit_load_int_immediate(emitter, "x2", -1); - abi::emit_call_label(emitter, "__rt_hash_get"); - } - crate::codegen::platform::Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r8", prefix_offset); - emitter.instruction("mov rdi, r8"); // pass the associative prefix hash to the numeric-key duplicate probe - abi::emit_load_int_immediate(emitter, "rsi", *param_idx as i64); - abi::emit_load_int_immediate(emitter, "rdx", -1); - abi::emit_call_label(emitter, "__rt_hash_get"); - } - } - abi::emit_branch_if_int_result_nonzero(emitter, &fail_label); - abi::emit_jump(emitter, &ok_label); - emitter.label(&fail_label); - emit_named_spread_duplicate_abort(emitter, data, param_name); - emitter.label(&ok_label); - } -} - -/// Pushes a prefix array element as a named call argument, with optional default. -pub(super) fn push_prefix_array_element_arg( - prefix_temp_idx: usize, - element_idx: usize, - param_name: Option<&str>, - default: Option<&Expr>, - target_ty: Option<&PhpType>, - source_temp_types: &[PhpType], - final_pushed_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if matches!(source_temp_types[prefix_temp_idx], PhpType::AssocArray { .. }) { - return push_assoc_prefix_array_element_arg( - prefix_temp_idx, - element_idx, - param_name, - default, - target_ty, - source_temp_types, - final_pushed_bytes, - emitter, - ctx, - data, - ); - } - - if let Some(default) = default { - let use_default = ctx.next_label("named_prefix_default"); - let done = ctx.next_label("named_prefix_done"); - emit_branch_if_prefix_element_missing( - prefix_temp_idx, - element_idx, - source_temp_types, - final_pushed_bytes, - &use_default, - emitter, - ); - let loaded_ty = push_existing_prefix_array_element_arg( - prefix_temp_idx, - element_idx, - target_ty, - source_temp_types, - final_pushed_bytes, - emitter, - ctx, - data, - ); - abi::emit_jump(emitter, &done); - emitter.label(&use_default); - let default_ty = push_expr_arg(default, target_ty, emitter, ctx, data); - emitter.label(&done); - return super::super::super::super::widen_codegen_type(&loaded_ty, &default_ty); - } - - push_existing_prefix_array_element_arg( - prefix_temp_idx, - element_idx, - target_ty, - source_temp_types, - final_pushed_bytes, - emitter, - ctx, - data, - ) -} - -/// Emits a conditional branch to `label` when the positional-prefix array is too short -/// to contain `element_idx` (i.e., when prefix length <= element index). -/// -/// Loads the prefix array length from the temporary stack slot, compares it against -/// `element_idx`, and jumps to `label` via `b.le` (ARM64) or `jle` (x86_64) if the -/// element does not exist and a default value should be used instead. -fn emit_branch_if_prefix_element_missing( - prefix_temp_idx: usize, - element_idx: usize, - source_temp_types: &[PhpType], - final_pushed_bytes: usize, - label: &str, - emitter: &mut Emitter, -) { - let prefix_offset = source_temp_offset(source_temp_types, prefix_temp_idx, final_pushed_bytes); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x8", prefix_offset); - emitter.instruction("ldr x9, [x8]"); // load prefix length before choosing spread element or default - abi::emit_load_int_immediate(emitter, "x10", element_idx as i64); - emitter.instruction("cmp x9, x10"); // check whether this optional prefix element exists - emitter.instruction(&format!("b.le {}", label)); // use the default when the prefix is too short for this slot - } - crate::codegen::platform::Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r8", prefix_offset); - emitter.instruction("mov r10, QWORD PTR [r8]"); // load prefix length before choosing spread element or default - abi::emit_load_int_immediate(emitter, "r11", element_idx as i64); - emitter.instruction("cmp r10, r11"); // check whether this optional prefix element exists - emitter.instruction(&format!("jle {}", label)); // use the default when the prefix is too short for this slot - } - } -} - -/// Loads the element at `element_idx` from a positional-prefix array and pushes it as a -/// call argument. -/// -/// Uses `array_element_stride` to compute the byte offset into the payload region -/// (skipping the 24-byte array header). Requires the element to exist; does not handle -/// defaults or missing elements. Returns the PHP type of the loaded element. -fn push_existing_prefix_array_element_arg( - prefix_temp_idx: usize, - element_idx: usize, - target_ty: Option<&PhpType>, - source_temp_types: &[PhpType], - final_pushed_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let prefix_ty = source_temp_types[prefix_temp_idx].clone(); - let source_elem_ty = spread_source_elem_ty(&prefix_ty); - let elem_stride = array_element_stride(&source_elem_ty); - let prefix_offset = source_temp_offset(source_temp_types, prefix_temp_idx, final_pushed_bytes); - let array_data_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "x20", - crate::codegen::platform::Arch::X86_64 => "r10", - }; - abi::emit_load_temporary_stack_slot(emitter, array_data_reg, prefix_offset); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #24", array_data_reg, array_data_reg)); // address the positional-prefix array payload - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("add {}, 24", array_data_reg)); // address the positional-prefix array payload - } - } - load_array_element_to_result(emitter, &source_elem_ty, array_data_reg, element_idx * elem_stride); - push_loaded_array_element_arg(&source_elem_ty, target_ty, emitter, ctx, data) -} - -/// Loads and pushes an element from an associative-prefix array as a named call argument. -/// -/// Performs a hash lookup for `param_name` within the associative-prefix array. -/// If `default` is provided, returns the loaded value or the default if the key is absent. -/// If no default is provided and the key is missing, emits a runtime abort (matching PHP -/// behavior for missing required named arguments from spread arrays). -/// -/// Returns the widened PHP type resulting from merging the loaded element type with the -/// default expression type when a default is present. -#[allow(clippy::too_many_arguments)] -fn push_assoc_prefix_array_element_arg( - prefix_temp_idx: usize, - element_idx: usize, - param_name: Option<&str>, - default: Option<&Expr>, - target_ty: Option<&PhpType>, - source_temp_types: &[PhpType], - final_pushed_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let PhpType::AssocArray { value, .. } = &source_temp_types[prefix_temp_idx] else { - unreachable!("assoc prefix helper requires an associative spread prefix"); - }; - let source_elem_ty = *value.clone(); - let prefix_offset = source_temp_offset(source_temp_types, prefix_temp_idx, final_pushed_bytes); - let hash_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "x20", - crate::codegen::platform::Arch::X86_64 => "r12", - }; - abi::emit_load_temporary_stack_slot(emitter, hash_reg, prefix_offset); - emit_hash_lookup_for_param_or_index(hash_reg, param_name, element_idx, emitter, ctx, data); - - if let Some(default) = default { - let use_default = ctx.next_label("named_assoc_prefix_default"); - let done = ctx.next_label("named_assoc_prefix_done"); - abi::emit_branch_if_int_result_zero(emitter, &use_default); - let loaded_ty = push_loaded_hash_value_arg(&source_elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&use_default); - let default_ty = push_expr_arg(default, target_ty, emitter, ctx, data); - emitter.label(&done); - return super::super::super::super::widen_codegen_type(&loaded_ty, &default_ty); - } - - let missing = ctx.next_label("named_assoc_prefix_missing"); - let done = ctx.next_label("named_assoc_prefix_done"); - abi::emit_branch_if_int_result_zero(emitter, &missing); - let loaded_ty = push_loaded_hash_value_arg(&source_elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&missing); - emit_named_spread_length_abort(emitter, data); - emitter.label(&done); - loaded_ty -} diff --git a/src/codegen/expr/calls/args/named/source_order.rs b/src/codegen/expr/calls/args/named/source_order.rs deleted file mode 100644 index fd4dfdc88b..0000000000 --- a/src/codegen/expr/calls/args/named/source_order.rs +++ /dev/null @@ -1,368 +0,0 @@ -//! Purpose: -//! Lowers source-order evaluation for named and spread arguments. -//! Works with the shared call-argument plan to preserve PHP named-argument semantics. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args::named` -//! -//! Key details: -//! - Side effects occur in source order, while final argument materialization follows parameter and ABI order. - -use crate::codegen::emit::Emitter; -use crate::codegen::{context::Context, data_section::DataSection}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::span::Span; -use crate::types::{call_args, FunctionSig, PhpType}; - -use super::final_args::push_final_call_args_from_sources; -use super::prefix::{ - emit_prefix_array_length_check, emit_prefix_duplicate_named_checks, -}; -use super::temps::{emit_source_temp_arg, push_source_temp_type}; -use super::{FinalArgSource, PrefixVariadicTail, VariadicArgSource}; -use super::super::{push_expr_arg, EmittedCallArgs}; - -/// Evaluates source expressions and builds final call args in source order. -pub(in crate::codegen::expr::calls::args) fn emit_source_order_named_call_args( - args_exprs: &[Expr], - sig: &FunctionSig, - regular_param_count: usize, - ref_arg_context_label: &str, - retain_non_variable_ref_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> EmittedCallArgs { - let assoc_spread_sources = assoc_spread_sources(args_exprs, ctx); - let plan = call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( - sig, - args_exprs, - Span::dummy(), - regular_param_count, - false, - true, - &assoc_spread_sources, - ) - .expect("codegen received invalid named call arguments after type checking"); - debug_assert!(plan.has_named_args()); - - if plan.has_spread_args() { - return emit_source_order_named_spread_call_args( - &plan, - sig, - regular_param_count, - Span::dummy(), - emitter, - ctx, - data, - ); - } - - emit_source_order_named_non_spread_call_args( - &plan, - sig, - regular_param_count, - ref_arg_context_label, - retain_non_variable_ref_args, - emitter, - ctx, - data, - ) -} - -/// Emits call args for named non-spread calls. -/// Evaluates each source expression to a temporary, records its type, then maps -/// regular and variadic slots to those temporaries before pushing final args. -fn emit_source_order_named_non_spread_call_args( - plan: &call_args::CallArgPlan, - sig: &FunctionSig, - regular_param_count: usize, - ref_arg_context_label: &str, - retain_non_variable_ref_args: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> EmittedCallArgs { - let mut slot_sources: Vec> = vec![None; regular_param_count]; - let mut variadic_sources = Vec::new(); - let mut source_temp_types = Vec::new(); - let mut source_temp_by_index: Vec> = vec![None; plan.source_args.len()]; - - for source in &plan.source_values { - match source { - call_args::PlannedSourceValue::Regular { - source_index, - param_idx, - expr, - } => { - let temp_idx = emit_source_temp_arg( - expr, - sig, - Some(*param_idx), - ref_arg_context_label, - retain_non_variable_ref_args, - &mut source_temp_types, - emitter, - ctx, - data, - ); - source_temp_by_index[*source_index] = Some(temp_idx); - } - call_args::PlannedSourceValue::Variadic { - source_index, - key, - expr, - } => { - let temp_idx = emit_source_temp_arg( - expr, - sig, - None, - ref_arg_context_label, - retain_non_variable_ref_args, - &mut source_temp_types, - emitter, - ctx, - data, - ); - source_temp_by_index[*source_index] = Some(temp_idx); - variadic_sources.push(VariadicArgSource { - key: key.clone(), - source: FinalArgSource::SourceTemp(temp_idx), - }); - } - } - } - - for (idx, planned) in plan.regular_args.iter().enumerate() { - match planned { - call_args::PlannedRegularArg::Source { source_index, .. } => { - let temp_idx = source_temp_by_index[*source_index] - .expect("planned regular source was not evaluated"); - slot_sources[idx] = Some(FinalArgSource::SourceTemp(temp_idx)); - } - call_args::PlannedRegularArg::Default(default) => { - slot_sources[idx] = Some(FinalArgSource::Default(default.clone())); - } - call_args::PlannedRegularArg::SpreadElement { .. } => { - unreachable!("non-spread named call plan contained a spread element"); - } - } - } - - push_final_call_args_from_sources( - slot_sources, - variadic_sources, - None, - sig, - regular_param_count, - &source_temp_types, - emitter, - ctx, - data, - ) -} - -/// Emits call args for named calls that include spread arguments. -/// The positional prefix is evaluated first, then named arguments, with variadic -/// tail handling determined by the signature. Emits a prefix length check when -/// the prefix does not contain a dynamic named spread. -fn emit_source_order_named_spread_call_args( - plan: &call_args::CallArgPlan, - sig: &FunctionSig, - regular_param_count: usize, - call_span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> EmittedCallArgs { - let first_named_pos = plan.first_named_pos.unwrap_or(plan.source_args.len()); - let prefix_args = &plan.source_args[..first_named_pos]; - let prefix_span = prefix_args - .first() - .map(|arg| arg.span) - .unwrap_or_else(Span::dummy); - let prefix_expr = plan - .positional_prefix_expr(call_span) - .unwrap_or_else(|| Expr::new(ExprKind::ArrayLiteral(Vec::new()), prefix_span)); - let mut source_temp_types = Vec::new(); - emitter.comment("evaluate named-call positional prefix"); - let prefix_ty = push_expr_arg(&prefix_expr, None, emitter, ctx, data); - let prefix_temp_idx = push_source_temp_type(&mut source_temp_types, prefix_ty); - - let mut source_temp_by_index: Vec> = vec![None; plan.source_args.len()]; - let mut variadic_sources = Vec::new(); - for source in &plan.source_values { - if source.source_index() < first_named_pos { - continue; - } - let param_idx = source.param_idx(); - let temp_idx = emit_source_temp_arg( - source.expr(), - sig, - param_idx, - if param_idx.is_some() { - "named arg" - } else { - "named variadic arg" - }, - false, - &mut source_temp_types, - emitter, - ctx, - data, - ); - source_temp_by_index[source.source_index()] = Some(temp_idx); - if param_idx.is_none() { - variadic_sources.push(VariadicArgSource { - key: source.key().map(str::to_string), - source: FinalArgSource::SourceTemp(temp_idx), - }); - } - } - - let fixed_max_prefix_len = plan - .regular_args - .iter() - .filter_map(|planned| match planned { - call_args::PlannedRegularArg::SpreadElement { - prefix_element_idx, - .. - } => Some(prefix_element_idx + 1), - _ => None, - }) - .max() - .unwrap_or(0); - let has_later_named_regular = plan - .source_values - .iter() - .any(|source| source.source_index() >= first_named_pos && source.param_idx().is_some()); - let max_prefix_len = if sig.variadic.is_some() && !has_later_named_regular { - None - } else { - Some(fixed_max_prefix_len) - }; - let duplicate_named_params = later_named_regular_params(plan, sig, first_named_pos); - let max_prefix_param_name = duplicate_named_params - .first() - .map(|(_, param_name)| *param_name); - let min_prefix_len = plan - .regular_args - .iter() - .filter_map(|planned| match planned { - call_args::PlannedRegularArg::SpreadElement { - prefix_element_idx, - default, - .. - } if default.is_none() => Some(prefix_element_idx + 1), - _ => None, - }) - .max() - .unwrap_or(0); - if !plan.prefix_has_dynamic_named_spread { - emit_prefix_array_length_check( - prefix_temp_idx, - &source_temp_types, - min_prefix_len, - max_prefix_len, - max_prefix_param_name, - emitter, - ctx, - data, - ); - } else { - emit_prefix_duplicate_named_checks( - prefix_temp_idx, - &source_temp_types, - &duplicate_named_params, - emitter, - ctx, - data, - ); - } - - let prefix_variadic_tail = if sig.variadic.is_some() && max_prefix_len.is_none() { - Some(PrefixVariadicTail { - prefix_temp_idx, - start_idx: regular_param_count, - }) - } else { - None - }; - - let mut slot_sources = Vec::new(); - for planned in &plan.regular_args { - match planned { - call_args::PlannedRegularArg::Source { source_index, .. } => { - let temp_idx = source_temp_by_index[*source_index] - .expect("planned named source was not evaluated"); - slot_sources.push(Some(FinalArgSource::SourceTemp(temp_idx))); - } - call_args::PlannedRegularArg::SpreadElement { - prefix_element_idx, - prefer_named_key, - default, - guaranteed_present, - .. - } => { - slot_sources.push(Some(FinalArgSource::PrefixElement { - prefix_temp_idx, - element_idx: *prefix_element_idx, - prefer_named_key: *prefer_named_key, - default: if *guaranteed_present { - None - } else { - default.clone() - }, - })); - } - call_args::PlannedRegularArg::Default(default) => { - slot_sources.push(Some(FinalArgSource::Default(default.clone()))); - } - } - } - - push_final_call_args_from_sources( - slot_sources, - variadic_sources, - prefix_variadic_tail, - sig, - regular_param_count, - &source_temp_types, - emitter, - ctx, - data, - ) -} - -/// Returns a bool vector indicating which spread elements in `args` are -/// associative arrays. Used to determine whether static named spread arguments -/// can be reordered into the final arg list. -fn assoc_spread_sources(args: &[Expr], ctx: &Context) -> Vec { - call_args::expand_static_assoc_spread_args(args) - .iter() - .map(|arg| match &arg.kind { - ExprKind::Spread(inner) => matches!( - crate::codegen::functions::infer_contextual_type(inner, ctx), - PhpType::AssocArray { .. } - ), - _ => false, - }) - .collect() -} - -/// Returns later explicit named regular parameters in source order for duplicate checks. -fn later_named_regular_params<'a>( - plan: &'a call_args::CallArgPlan, - sig: &'a FunctionSig, - first_named_pos: usize, -) -> Vec<(usize, &'a str)> { - plan.source_values - .iter() - .filter(|source| source.source_index() >= first_named_pos) - .filter_map(|source| { - let param_idx = source.param_idx()?; - let param_name = sig.params.get(param_idx).map(|(name, _)| name.as_str())?; - Some((param_idx, param_name)) - }) - .collect() -} diff --git a/src/codegen/expr/calls/args/named/temps.rs b/src/codegen/expr/calls/args/named/temps.rs deleted file mode 100644 index 4a0e3bf103..0000000000 --- a/src/codegen/expr/calls/args/named/temps.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Purpose: -//! Lowers hidden temporary slots for named-argument preevaluation. -//! Works with the shared call-argument plan to preserve PHP named-argument semantics. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args::named` -//! -//! Key details: -//! - Side effects occur in source order, while final argument materialization follows parameter and ABI order. - -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, context::Context, data_section::DataSection}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; - -use super::super::{ - declared_target_ty, emit_ref_arg_variable_address, push_arg_value, push_expr_arg, - push_non_variable_ref_arg_address, -}; - -/// Appends a source temp type and returns its index. -pub(super) fn push_source_temp_type(source_temp_types: &mut Vec, ty: PhpType) -> usize { - let idx = source_temp_types.len(); - source_temp_types.push(ty); - idx -} - -/// Emits a source argument into a temp slot and returns the temp index. -#[allow(clippy::too_many_arguments)] -pub(super) fn emit_source_temp_arg( - arg: &Expr, - sig: &FunctionSig, - param_idx: Option, - ref_arg_context_label: &str, - _retain_non_variable_ref_args: bool, - source_temp_types: &mut Vec, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> usize { - let is_ref = param_idx - .and_then(|idx| sig.ref_params.get(idx)) - .copied() - .unwrap_or(false); - let pushed_ty = if is_ref { - if let ExprKind::Variable(var_name) = &arg.kind { - emit_ref_arg_variable_address(var_name, ref_arg_context_label, emitter, ctx); - push_arg_value(emitter, &PhpType::Int); - } else { - let target_ty = param_idx.and_then(|idx| declared_target_ty(Some(sig), idx)); - push_non_variable_ref_arg_address(arg, target_ty, emitter, ctx, data); - } - PhpType::Int - } else { - let target_ty = param_idx.and_then(|idx| declared_target_ty(Some(sig), idx)); - push_expr_arg(arg, target_ty, emitter, ctx, data) - }; - push_source_temp_type(source_temp_types, pushed_ty) -} - -/// Returns the stack slot size for a PhpType (16 bytes, or 0 for void/never). -pub(super) fn temp_slot_size(ty: &PhpType) -> usize { - if matches!(ty, PhpType::Void | PhpType::Never) { - 0 - } else { - 16 - } -} - -/// Computes the total bytes needed for all source temps (for stack allocation). -pub(crate) fn pushed_temp_bytes(types: &[PhpType]) -> usize { - types.iter().map(temp_slot_size).sum() -} - -/// Computes reversed cumulative offsets (from low to high memory) for temp slots. -fn temp_offsets(types: &[PhpType]) -> Vec { - let mut offsets = vec![0usize; types.len()]; - let mut running = 0usize; - for idx in (0..types.len()).rev() { - offsets[idx] = running; - running += temp_slot_size(&types[idx]); - } - offsets -} - -/// Computes the stack offset for a source temp slot, including extra_bytes preamble. -pub(super) fn source_temp_offset(source_temp_types: &[PhpType], temp_idx: usize, extra_bytes: usize) -> usize { - extra_bytes + temp_offsets(source_temp_types)[temp_idx] -} - -/// Loads a saved source temp into the result register and returns its type. -pub(super) fn load_source_temp_to_result( - temp_idx: usize, - source_temp_types: &[PhpType], - extra_bytes: usize, - emitter: &mut Emitter, -) -> PhpType { - let ty = source_temp_types[temp_idx].clone(); - let offset = source_temp_offset(source_temp_types, temp_idx, extra_bytes); - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_load_temporary_stack_slot(emitter, abi::float_result_reg(emitter), offset); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, offset); - abi::emit_load_temporary_stack_slot(emitter, len_reg, offset + 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), offset); - } - } - ty -} - -/// Pushes a saved source temp arg onto the ABI stack and returns its type. -pub(super) fn push_saved_source_temp_arg( - temp_idx: usize, - source_temp_types: &[PhpType], - final_pushed_bytes: usize, - emitter: &mut Emitter, -) -> PhpType { - let ty = load_source_temp_to_result(temp_idx, source_temp_types, final_pushed_bytes, emitter); - push_arg_value(emitter, &ty); - ty -} diff --git a/src/codegen/expr/calls/args/named/variadic.rs b/src/codegen/expr/calls/args/named/variadic.rs deleted file mode 100644 index d2aba766c0..0000000000 --- a/src/codegen/expr/calls/args/named/variadic.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! Purpose: -//! Lowers variadic array construction from named argument sources. -//! Works with the shared call-argument plan to preserve PHP named-argument semantics. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args::named` -//! -//! Key details: -//! - Side effects occur in source order, while final argument materialization follows parameter and ABI order. - -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, context::Context, data_section::DataSection}; -use crate::types::PhpType; - -use super::temps::{load_source_temp_to_result, source_temp_offset}; -use super::{FinalArgSource, PrefixVariadicTail, VariadicArgSource}; -use super::super::{ - array_element_stride, load_array_element_to_result, spread_source_elem_ty, - store_current_array_element, variadic_container_elem_ty, -}; - -/// Builds the variadic array from named sources, including optional prefix tail. -pub(super) fn emit_variadic_array_arg_from_sources( - variadic_sources: &[VariadicArgSource], - prefix_variadic_tail: Option<&PrefixVariadicTail>, - source_temp_types: &[PhpType], - final_pushed_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if variadic_sources.iter().any(|source| source.key.is_some()) { - return emit_variadic_assoc_arg_from_sources( - variadic_sources, - prefix_variadic_tail, - source_temp_types, - final_pushed_bytes, - emitter, - ctx, - data, - ); - } - - let elem_count = variadic_sources.len(); - let first_elem_ty = match variadic_sources.first() { - Some(VariadicArgSource { - source: FinalArgSource::SourceTemp(temp_idx), - .. - }) => source_temp_types[*temp_idx].clone(), - _ => PhpType::Int, - }; - let container_elem_ty = variadic_container_elem_ty(&first_elem_ty); - let elem_size = match container_elem_ty.codegen_repr() { - PhpType::Str => 16, - _ => 8, - }; - let (capacity_reg, elem_size_reg, peek_reg, len_reg) = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => ("x0", "x1", "x9", "x10"), - crate::codegen::platform::Arch::X86_64 => ("rdi", "rsi", "r11", "r10"), - }; - - emitter.comment(&format!("build variadic array ({} elements)", elem_count)); - abi::emit_load_int_immediate(emitter, capacity_reg, elem_count as i64); - abi::emit_load_int_immediate(emitter, elem_size_reg, elem_size as i64); - abi::emit_call_label(emitter, "__rt_array_new"); - abi::emit_push_result_value(emitter, &PhpType::Array(Box::new(container_elem_ty.clone()))); - - for (idx, source) in variadic_sources.iter().enumerate() { - let mut elem_ty = match &source.source { - FinalArgSource::SourceTemp(temp_idx) => load_source_temp_to_result( - *temp_idx, - source_temp_types, - final_pushed_bytes + 16, - emitter, - ), - _ => PhpType::Int, - }; - let boxed_for_container = if matches!(container_elem_ty, PhpType::Mixed) - && !matches!(elem_ty, PhpType::Mixed | PhpType::Union(_)) - { - crate::codegen::emit_box_current_value_as_mixed(emitter, &elem_ty); - elem_ty = PhpType::Mixed; - true - } else { - false - }; - if !boxed_for_container { - abi::emit_incref_if_refcounted(emitter, &elem_ty.codegen_repr()); - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("ldr {}, [sp]", peek_reg)); // peek the variadic array pointer without removing it from the stack - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, QWORD PTR [rsp]", peek_reg)); // peek the variadic array pointer without removing it from the stack - } - } - if idx == 0 { - super::super::super::super::arrays::emit_array_value_type_stamp(emitter, peek_reg, &elem_ty); - } - store_current_array_element(emitter, peek_reg, idx, &elem_ty); - abi::emit_load_int_immediate(emitter, len_reg, (idx + 1) as i64); - abi::emit_store_to_address(emitter, len_reg, peek_reg, 0); - } - - PhpType::Array(Box::new(container_elem_ty)) -} - -/// Copies spread-tail elements from a prefix array into a named variadic hash table. -/// Uses a pre-allocated scratch area on the stack to hold loop counters and hash state -/// across iterations. Emits a dynamic loop that walks the tail portion of the prefix -/// array and inserts each element into the hash via `__rt_hash_set`. The loop stops -/// when the tail index reaches the computed tail length. Preserves PHP semantics: -/// numeric keys are generated from zero-based tail indices, and strings are persisted -/// before insertion. Returns the final array length in the scratch slot. -fn emit_prefix_tail_into_variadic_hash( - tail: &PrefixVariadicTail, - container_elem_ty: &PhpType, - source_temp_types: &[PhpType], - final_pushed_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) { - const SCRATCH_BYTES: usize = 48; - const HASH_SLOT_BYTES: usize = 16; - - let source_elem_ty = spread_source_elem_ty(&source_temp_types[tail.prefix_temp_idx]); - let elem_stride = array_element_stride(&source_elem_ty); - let loop_start = ctx.next_label("named_variadic_tail_loop"); - let loop_done = ctx.next_label("named_variadic_tail_done"); - let tail_empty = ctx.next_label("named_variadic_tail_empty"); - let tail_ready = ctx.next_label("named_variadic_tail_ready"); - let result_reg = abi::int_result_reg(emitter); - let hash_reg = abi::int_arg_reg_name(emitter.target, 0); - let key_ptr_reg = abi::int_arg_reg_name(emitter.target, 1); - let key_len_reg = abi::int_arg_reg_name(emitter.target, 2); - let value_lo_reg = abi::int_arg_reg_name(emitter.target, 3); - let value_hi_reg = abi::int_arg_reg_name(emitter.target, 4); - let value_tag_reg = abi::int_arg_reg_name(emitter.target, 5); - let zero_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "xzr", - crate::codegen::platform::Arch::X86_64 => "0", - }; - let stack_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "sp", - crate::codegen::platform::Arch::X86_64 => "rsp", - }; - - emitter.comment("copy spread tail into named variadic array"); - abi::emit_reserve_temporary_stack(emitter, SCRATCH_BYTES); - let prefix_offset = source_temp_offset( - source_temp_types, - tail.prefix_temp_idx, - final_pushed_bytes + SCRATCH_BYTES + HASH_SLOT_BYTES, - ); - - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x10", prefix_offset); - abi::emit_store_to_address(emitter, "x10", stack_reg, 32); - emitter.instruction("ldr x9, [x10]"); // load the evaluated spread prefix length before slicing its variadic tail - abi::emit_load_int_immediate(emitter, "x11", tail.start_idx as i64); - emitter.instruction("cmp x9, x11"); // check whether the prefix has values beyond the regular parameters - emitter.instruction(&format!("b.le {}", tail_empty)); // no variadic tail exists when the prefix fits in regular parameters - emitter.instruction("sub x9, x9, x11"); // compute variadic tail length as prefix length minus regular parameter count - emitter.instruction(&format!("b {}", tail_ready)); // store the computed non-empty variadic tail length - emitter.label(&tail_empty); - emitter.instruction("mov x9, #0"); // use an empty variadic tail when the prefix has no remaining values - emitter.label(&tail_ready); - abi::emit_store_to_address(emitter, "x9", stack_reg, 16); - abi::emit_store_to_address(emitter, "xzr", stack_reg, 0); - - emitter.label(&loop_start); - abi::emit_load_temporary_stack_slot(emitter, "x8", 0); - abi::emit_load_temporary_stack_slot(emitter, "x9", 16); - emitter.instruction("cmp x8, x9"); // stop after every spread-tail element has been copied into ...$rest - emitter.instruction(&format!("b.ge {}", loop_done)); // finish the dynamic variadic-tail copy loop - abi::emit_load_temporary_stack_slot(emitter, "x10", 32); - abi::emit_load_int_immediate(emitter, "x11", tail.start_idx as i64); - emitter.instruction("add x11, x11, x8"); // convert tail index to source prefix element index - if elem_stride == 16 { - emitter.instruction("lsl x11, x11, #4"); // scale source prefix element index by the string slot width - } else { - emitter.instruction("lsl x11, x11, #3"); // scale source prefix element index by the scalar slot width - } - emitter.instruction("add x10, x10, #24"); // address the spread prefix payload after its array header - emitter.instruction("add x10, x10, x11"); // address the current spread-tail element payload slot - load_array_element_to_result(emitter, &source_elem_ty, "x10", 0); - } - crate::codegen::platform::Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r12", prefix_offset); - abi::emit_store_to_address(emitter, "r12", stack_reg, 32); - emitter.instruction("mov r11, QWORD PTR [r12]"); // load the evaluated spread prefix length before slicing its variadic tail - abi::emit_load_int_immediate(emitter, "r10", tail.start_idx as i64); - emitter.instruction("cmp r11, r10"); // check whether the prefix has values beyond the regular parameters - emitter.instruction(&format!("jle {}", tail_empty)); // no variadic tail exists when the prefix fits in regular parameters - emitter.instruction("sub r11, r10"); // compute variadic tail length as prefix length minus regular parameter count - emitter.instruction(&format!("jmp {}", tail_ready)); // store the computed non-empty variadic tail length - emitter.label(&tail_empty); - emitter.instruction("mov r11, 0"); // use an empty variadic tail when the prefix has no remaining values - emitter.label(&tail_ready); - abi::emit_store_to_address(emitter, "r11", stack_reg, 16); - abi::emit_store_to_address(emitter, "0", stack_reg, 0); - - emitter.label(&loop_start); - abi::emit_load_temporary_stack_slot(emitter, "r10", 0); - abi::emit_load_temporary_stack_slot(emitter, "r11", 16); - emitter.instruction("cmp r10, r11"); // stop after every spread-tail element has been copied into ...$rest - emitter.instruction(&format!("jge {}", loop_done)); // finish the dynamic variadic-tail copy loop - abi::emit_load_temporary_stack_slot(emitter, "r12", 32); - abi::emit_load_int_immediate(emitter, "r11", tail.start_idx as i64); - emitter.instruction("add r11, r10"); // convert tail index to source prefix element index - emitter.instruction(&format!("imul r11, {}", elem_stride)); // scale source prefix element index by the payload slot width - emitter.instruction("add r12, 24"); // address the spread prefix payload after its array header - emitter.instruction("add r12, r11"); // address the current spread-tail element payload slot - load_array_element_to_result(emitter, &source_elem_ty, "r12", 0); - } - } - - let mut elem_ty = source_elem_ty.clone(); - let boxed_for_container = if matches!(container_elem_ty, PhpType::Mixed) - && !matches!(elem_ty, PhpType::Mixed | PhpType::Union(_)) - { - crate::codegen::emit_box_current_value_as_mixed(emitter, &elem_ty); - elem_ty = PhpType::Mixed; - true - } else { - false - }; - if !boxed_for_container && matches!(elem_ty, PhpType::Str) { - abi::emit_call_label(emitter, "__rt_str_persist"); // persist spread-tail strings before storing them in the variadic hash - } else if !boxed_for_container { - abi::emit_incref_if_refcounted(emitter, &elem_ty.codegen_repr()); - } - - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x8", 0); - emitter.instruction(&format!("mov {}, x8", key_ptr_reg)); // use the zero-based tail index as the numeric variadic key - abi::emit_load_int_immediate(emitter, key_len_reg, -1); - } - crate::codegen::platform::Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r10", 0); - emitter.instruction(&format!("mov {}, r10", key_ptr_reg)); // use the zero-based tail index as the numeric variadic key - abi::emit_load_int_immediate(emitter, key_len_reg, -1); - } - } - - let (val_lo, val_hi) = match elem_ty.codegen_repr() { - PhpType::Float => { - let bits_reg = abi::temp_int_reg(emitter.target); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("fmov {}, {}", bits_reg, abi::float_result_reg(emitter))); // move variadic float bits into the hash value register - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("movq {}, {}", bits_reg, abi::float_result_reg(emitter))); // move variadic float bits into the hash value register - } - } - (bits_reg, zero_reg) - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - (ptr_reg, len_reg) - } - _ => (result_reg, zero_reg), - }; - emitter.instruction(&format!("mov {}, {}", value_lo_reg, val_lo)); // move the spread-tail value low word into the hash-set ABI register - emitter.instruction(&format!("mov {}, {}", value_hi_reg, val_hi)); // move the spread-tail value high word into the hash-set ABI register - abi::emit_load_int_immediate( - emitter, - value_tag_reg, - crate::codegen::runtime_value_tag(&elem_ty) as i64, - ); - abi::emit_load_temporary_stack_slot(emitter, hash_reg, SCRATCH_BYTES); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, result_reg, stack_reg, SCRATCH_BYTES); - - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x8", 0); - emitter.instruction("add x8, x8, #1"); // advance to the next spread-tail variadic element - abi::emit_store_to_address(emitter, "x8", stack_reg, 0); - emitter.instruction(&format!("b {}", loop_start)); // continue copying spread-tail elements into ...$rest - } - crate::codegen::platform::Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r10", 0); - emitter.instruction("add r10, 1"); // advance to the next spread-tail variadic element - abi::emit_store_to_address(emitter, "r10", stack_reg, 0); - emitter.instruction(&format!("jmp {}", loop_start)); // continue copying spread-tail elements into ...$rest - } - } - - emitter.label(&loop_done); - abi::emit_release_temporary_stack(emitter, SCRATCH_BYTES); -} - -/// Builds a named (associative) variadic array from source temps and an optional prefix tail. -/// Dispatches to `emit_prefix_tail_into_variadic_hash` for the prefix tail, then iterates over -/// `variadic_sources` to insert each named element into a hash table via `__rt_hash_set`. -/// Numeric keys use a length of `-1`; string keys are loaded from the data section. -/// Returns the final `PhpType::AssocArray` with `Mixed` keys and a uniform `container_elem_ty`. -/// Side effects (incref, str_persist) occur in source order before hash insertion. -fn emit_variadic_assoc_arg_from_sources( - variadic_sources: &[VariadicArgSource], - prefix_variadic_tail: Option<&PrefixVariadicTail>, - source_temp_types: &[PhpType], - final_pushed_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let elem_count = variadic_sources.len(); - let first_elem_ty = if let Some(tail) = prefix_variadic_tail { - spread_source_elem_ty(&source_temp_types[tail.prefix_temp_idx]) - } else { - match variadic_sources.first() { - Some(VariadicArgSource { - source: FinalArgSource::SourceTemp(temp_idx), - .. - }) => source_temp_types[*temp_idx].clone(), - _ => PhpType::Int, - } - }; - let container_elem_ty = variadic_container_elem_ty(&first_elem_ty); - let hash_capacity_reg = abi::int_arg_reg_name(emitter.target, 0); - let key_ptr_reg = abi::int_arg_reg_name(emitter.target, 1); - let key_len_reg = abi::int_arg_reg_name(emitter.target, 2); - let value_lo_reg = abi::int_arg_reg_name(emitter.target, 3); - let value_hi_reg = abi::int_arg_reg_name(emitter.target, 4); - let value_tag_reg = abi::int_arg_reg_name(emitter.target, 5); - let tag_reg = abi::int_arg_reg_name(emitter.target, 1); - let result_reg = abi::int_result_reg(emitter); - let stack_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "sp", - crate::codegen::platform::Arch::X86_64 => "rsp", - }; - let zero_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "xzr", - crate::codegen::platform::Arch::X86_64 => "0", - }; - - emitter.comment(&format!("build named variadic array ({} elements)", elem_count)); - abi::emit_load_int_immediate( - emitter, - hash_capacity_reg, - std::cmp::max(elem_count * 2, 16) as i64, - ); - abi::emit_load_int_immediate( - emitter, - tag_reg, - crate::codegen::runtime_value_tag(&container_elem_ty) as i64, - ); - abi::emit_call_label(emitter, "__rt_hash_new"); - abi::emit_push_result_value(emitter, &PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(container_elem_ty.clone()), - }); - - if let Some(tail) = prefix_variadic_tail { - emit_prefix_tail_into_variadic_hash( - tail, - &container_elem_ty, - source_temp_types, - final_pushed_bytes, - emitter, - ctx, - ); - } - - for (idx, source) in variadic_sources.iter().enumerate() { - match &source.key { - Some(key) => { - let (key_label, key_len) = data.add_string(key.as_bytes()); - abi::emit_symbol_address(emitter, key_ptr_reg, &key_label); - abi::emit_load_int_immediate(emitter, key_len_reg, key_len as i64); - } - None => { - abi::emit_load_int_immediate(emitter, key_ptr_reg, idx as i64); - abi::emit_load_int_immediate(emitter, key_len_reg, -1); - } - } - abi::emit_push_reg_pair(emitter, key_ptr_reg, key_len_reg); // preserve the variadic hash key while loading the saved argument value - let mut elem_ty = match &source.source { - FinalArgSource::SourceTemp(temp_idx) => load_source_temp_to_result( - *temp_idx, - source_temp_types, - final_pushed_bytes + 32, - emitter, - ), - _ => PhpType::Int, - }; - let boxed_for_container = if matches!(container_elem_ty, PhpType::Mixed) - && !matches!(elem_ty, PhpType::Mixed | PhpType::Union(_)) - { - crate::codegen::emit_box_current_value_as_mixed(emitter, &elem_ty); - elem_ty = PhpType::Mixed; - true - } else { - false - }; - if !boxed_for_container && matches!(elem_ty, PhpType::Str) { - abi::emit_call_label(emitter, "__rt_str_persist"); // persist variadic strings before storing them in the hash table - } else if !boxed_for_container { - abi::emit_incref_if_refcounted(emitter, &elem_ty.codegen_repr()); - } - let (val_lo, val_hi) = match elem_ty.codegen_repr() { - PhpType::Float => { - let bits_reg = abi::temp_int_reg(emitter.target); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("fmov {}, {}", bits_reg, abi::float_result_reg(emitter))); // move variadic float bits into the hash value register - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("movq {}, {}", bits_reg, abi::float_result_reg(emitter))); // move variadic float bits into the hash value register - } - } - (bits_reg, zero_reg) - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - (ptr_reg, len_reg) - } - _ => (result_reg, zero_reg), - }; - emitter.instruction(&format!("mov {}, {}", value_lo_reg, val_lo)); // move the variadic value low word into the hash-set ABI register - emitter.instruction(&format!("mov {}, {}", value_hi_reg, val_hi)); // move the variadic value high word into the hash-set ABI register - abi::emit_load_int_immediate( - emitter, - value_tag_reg, - crate::codegen::runtime_value_tag(&elem_ty) as i64, - ); - abi::emit_pop_reg_pair(emitter, key_ptr_reg, key_len_reg); // restore the variadic hash key into the hash-set ABI registers - abi::emit_load_temporary_stack_slot(emitter, hash_capacity_reg, 0); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, result_reg, stack_reg, 0); - } - - PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(container_elem_ty), - } -} diff --git a/src/codegen/expr/calls/args/normalize.rs b/src/codegen/expr/calls/args/normalize.rs deleted file mode 100644 index 4e91105300..0000000000 --- a/src/codegen/expr/calls/args/normalize.rs +++ /dev/null @@ -1,381 +0,0 @@ -//! Purpose: -//! Lowers preevaluation and normalization before ABI materialization. -//! Converts evaluated PHP argument expressions into temporary values ready for ABI assignment. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args` -//! -//! Key details: -//! - Argument checks must happen at PHP-observable points without skipping later side effects. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::{Expr, ExprKind}; -use crate::span::Span; -use crate::types::{call_args, FunctionSig, PhpType}; - -use super::{NormalizedCallArgs, PreparedCallArgs}; - -/// Returns true if any argument is a named argument. -pub(crate) fn has_named_args(args: &[Expr]) -> bool { - call_args::has_named_args(args) -} - -/// Returns the regular (non-variadic) parameter count from a signature, or fallback. -pub(crate) fn regular_param_count(sig: Option<&FunctionSig>, fallback_arg_count: usize) -> usize { - sig.map(call_args::regular_param_count) - .unwrap_or(fallback_arg_count) -} - -/// Generates a unique temp name for a named call argument at a given position. -pub(crate) fn named_call_arg_temp_name(call_span: Span, idx: usize) -> String { - format!( - "__elephc_named_arg_{}_{}_{}", - call_span.line, call_span.col, idx - ) -} - -/// Generates a unique temp name for the positional prefix in a spread named call. -pub(crate) fn named_call_prefix_temp_name(call_span: Span) -> String { - format!("__elephc_named_prefix_{}_{}", call_span.line, call_span.col) -} - -/// Normalizes named call args with runtime checks for spread bounds. -pub(crate) fn normalize_named_call_args_with_checks( - sig: &FunctionSig, - args: &[Expr], - regular_param_count: usize, -) -> NormalizedCallArgs { - normalize_call_args(sig, args, regular_param_count, false, true, &[]) -} - -/// Normalizes builtin call args with runtime checks for spread bounds. -pub(crate) fn normalize_builtin_call_args_with_checks( - sig: &FunctionSig, - args: &[Expr], -) -> NormalizedCallArgs { - normalize_call_args( - sig, - args, - regular_param_count(Some(sig), args.len()), - true, - false, - &[], - ) -} - -/// Preevaluates named call args to temps in source order before final materialization. -pub(crate) fn preevaluate_named_call_args_to_temps( - sig: &FunctionSig, - args: &[Expr], - call_span: Span, - regular_param_count: usize, - trim_trailing_defaults: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> NormalizedCallArgs { - let expanded_args = call_args::expand_static_assoc_spread_args(args); - let args = expanded_args.as_slice(); - - if !has_named_args(args) { - return normalize_call_args( - sig, - args, - regular_param_count, - trim_trailing_defaults, - false, - &[], - ); - } - - let rewritten = if args.iter().any(|arg| matches!(arg.kind, ExprKind::Spread(_))) { - preevaluate_named_spread_args_to_temps(sig, args, call_span, regular_param_count, emitter, ctx, data) - } else { - preevaluate_named_non_spread_args_to_temps( - sig, - args, - call_span, - regular_param_count, - emitter, - ctx, - data, - ) - }; - let assoc_spread_sources = assoc_spread_sources(&rewritten, ctx); - normalize_call_args( - sig, - &rewritten, - regular_param_count, - trim_trailing_defaults, - false, - &assoc_spread_sources, - ) -} - -/// Rewrites named call args that include a positional spread prefix. -/// The spread prefix is extracted to a temp variable, wrapped in a Spread node, -/// and placed before all named arguments in the result. Named values are preevaluated -/// to temps if needed to preserve side effects ordering. -fn preevaluate_named_spread_args_to_temps( - sig: &FunctionSig, - args: &[Expr], - call_span: Span, - regular_param_count: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Vec { - let first_named_pos = args - .iter() - .position(|arg| matches!(arg.kind, ExprKind::NamedArg { .. })) - .unwrap_or(args.len()); - let prefix_args = args[..first_named_pos].to_vec(); - let prefix_span = prefix_args - .first() - .map(|arg| arg.span) - .unwrap_or(call_span); - let prefix_name = named_call_prefix_temp_name(call_span); - let prefix_expr = single_spread_inner(&prefix_args) - .unwrap_or_else(|| Expr::new(ExprKind::ArrayLiteral(prefix_args), prefix_span)); - crate::codegen::stmt::emit_assign_stmt(&prefix_name, &prefix_expr, emitter, ctx, data); - - let mut rewritten = vec![Expr::new( - ExprKind::Spread(Box::new(Expr::new( - ExprKind::Variable(prefix_name), - prefix_span, - ))), - prefix_span, - )]; - - for (idx, arg) in args.iter().enumerate().skip(first_named_pos) { - if let ExprKind::NamedArg { name, value } = &arg.kind { - let rewritten_value = - preevaluate_named_value_if_needed(sig, regular_param_count, call_span, idx, name, value, emitter, ctx, data); - rewritten.push(Expr::new( - ExprKind::NamedArg { - name: name.clone(), - value: Box::new(rewritten_value), - }, - arg.span, - )); - } - } - - rewritten -} - -/// Rewrites named call args with no spread operators. -/// Non-named positional args that are side-effect-free literals are kept as-is; -/// others are assigned to temps. Named arguments are preevaluated to temps if needed -/// to preserve source-order evaluation before materialization. -fn preevaluate_named_non_spread_args_to_temps( - sig: &FunctionSig, - args: &[Expr], - call_span: Span, - regular_param_count: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Vec { - let mut rewritten = Vec::new(); - let mut positional_idx = 0usize; - - for (idx, arg) in args.iter().enumerate() { - match &arg.kind { - ExprKind::NamedArg { name, value } => { - let rewritten_value = - preevaluate_named_value_if_needed(sig, regular_param_count, call_span, idx, name, value, emitter, ctx, data); - rewritten.push(Expr::new( - ExprKind::NamedArg { - name: name.clone(), - value: Box::new(rewritten_value), - }, - arg.span, - )); - } - _ => { - let is_ref = sig - .ref_params - .get(positional_idx) - .copied() - .unwrap_or(false); - if is_ref || is_side_effect_free_literal(arg) { - rewritten.push(arg.clone()); - } else { - let temp_name = named_call_arg_temp_name(call_span, idx); - crate::codegen::stmt::emit_assign_stmt(&temp_name, arg, emitter, ctx, data); - rewritten.push(Expr::new(ExprKind::Variable(temp_name), arg.span)); - } - positional_idx += 1; - } - } - } - - rewritten -} - -/// Possibly preevaluates a named argument value to a temp. -/// Returns a clone if the parameter is by-reference or the value is a side-effect-free -/// literal; otherwise emits an assignment to a unique temp and returns a Variable -/// referencing that temp. This ensures named arg side effects are observable at the -/// PHP-expected point and not skipped by early ABI checks. -#[allow(clippy::too_many_arguments)] -fn preevaluate_named_value_if_needed( - sig: &FunctionSig, - regular_param_count: usize, - call_span: Span, - arg_idx: usize, - name: &str, - value: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Expr { - let is_ref = call_args::named_param_index(sig, regular_param_count, name) - .and_then(|param_idx| sig.ref_params.get(param_idx)) - .copied() - .unwrap_or(false); - if is_ref || is_side_effect_free_literal(value) { - return value.clone(); - } - - let temp_name = named_call_arg_temp_name(call_span, arg_idx); - crate::codegen::stmt::emit_assign_stmt(&temp_name, value, emitter, ctx, data); - Expr::new(ExprKind::Variable(temp_name), value.span) -} - -/// Returns the inner expression if the slice is a single Spread node. -fn single_spread_inner(prefix_args: &[Expr]) -> Option { - if let [arg] = prefix_args { - if let ExprKind::Spread(inner) = &arg.kind { - return Some((**inner).clone()); - } - } - None -} - -/// Returns true if the expression is a literal with no side effects. -/// Used to avoid unnecessary temp allocation for simple literals. -fn is_side_effect_free_literal(expr: &Expr) -> bool { - matches!( - expr.kind, - ExprKind::StringLiteral(_) - | ExprKind::IntLiteral(_) - | ExprKind::FloatLiteral(_) - | ExprKind::BoolLiteral(_) - | ExprKind::Null - ) -} - -/// Core normalization using the shared CallArgs planner. -/// Produces NormalizedCallArgs with normalized arg order and spread bounds checks -/// based on the provided signature, param count, and association spread sources. -fn normalize_call_args( - sig: &FunctionSig, - args: &[Expr], - regular_param_count: usize, - trim_trailing_defaults: bool, - allow_unknown_named_variadic: bool, - assoc_spread_sources: &[bool], -) -> NormalizedCallArgs { - let plan = call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( - sig, - args, - Span::dummy(), - regular_param_count, - trim_trailing_defaults, - allow_unknown_named_variadic, - assoc_spread_sources, - ) - .expect("codegen received invalid call arguments after type checking"); - NormalizedCallArgs { - args: plan.normalized_args(), - spread_length_checks: plan.spread_bounds_checks, - } -} - -/// Returns a vec of booleans indicating which spread args resolve to AssocArray. -/// Used to skip runtime length checks for static array spreads where the length -/// is known at compile time, avoiding unnecessary runtime overhead. -fn assoc_spread_sources(args: &[Expr], ctx: &Context) -> Vec { - call_args::expand_static_assoc_spread_args(args) - .iter() - .map(|arg| match &arg.kind { - ExprKind::Spread(inner) => matches!( - crate::codegen::functions::infer_contextual_type(inner, ctx), - PhpType::AssocArray { .. } - ), - _ => false, - }) - .collect() -} - -/// Prepares positional call args for ABI materialization (no named arguments). -pub(crate) fn prepare_call_args( - sig: Option<&FunctionSig>, - args_exprs: &[Expr], - regular_param_count: usize, -) -> PreparedCallArgs { - debug_assert!(sig.is_none() || !has_named_args(args_exprs)); - - let is_variadic = sig.map(|s| s.variadic.is_some()).unwrap_or(false); - - let mut regular_args = Vec::new(); - let mut variadic_args = Vec::new(); - let mut spread_segments = Vec::new(); - let mut first_spread_span = None; - let mut spread_at_index = 0usize; - - for (idx, arg) in args_exprs.iter().enumerate() { - if let ExprKind::Spread(inner) = &arg.kind { - if spread_segments.is_empty() { - spread_at_index = regular_args.len(); - first_spread_span = Some(arg.span); - } - spread_segments.push(Expr::new( - ExprKind::Spread(Box::new((**inner).clone())), - arg.span, - )); - } else if is_variadic && idx >= regular_param_count { - variadic_args.push(arg.clone()); - } else { - regular_args.push(arg.clone()); - } - } - - let spread_arg = match spread_segments.len() { - 0 => None, - 1 => match spread_segments.pop().map(|segment| segment.kind) { - Some(ExprKind::Spread(inner)) => Some(*inner), - _ => unreachable!("spread segment must keep its spread wrapper"), - }, - _ => Some(Expr::new( - ExprKind::ArrayLiteral(spread_segments), - first_spread_span.unwrap_or_else(Span::dummy), - )), - }; - - let spread_into_named = spread_arg.is_some() && spread_at_index < regular_param_count; - let mut all_args = regular_args; - if !spread_into_named { - if let Some(sig) = sig { - for idx in all_args.len()..regular_param_count { - if let Some(Some(default)) = sig.defaults.get(idx) { - all_args.push(default.clone()); - } - } - } - } - - PreparedCallArgs { - all_args, - variadic_args, - spread_arg, - spread_at_index, - regular_param_count, - is_variadic, - spread_into_named, - } -} diff --git a/src/codegen/expr/calls/args/spread.rs b/src/codegen/expr/calls/args/spread.rs deleted file mode 100644 index 868084ce60..0000000000 --- a/src/codegen/expr/calls/args/spread.rs +++ /dev/null @@ -1,482 +0,0 @@ -//! Purpose: -//! Lowers positional and named spread argument expansion. -//! Converts evaluated PHP argument expressions into temporary values ready for ABI assignment. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args` -//! -//! Key details: -//! - Argument checks must happen at PHP-observable points without skipping later side effects. - -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, context::Context, data_section::DataSection, functions}; -use crate::parser::ast::Expr; -use crate::types::{FunctionSig, PhpType}; - -use super::array_elements::{ - array_element_stride, emit_hash_lookup_for_param_or_index, load_array_element_to_result, - push_loaded_array_element_arg, push_loaded_hash_value_arg, spread_source_elem_ty, -}; -use super::common::{declared_target_ty, push_expr_arg}; -use super::variadic::variadic_container_elem_ty; - -/// Emits code that unpacks a spread array's elements into the remaining named parameter slots. -/// For positional (non-assoc) spreads, emits a length check before accessing elements to ensure -/// required parameters are covered. For assoc spreads, performs key-based lookups against -/// parameter names. Each element is pushed as an ABI-ready argument and its type appended to `arg_types`. -/// Returns early with no emitted code if `remaining == 0`. -pub(crate) fn emit_spread_into_named_params( - spread_expr: &Expr, - sig: Option<&FunctionSig>, - spread_at_index: usize, - regular_param_count: usize, - context_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - arg_types: &mut Vec, -) { - let remaining = regular_param_count.saturating_sub(spread_at_index); - if remaining == 0 { - return; - } - - emitter.comment(&format!("unpack spread into {} {}", remaining, context_label)); - let spread_ty = functions::infer_contextual_type(spread_expr, ctx); - let source_elem_ty = spread_source_elem_ty(&spread_ty); - let elem_stride = array_element_stride(&source_elem_ty); - let _ = super::super::super::emit_expr(spread_expr, emitter, ctx, data); - let array_base_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "x20", - crate::codegen::platform::Arch::X86_64 => "r12", - }; - emitter.instruction(&format!("mov {}, {}", array_base_reg, abi::int_result_reg(emitter))); // preserve the spread array pointer across boxing or incref helper calls - let min_required = (0..remaining) - .filter(|idx| { - sig.and_then(|sig| sig.defaults.get(spread_at_index + idx)) - .and_then(|default| default.as_ref()) - .is_none() - }) - .map(|idx| idx + 1) - .max() - .unwrap_or(0); - if min_required > 0 { - emit_spread_required_length_check(array_base_reg, min_required, emitter, ctx, data); - } - for idx in 0..remaining { - let target_ty = declared_target_ty(sig, spread_at_index + idx); - let default = sig - .and_then(|sig| sig.defaults.get(spread_at_index + idx)) - .and_then(|default| default.as_ref()); - let pushed_ty = if matches!(spread_ty, PhpType::AssocArray { .. }) { - let param_name = sig - .and_then(|sig| sig.params.get(spread_at_index + idx)) - .map(|(name, _)| name.as_str()); - push_assoc_spread_element_or_default_arg( - array_base_reg, - param_name, - idx, - &source_elem_ty, - default, - target_ty, - emitter, - ctx, - data, - ) - } else { - push_spread_element_or_default_arg( - array_base_reg, - idx, - elem_stride, - &source_elem_ty, - default, - target_ty, - emitter, - ctx, - data, - ) - }; - arg_types.push(pushed_ty); - } -} - -/// Generates a bounds check that aborts if the spread array contains fewer than `min_len` elements. -/// Loads the spread array length from `array_base_reg`, compares against `min_len`, and branches to -/// `emit_spread_too_few_args_abort` on failure. On success falls through to the next label. -fn emit_spread_required_length_check( - array_base_reg: &str, - min_len: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let ok_label = ctx.next_label("spread_required_len_ok"); - let fail_label = ctx.next_label("spread_required_len_fail"); - emitter.comment("validate spread covers required parameters"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("ldr x9, [{}]", array_base_reg)); // load spread length before reading required unpacked parameters - abi::emit_load_int_immediate(emitter, "x10", min_len as i64); - emitter.instruction("cmp x9, x10"); // ensure the spread provides every required positional parameter - emitter.instruction(&format!("b.ge {}", ok_label)); // continue when all required spread slots are available - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov r10, QWORD PTR [{}]", array_base_reg)); // load spread length before reading required unpacked parameters - abi::emit_load_int_immediate(emitter, "r11", min_len as i64); - emitter.instruction("cmp r10, r11"); // ensure the spread provides every required positional parameter - emitter.instruction(&format!("jge {}", ok_label)); // continue when all required spread slots are available - } - } - emitter.label(&fail_label); - emit_spread_too_few_args_abort(emitter, data); - emitter.label(&ok_label); -} - -/// Emits a fatal runtime abort with a "too few arguments" diagnostic message. -/// Writes a fixed string to stderr and exits with code 1. Used when a spread provides -/// insufficient elements for required parameters. -fn emit_spread_too_few_args_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = - data.add_string(b"Fatal error: too few arguments for spread call\n"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the spread arity diagnostic to stderr - abi::emit_symbol_address(emitter, "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); - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the spread arity 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 spread arity diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Emits code to push either a spread element at `element_idx` or a default expression to the ABI. -/// For non-assoc (positional) spread arrays. Reads the element from the spread array at offset -/// `24 + element_idx * elem_stride` (skipping the array header). If `default` is present and the -/// spread is too short, jumps to the default path. Returns the widnened PHP type of the pushed argument. -#[allow(clippy::too_many_arguments)] -fn push_spread_element_or_default_arg( - array_base_reg: &str, - element_idx: usize, - elem_stride: usize, - source_elem_ty: &PhpType, - default: Option<&Expr>, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if let Some(default) = default { - let use_default = ctx.next_label("spread_default"); - let done = ctx.next_label("spread_done"); - emit_branch_if_spread_element_missing(array_base_reg, element_idx, &use_default, emitter); - load_array_element_to_result( - emitter, - source_elem_ty, - array_base_reg, - 24 + element_idx * elem_stride, - ); - let loaded_ty = - push_loaded_array_element_arg(source_elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&use_default); - let default_ty = push_expr_arg(default, target_ty, emitter, ctx, data); - emitter.label(&done); - return super::super::super::widen_codegen_type(&loaded_ty, &default_ty); - } - - load_array_element_to_result( - emitter, - source_elem_ty, - array_base_reg, - 24 + element_idx * elem_stride, - ); - push_loaded_array_element_arg(source_elem_ty, target_ty, emitter, ctx, data) -} - -/// Emits code to push either an associative spread element matching `param_name` or a default expression. -/// Performs a hash lookup for `param_name` in the spread array. If found, pushes the loaded value; -/// if not found and a default exists, pushes the default expression. If no default and the key is -/// missing, aborts with a fatal error. Returns the widnened PHP type of the pushed argument. -#[allow(clippy::too_many_arguments)] -fn push_assoc_spread_element_or_default_arg( - hash_base_reg: &str, - param_name: Option<&str>, - element_idx: usize, - source_elem_ty: &PhpType, - default: Option<&Expr>, - target_ty: Option<&PhpType>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("lookup associative spread argument"); - emit_hash_lookup_for_param_or_index( - hash_base_reg, - param_name, - element_idx, - emitter, - ctx, - data, - ); - - if let Some(default) = default { - let use_default = ctx.next_label("assoc_spread_default"); - let done = ctx.next_label("assoc_spread_done"); - abi::emit_branch_if_int_result_zero(emitter, &use_default); - let loaded_ty = push_loaded_hash_value_arg(source_elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&use_default); - let default_ty = push_expr_arg(default, target_ty, emitter, ctx, data); - emitter.label(&done); - return super::super::super::widen_codegen_type(&loaded_ty, &default_ty); - } - - let missing = ctx.next_label("assoc_spread_missing"); - let done = ctx.next_label("assoc_spread_done"); - abi::emit_branch_if_int_result_zero(emitter, &missing); - let loaded_ty = push_loaded_hash_value_arg(source_elem_ty, target_ty, emitter, ctx, data); - abi::emit_jump(emitter, &done); - emitter.label(&missing); - emit_spread_too_few_args_abort(emitter, data); - emitter.label(&done); - loaded_ty -} - -/// Emits a conditional branch to `label` if the spread array has fewer than `element_idx + 1` elements. -/// Loads the spread array length from `array_base_reg` and compares against `element_idx`. -/// On AArch64 uses `x9`/`x10`; on x86_64 uses `r10`/`r11`. Branches to `label` when the spread -/// is too short to contain this element (i.e., the element is missing and a default should be used). -fn emit_branch_if_spread_element_missing( - array_base_reg: &str, - element_idx: usize, - label: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("ldr x9, [{}]", array_base_reg)); // load spread length before choosing spread element or default - abi::emit_load_int_immediate(emitter, "x10", element_idx as i64); - emitter.instruction("cmp x9, x10"); // check whether this optional spread element exists - emitter.instruction(&format!("b.le {}", label)); // use the default when the spread is too short for this slot - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov r10, QWORD PTR [{}]", array_base_reg)); // load spread length before choosing spread element or default - abi::emit_load_int_immediate(emitter, "r11", element_idx as i64); - emitter.instruction("cmp r10, r11"); // check whether this optional spread element exists - emitter.instruction(&format!("jle {}", label)); // use the default when the spread is too short for this slot - } - } -} - -/// Emits a variadic array from the tail of a spread expression starting at a given offset. -pub(crate) fn emit_spread_tail_variadic_array_arg( - spread_expr: &Expr, - sig: Option<&FunctionSig>, - tail_start: usize, - regular_param_count: usize, - context_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(context_label); - let spread_ty = super::super::super::emit_expr(spread_expr, emitter, ctx, data); - if matches!(spread_ty.codegen_repr(), PhpType::Iterable) { - return emit_iterable_spread_tail_variadic_array_arg( - sig, - tail_start, - regular_param_count, - emitter, - ctx, - data, - ); - } - if matches!(spread_ty, PhpType::AssocArray { .. }) { - return emit_assoc_spread_tail_variadic_array_arg( - &spread_ty, - sig, - tail_start, - regular_param_count, - emitter, - ctx, - data, - ); - } - emit_indexed_spread_tail_variadic_array_arg(&spread_ty, tail_start, emitter) -} - -/// Emits an indexed-array slice for a spread tail that is known to use indexed storage. -fn emit_indexed_spread_tail_variadic_array_arg( - spread_ty: &PhpType, - tail_start: usize, - emitter: &mut Emitter, -) -> PhpType { - let source_elem_ty = spread_source_elem_ty(spread_ty); - let container_elem_ty = variadic_container_elem_ty(&source_elem_ty); - if emitter.target.arch == crate::codegen::platform::Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // pass the spread source array pointer to the x86_64 slice helper - } - let offset_reg = abi::int_arg_reg_name(emitter.target, 1); - let length_reg = abi::int_arg_reg_name(emitter.target, 2); - abi::emit_load_int_immediate(emitter, offset_reg, tail_start as i64); - abi::emit_load_int_immediate(emitter, length_reg, -1); - let helper = if source_elem_ty.codegen_repr().is_refcounted() { - "__rt_array_slice_refcounted" - } else { - "__rt_array_slice" - }; - abi::emit_call_label(emitter, helper); - super::super::super::arrays::emit_array_value_type_stamp( - emitter, - abi::int_result_reg(emitter), - &container_elem_ty, - ); - abi::emit_push_result_value(emitter, &PhpType::Array(Box::new(container_elem_ty.clone()))); - PhpType::Array(Box::new(container_elem_ty)) -} - -/// Emits a keyed variadic hash for a spread tail that is known to use associative storage. -fn emit_assoc_spread_tail_variadic_array_arg( - spread_ty: &PhpType, - sig: Option<&FunctionSig>, - tail_start: usize, - regular_param_count: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let source_elem_ty = spread_source_elem_ty(spread_ty); - let source_hash_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "x20", - crate::codegen::platform::Arch::X86_64 => "r13", - }; - emitter.instruction(&format!("mov {}, {}", source_hash_reg, abi::int_result_reg(emitter))); // preserve the spread source hash while building the variadic tail - - let fallback_sig; - let effective_sig = if let Some(sig) = sig { - sig - } else { - fallback_sig = fallback_variadic_sig(); - &fallback_sig - }; - super::emit_loaded_assoc_variadic_array_arg( - source_hash_reg, - &source_elem_ty, - effective_sig, - tail_start, - regular_param_count, - "build associative spread variadic tail", - emitter, - ctx, - data, - ) -} - -/// Emits runtime dispatch for an `Iterable` spread tail, preserving indexed and hash layouts. -fn emit_iterable_spread_tail_variadic_array_arg( - sig: Option<&FunctionSig>, - tail_start: usize, - regular_param_count: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let indexed_case = ctx.next_label("spread_iterable_indexed"); - let hash_case = ctx.next_label("spread_iterable_hash"); - let done_label = ctx.next_label("spread_iterable_done"); - let source_hash_reg = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => "x20", - crate::codegen::platform::Arch::X86_64 => "r13", - }; - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve iterable spread pointer across heap-kind dispatch - abi::emit_call_label(emitter, "__rt_heap_kind"); // classify the iterable spread payload by runtime heap kind - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("cmp x0, #2"); // is the iterable spread backed by an indexed array? - emitter.instruction(&format!("b.eq {}", indexed_case)); // slice indexed iterables using the array-tail path - emitter.instruction("cmp x0, #3"); // is the iterable spread backed by an associative hash? - emitter.instruction(&format!("b.eq {}", hash_case)); // rebuild associative iterables as a keyed variadic hash - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("cmp rax, 2"); // is the iterable spread backed by an indexed array? - emitter.instruction(&format!("je {}", indexed_case)); // slice indexed iterables using the array-tail path - emitter.instruction("cmp rax, 3"); // is the iterable spread backed by an associative hash? - emitter.instruction(&format!("je {}", hash_case)); // rebuild associative iterables as a keyed variadic hash - } - } - abi::emit_call_label(emitter, "__rt_iterable_unsupported_kind"); // reject non-array iterable spread tails for call forwarding - - emitter.label(&indexed_case); - let array_arg_reg = abi::int_arg_reg_name(emitter.target, 0); - abi::emit_pop_reg(emitter, array_arg_reg); // restore indexed iterable pointer for slicing - emit_indexed_mixed_spread_tail_slice(tail_start, emitter); - let indexed_ty = PhpType::Array(Box::new(PhpType::Mixed)); - abi::emit_push_result_value(emitter, &indexed_ty); - abi::emit_jump(emitter, &done_label); - - emitter.label(&hash_case); - abi::emit_pop_reg(emitter, source_hash_reg); // restore associative iterable pointer for keyed tail construction - let fallback_sig; - let effective_sig = if let Some(sig) = sig { - sig - } else { - fallback_sig = fallback_variadic_sig(); - &fallback_sig - }; - super::emit_loaded_assoc_variadic_array_arg( - source_hash_reg, - &PhpType::Mixed, - effective_sig, - tail_start, - regular_param_count, - "build associative iterable variadic tail", - emitter, - ctx, - data, - ); - abi::emit_jump(emitter, &done_label); - - emitter.label(&done_label); - PhpType::Iterable -} - -/// Emits an indexed Mixed slice from an already-restored iterable array pointer. -fn emit_indexed_mixed_spread_tail_slice(tail_start: usize, emitter: &mut Emitter) { - let offset_reg = abi::int_arg_reg_name(emitter.target, 1); - let length_reg = abi::int_arg_reg_name(emitter.target, 2); - abi::emit_load_int_immediate(emitter, offset_reg, tail_start as i64); - abi::emit_load_int_immediate(emitter, length_reg, -1); - abi::emit_call_label(emitter, "__rt_array_slice_refcounted"); - super::super::super::arrays::emit_array_value_type_stamp( - emitter, - abi::int_result_reg(emitter), - &PhpType::Mixed, - ); -} - -/// Builds a permissive variadic signature for unreachable no-signature spread fallback paths. -fn fallback_variadic_sig() -> FunctionSig { - FunctionSig { - params: vec![( - "rest".to_string(), - PhpType::Array(Box::new(PhpType::Mixed)), - )], - defaults: vec![None], - return_type: PhpType::Mixed, - declared_return: false, - by_ref_return: false, - ref_params: vec![false], - declared_params: vec![false], - variadic: Some("rest".to_string()), - deprecation: None, - } -} diff --git a/src/codegen/expr/calls/args/spread_checks.rs b/src/codegen/expr/calls/args/spread_checks.rs deleted file mode 100644 index 76f4a0a20e..0000000000 --- a/src/codegen/expr/calls/args/spread_checks.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Purpose: -//! Lowers runtime checks for spread array lengths and required parameter bounds. -//! Converts evaluated PHP argument expressions into temporary values ready for ABI assignment. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args` -//! -//! Key details: -//! - Argument checks must happen at PHP-observable points without skipping later side effects. - -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, context::Context, data_section::DataSection}; -use crate::types::call_args::SpreadBoundsCheck; - -/// Iterates over `SpreadBoundsCheck` descriptors, emits the spread expression evaluation, -/// then emits a bounds check that branches to `fail_label` if the array length does not -/// cover all required positional slots, or to `ok_label` if it does. On failure, calls -/// `emit_named_spread_length_abort`. -pub(crate) fn emit_spread_length_checks( - checks: &[SpreadBoundsCheck], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - for check in checks { - let ok_label = ctx.next_label("named_spread_len_ok"); - let underflow_label = ctx.next_label("named_spread_len_underflow"); - let overflow_label = ctx.next_label("named_spread_len_overflow"); - emitter.comment("validate named-argument spread length"); - let _ = super::super::super::emit_expr(&check.spread_expr, emitter, ctx, data); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldr x9, [x0]"); // load the logical spread-array length before using synthetic positional reads - emit_array_length_bounds_check( - "x9", - check.min_len, - check.max_len, - &underflow_label, - &overflow_label, - &ok_label, - emitter, - ); - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rax]"); // load the logical spread-array length before using synthetic positional reads - emit_array_length_bounds_check( - "r10", - check.min_len, - check.max_len, - &underflow_label, - &overflow_label, - &ok_label, - emitter, - ); - } - } - emitter.label(&underflow_label); - emit_named_spread_length_abort(emitter, data); - emitter.label(&overflow_label); - if let Some(param_name) = check.max_len_param_name.as_deref() { - emit_named_spread_duplicate_abort(emitter, data, param_name); - } else { - emit_named_spread_length_abort(emitter, data); - } - emitter.label(&ok_label); - } -} - -/// Loads the integer constant `min_len` into a scratch register and compares it again `length_reg`. -/// Branches to `underflow_fail_label` if `length_reg < min_len`. If `max_len` is `Some`, -/// compares `length_reg` against it and branches to `overflow_fail_label` when the -/// spread would overwrite a later named argument; otherwise it branches to `ok_label`. -pub(super) fn emit_array_length_bounds_check( - length_reg: &str, - min_len: usize, - max_len: Option, - underflow_fail_label: &str, - overflow_fail_label: &str, - ok_label: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_load_int_immediate(emitter, "x10", min_len as i64); - emitter.instruction(&format!("cmp {}, x10", length_reg)); // ensure the array covers every required positional slot - emitter.instruction(&format!("b.lt {}", underflow_fail_label)); // report a missing required argument instead of reading past the payload - if let Some(max_len) = max_len { - abi::emit_load_int_immediate(emitter, "x10", max_len as i64); - emitter.instruction(&format!("cmp {}, x10", length_reg)); // ensure the array does not overwrite the next named slot - emitter.instruction(&format!("b.le {}", ok_label)); // continue when the array length is within the allowed bounds - emitter.instruction(&format!("b {}", overflow_fail_label)); // report the named parameter overwritten by the spread prefix - } else { - emitter.instruction(&format!("b {}", ok_label)); // variadic calls allow remaining spread values to flow into ...$rest - } - } - crate::codegen::platform::Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "r11", min_len as i64); - emitter.instruction(&format!("cmp {}, r11", length_reg)); // ensure the array covers every required positional slot - emitter.instruction(&format!("jl {}", underflow_fail_label)); // report a missing required argument instead of reading past the payload - if let Some(max_len) = max_len { - abi::emit_load_int_immediate(emitter, "r11", max_len as i64); - emitter.instruction(&format!("cmp {}, r11", length_reg)); // ensure the array does not overwrite the next named slot - emitter.instruction(&format!("jle {}", ok_label)); // continue when the array length is within the allowed bounds - emitter.instruction(&format!("jmp {}", overflow_fail_label)); // report the named parameter overwritten by the spread prefix - } else { - emitter.instruction(&format!("jmp {}", ok_label)); // variadic calls allow remaining spread values to flow into ...$rest - } - } - } -} - -/// Writes a fixed diagnostic message to stderr and then exits the process with code 1. -pub(super) fn emit_named_spread_length_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = - data.add_string(b"Fatal error: named argument spread length mismatch\n"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the named-argument spread diagnostic to stderr - abi::emit_symbol_address(emitter, "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); - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the named-argument spread 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 named-argument spread diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Writes the PHP-compatible duplicate named-parameter diagnostic and exits with code 1. -pub(super) fn emit_named_spread_duplicate_abort( - emitter: &mut Emitter, - data: &mut DataSection, - param_name: &str, -) { - let message = format!( - "Fatal error: Named parameter ${} overwrites previous argument\n", - param_name - ); - let (message_label, message_len) = data.add_string(message.as_bytes()); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the duplicate named-argument diagnostic to stderr - abi::emit_symbol_address(emitter, "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); - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the duplicate named-argument 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 duplicate named-argument diagnostic - abi::emit_exit(emitter, 1); - } - } -} diff --git a/src/codegen/expr/calls/args/variadic.rs b/src/codegen/expr/calls/args/variadic.rs deleted file mode 100644 index 5cd8ce54a6..0000000000 --- a/src/codegen/expr/calls/args/variadic.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! Purpose: -//! Lowers variadic array argument construction and storage. -//! Converts evaluated PHP argument expressions into temporary values ready for ABI assignment. -//! -//! Called from: -//! - `crate::codegen::expr::calls::args` -//! -//! Key details: -//! - Argument checks must happen at PHP-observable points without skipping later side effects. - -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, context::Context, data_section::DataSection, functions}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Stores the current result value (from ABI result registers) into a variadic array -/// element at the given index. -/// -/// Reads from `int_result_reg`, `float_result_reg`, or `string_result_regs` depending -/// on `elem_ty`. The element is stored at offset `24 + elem_idx * elem_size` to account -/// for the 24-byte array header. String elements occupy 16 bytes (ptr + len); all other -/// types occupy 8 bytes. Does nothing for `Void` type. -pub(super) fn store_current_array_element( - emitter: &mut Emitter, - array_reg: &str, - elem_idx: usize, - elem_ty: &PhpType, -) { - match elem_ty.codegen_repr() { - PhpType::Float => { - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), array_reg, 24 + elem_idx * 8); // store float element into the variadic array payload - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_store_to_address(emitter, ptr_reg, array_reg, 24 + elem_idx * 16); // store variadic string pointer into the array payload - abi::emit_store_to_address(emitter, len_reg, array_reg, 24 + elem_idx * 16 + 8); // store variadic string length next to the payload pointer - } - PhpType::Void => {} - _ => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), array_reg, 24 + elem_idx * 8); // store scalar or boxed variadic payload into the array data area - } - } -} - -/// Returns the element type for a variadic container, mapping `Iterable` to `Mixed`. -/// -/// Mixed is used as the container element type when the first argument is Iterable -/// because the array may contain heterogeneous values after spread unpacking. -pub(super) fn variadic_container_elem_ty(elem_ty: &PhpType) -> PhpType { - if matches!(elem_ty.codegen_repr(), PhpType::Iterable) { - PhpType::Mixed - } else { - elem_ty.clone() - } -} - -/// Emits an empty variadic array argument (capacity 4) for absent variadic params. -pub(crate) fn emit_empty_variadic_array_arg(context_label: &str, emitter: &mut Emitter) -> PhpType { - emitter.comment(context_label); - let (capacity_reg, elem_size_reg) = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => ("x0", "x1"), - crate::codegen::platform::Arch::X86_64 => ("rdi", "rsi"), - }; - abi::emit_load_int_immediate(emitter, capacity_reg, 4); - abi::emit_load_int_immediate(emitter, elem_size_reg, 8); - abi::emit_call_label(emitter, "__rt_array_new"); - abi::emit_push_result_value(emitter, &PhpType::Array(Box::new(PhpType::Int))); - PhpType::Array(Box::new(PhpType::Int)) -} - -/// Builds a variadic array from a list of evaluated expressions and pushes it as an argument. -pub(crate) fn emit_variadic_array_arg_from_exprs( - variadic_args: &[Expr], - context_label: &str, - retain_heap_values: bool, - stamp_value_type: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let elem_count = variadic_args.len(); - let first_elem_ty = functions::infer_contextual_type(&variadic_args[0], ctx); - let container_elem_ty = variadic_container_elem_ty(&first_elem_ty); - let elem_size = match container_elem_ty.codegen_repr() { - PhpType::Str => 16, - _ => 8, - }; - let (capacity_reg, elem_size_reg, peek_reg, len_reg) = match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => ("x0", "x1", "x9", "x10"), - crate::codegen::platform::Arch::X86_64 => ("rdi", "rsi", "r11", "r10"), - }; - - emitter.comment(&format!("{} ({} elements)", context_label, elem_count)); - abi::emit_load_int_immediate(emitter, capacity_reg, elem_count as i64); - abi::emit_load_int_immediate(emitter, elem_size_reg, elem_size as i64); - abi::emit_call_label(emitter, "__rt_array_new"); - abi::emit_push_result_value(emitter, &PhpType::Array(Box::new(container_elem_ty.clone()))); - - for (idx, variadic_arg) in variadic_args.iter().enumerate() { - let mut elem_ty = super::super::super::emit_expr(variadic_arg, emitter, ctx, data); - let boxed_for_container = if matches!(container_elem_ty, PhpType::Mixed) - && !matches!(elem_ty, PhpType::Mixed | PhpType::Union(_)) - { - crate::codegen::emit_box_current_value_as_mixed(emitter, &elem_ty); - elem_ty = PhpType::Mixed; - true - } else { - false - }; - if retain_heap_values && !boxed_for_container { - super::super::super::retain_borrowed_heap_arg(emitter, variadic_arg, &elem_ty); - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("ldr {}, [sp]", peek_reg)); // peek the variadic array pointer without removing it from the stack - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, QWORD PTR [rsp]", peek_reg)); // peek the variadic array pointer without removing it from the stack - } - } - if stamp_value_type && idx == 0 { - super::super::super::arrays::emit_array_value_type_stamp(emitter, peek_reg, &elem_ty); - } - store_current_array_element(emitter, peek_reg, idx, &elem_ty); - abi::emit_load_int_immediate(emitter, len_reg, (idx + 1) as i64); - abi::emit_store_to_address(emitter, len_reg, peek_reg, 0); - } - - PhpType::Array(Box::new(container_elem_ty)) -} diff --git a/src/codegen/expr/calls/callable_array_runtime.rs b/src/codegen/expr/calls/callable_array_runtime.rs deleted file mode 100644 index b4d90c9bdf..0000000000 --- a/src/codegen/expr/calls/callable_array_runtime.rs +++ /dev/null @@ -1,761 +0,0 @@ -//! Purpose: -//! Selects callable-array descriptors at runtime when receiver or method slots are not static literals. -//! Keeps dynamic `[$object, $method]` and `[$class, $method]` direct-call logic out of the call dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::calls::emit_runtime_callable_array_call()` -//! - `crate::codegen::expr::calls::emit_callable_array_literal_call()` -//! - `crate::codegen::expr::calls::emit_callable_array_variable_call()` -//! -//! Key details: -//! - Selector slots are read before user arguments so runtime method resolution observes the callable value first. -//! - Matched cases invoke the same descriptor invoker path as static callable-array calls. - -use crate::codegen::builtins::arrays::receiver_call_args; -use crate::codegen::callable_dispatch::{ - RuntimeCallableCase, RuntimeInstanceMethodCallableCase, RuntimeStaticMethodCallableCase, -}; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::abi; -use crate::codegen::platform::Arch; -use crate::parser::ast::{Expr, ExprKind}; -use crate::span::Span; -use crate::types::PhpType; - -const MIXED_METHOD_TAG_OFFSET: usize = 0; -const MIXED_METHOD_PAYLOAD_OFFSET: usize = 16; -const MIXED_RECEIVER_TAG_OFFSET: usize = 32; -const MIXED_RECEIVER_PAYLOAD_OFFSET: usize = 48; -const MIXED_SELECTOR_BYTES: usize = 64; -const STRING_METHOD_OFFSET: usize = 0; -const STRING_CLASS_OFFSET: usize = 16; -const STRING_SELECTOR_BYTES: usize = 32; - -/// Emits a descriptor invocation for callable-array variables whose receiver or method is runtime-selected. -pub(super) fn emit_variable_call( - var: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let var_ty = ctx.variables.get(var)?.ty.codegen_repr(); - match var_ty { - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Mixed) => { - emit_mixed_variable_call(var, args, emitter, ctx, data); - Some(PhpType::Mixed) - } - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Str) => { - emit_string_variable_call(var, args, emitter, ctx, data); - Some(PhpType::Mixed) - } - _ => None, - } -} - -/// Emits a descriptor invocation for callable-array literals whose slots are runtime-selected. -pub(super) fn emit_literal_call( - callee: &Expr, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - if !is_two_slot_callable_array_literal(callee) { - return None; - } - let callee_ty = crate::codegen::functions::infer_contextual_type(callee, ctx).codegen_repr(); - match callee_ty { - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Mixed) => { - emit_mixed_literal_call(callee, args, emitter, ctx, data); - Some(PhpType::Mixed) - } - PhpType::Array(elem_ty) if matches!(elem_ty.codegen_repr(), PhpType::Str) => { - emit_string_literal_call(callee, args, emitter, ctx, data); - Some(PhpType::Mixed) - } - _ => None, - } -} - -/// Emits runtime descriptor selection for heterogeneous callable arrays such as `[$object, $method]`. -fn emit_mixed_variable_call( - var: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let instance_cases = - crate::codegen::callable_dispatch::runtime_public_instance_method_cases(ctx, data); - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - emit_mixed_selector_slots(var, emitter, ctx, data); - emit_mixed_dispatch( - var, - args, - &instance_cases, - &static_cases, - emitter, - ctx, - data, - ); - abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); -} - -/// Emits runtime descriptor selection for string callable arrays such as `[$class, $method]`. -fn emit_string_variable_call( - var: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - emit_string_selector_slots(var, emitter, ctx, data); - emit_string_dispatch(args, &static_cases, emitter, ctx, data); - abi::emit_release_temporary_stack(emitter, STRING_SELECTOR_BYTES); -} - -/// Emits runtime descriptor selection for heterogeneous callable-array literals. -fn emit_mixed_literal_call( - callee: &Expr, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let instance_cases = - crate::codegen::callable_dispatch::runtime_public_instance_method_cases(ctx, data); - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - super::super::emit_expr(callee, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the evaluated runtime callable-array literal during descriptor selection - emit_mixed_literal_selector_slots(emitter); - emit_mixed_literal_dispatch(args, &instance_cases, &static_cases, emitter, ctx, data); - abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); // discard literal callable-array selector slots after invocation - release_preserved_literal_array_after_mixed_result( - &PhpType::Array(Box::new(PhpType::Mixed)), - emitter, - ); -} - -/// Emits runtime descriptor selection for string callable-array literals. -fn emit_string_literal_call( - callee: &Expr, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let static_cases = - crate::codegen::callable_dispatch::runtime_public_static_method_cases(ctx, data); - super::super::emit_expr(callee, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the evaluated runtime string callable-array literal during descriptor selection - emit_string_literal_selector_slots(emitter); - emit_string_dispatch(args, &static_cases, emitter, ctx, data); - abi::emit_release_temporary_stack(emitter, STRING_SELECTOR_BYTES); // discard literal callable-array selector slots after invocation - release_preserved_literal_array_after_mixed_result( - &PhpType::Array(Box::new(PhpType::Str)), - emitter, - ); -} - -/// Saves the unboxed receiver and method slots for a runtime heterogeneous callable-array dispatch. -fn emit_mixed_selector_slots( - var: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("runtime callable-array mixed selector"); - let receiver = callable_array_slot_expr(var, 0); - super::super::emit_expr(&receiver, emitter, ctx, data); - emit_unbox_mixed_result(emitter); - emit_push_mixed_unbox_payload(emitter); - - let method = callable_array_slot_expr(var, 1); - super::super::emit_expr(&method, emitter, ctx, data); - emit_unbox_mixed_result(emitter); - emit_push_mixed_unbox_payload(emitter); -} - -/// Saves class and method string slots for a runtime string callable-array dispatch. -fn emit_string_selector_slots( - var: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("runtime callable-array string selector"); - let class = callable_array_slot_expr(var, 0); - super::super::emit_expr(&class, emitter, ctx, data); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime class string while the method slot is read - - let method = callable_array_slot_expr(var, 1); - super::super::emit_expr(&method, emitter, ctx, data); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the runtime method string for descriptor-case selection -} - -/// Saves selector slots read from an already-evaluated mixed callable-array literal. -fn emit_mixed_literal_selector_slots(emitter: &mut Emitter) { - emitter.comment("runtime callable-array literal mixed selector"); - emit_unbox_mixed_literal_slot(0, 0, emitter); - emit_push_mixed_unbox_payload(emitter); - emit_unbox_mixed_literal_slot(32, 1, emitter); - emit_push_mixed_unbox_payload(emitter); -} - -/// Saves selector slots read from an already-evaluated string callable-array literal. -fn emit_string_literal_selector_slots(emitter: &mut Emitter) { - emitter.comment("runtime callable-array literal string selector"); - emit_push_string_literal_slot(0, 0, emitter); - emit_push_string_literal_slot(16, 1, emitter); -} - -/// Loads and unboxes one boxed Mixed slot from a preserved callable-array literal. -fn emit_unbox_mixed_literal_slot(array_stack_offset: usize, slot: usize, emitter: &mut Emitter) { - let array_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, array_reg, array_stack_offset); - abi::emit_load_from_address( - emitter, - abi::int_result_reg(emitter), - array_reg, - 24 + slot * 8, - ); - emit_unbox_mixed_result(emitter); -} - -/// Loads and saves one string slot from a preserved callable-array literal. -fn emit_push_string_literal_slot(array_stack_offset: usize, slot: usize, emitter: &mut Emitter) { - let array_reg = abi::symbol_scratch_reg(emitter); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, array_reg, array_stack_offset); - abi::emit_load_from_address(emitter, ptr_reg, array_reg, 24 + slot * 16); - abi::emit_load_from_address(emitter, len_reg, array_reg, 24 + slot * 16 + 8); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the literal callable-array string slot for descriptor-case selection -} - -/// Unboxes the current Mixed result into the target-specific tag and payload registers. -fn emit_unbox_mixed_result(emitter: &mut Emitter) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); -} - -/// Pushes the tag and payload returned by `__rt_mixed_unbox` onto the temporary stack. -fn emit_push_mixed_unbox_payload(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg_pair(emitter, "x1", "x2"); // preserve the unboxed Mixed payload words for runtime callable selection - abi::emit_push_reg(emitter, "x0"); // preserve the unboxed Mixed tag beside its payload words - } - Arch::X86_64 => { - abi::emit_push_reg_pair(emitter, "rdi", "rdx"); // preserve the unboxed Mixed payload words for runtime callable selection - abi::emit_push_reg(emitter, "rax"); // preserve the unboxed Mixed tag beside its payload words - } - } -} - -/// Dispatches a heterogeneous callable array to a descriptor selected from runtime receiver/method data. -#[allow(clippy::too_many_arguments)] -fn emit_mixed_dispatch( - var: &str, - args: &[Expr], - instance_cases: &[RuntimeInstanceMethodCallableCase], - static_cases: &[RuntimeStaticMethodCallableCase], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let done_label = ctx.next_label("callable_array_runtime_done"); - for case in instance_cases { - let next_case = ctx.next_label("callable_array_instance_next"); - emit_branch_if_mixed_instance_case_mismatch(case, &next_case, emitter, ctx, data); - emit_instance_case_call(var, args, &case.case, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - for case in static_cases { - let next_case = ctx.next_label("callable_array_static_next"); - emit_branch_if_mixed_static_case_mismatch(case, &next_case, emitter, ctx, data); - emit_static_case_call(args, &case.case, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - emit_no_match_abort(emitter, data); - emitter.label(&done_label); -} - -/// Dispatches a heterogeneous callable-array literal to a descriptor selected at runtime. -fn emit_mixed_literal_dispatch( - args: &[Expr], - instance_cases: &[RuntimeInstanceMethodCallableCase], - static_cases: &[RuntimeStaticMethodCallableCase], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let done_label = ctx.next_label("callable_array_runtime_done"); - for case in instance_cases { - let next_case = ctx.next_label("callable_array_instance_next"); - emit_branch_if_mixed_instance_case_mismatch(case, &next_case, emitter, ctx, data); - emit_instance_literal_case_call(args, &case.case, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - for case in static_cases { - let next_case = ctx.next_label("callable_array_static_next"); - emit_branch_if_mixed_static_case_mismatch(case, &next_case, emitter, ctx, data); - emit_static_case_call(args, &case.case, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - emit_no_match_abort(emitter, data); - emitter.label(&done_label); -} - -/// Dispatches a string callable array to a static-method descriptor selected from runtime strings. -fn emit_string_dispatch( - args: &[Expr], - static_cases: &[RuntimeStaticMethodCallableCase], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let done_label = ctx.next_label("callable_array_runtime_done"); - for case in static_cases { - let next_case = ctx.next_label("callable_array_static_next"); - emit_branch_if_string_static_case_mismatch(case, &next_case, emitter, ctx, data); - emit_static_case_call(args, &case.case, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); - emitter.label(&next_case); - } - emit_no_match_abort(emitter, data); - emitter.label(&done_label); -} - -/// Emits the descriptor call for one selected runtime instance-method callable-array case. -fn emit_instance_case_call( - var: &str, - args: &[Expr], - case: &RuntimeCallableCase, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let receiver = callable_array_slot_expr(var, 0); - let mut descriptor_args = Vec::with_capacity(args.len() + 1); - descriptor_args.push(receiver); - descriptor_args.extend(args.iter().cloned()); - let _ = super::emit_callable_array_descriptor_case_call( - &case.descriptor_label, - &case.sig, - &descriptor_args, - emitter, - ctx, - data, - ); -} - -/// Emits the descriptor call for a runtime literal instance-method callable-array case. -fn emit_instance_literal_case_call( - args: &[Expr], - case: &RuntimeCallableCase, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("call runtime literal callable-array descriptor"); - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - let concat_save_stack_bytes = if save_concat_before_args { - concat_save_stack_bytes(emitter, ctx) - } else { - 0 - }; - let object_stack_offset = - MIXED_RECEIVER_PAYLOAD_OFFSET + concat_save_stack_bytes; - let arr_ty = if let Some(arg_array) = single_spread_inner(args) { - let arg_array_ty = crate::codegen::functions::infer_contextual_type(arg_array, ctx); - let emitted_saved_args = receiver_call_args::emit_saved_receiver_prefixed_dynamic_arg_mixed( - object_stack_offset, - arg_array, - &arg_array_ty, - emitter, - ctx, - data, - ); - if emitted_saved_args { - PhpType::Mixed - } else { - super::descriptor_invoker_args::emit_descriptor_invoker_arg_array_with_saved_object_prefix( - object_stack_offset, - args, - Some(&case.sig), - Span::dummy(), - emitter, - ctx, - data, - ) - } - } else { - super::descriptor_invoker_args::emit_descriptor_invoker_arg_array_with_saved_object_prefix( - object_stack_offset, - args, - Some(&case.sig), - Span::dummy(), - emitter, - ctx, - data, - ) - }; - let call_reg = abi::nested_call_reg(emitter); - abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); - crate::codegen::builtins::arrays::call_user_func_array::emit_call_descriptor_array_invoker( - crate::codegen::builtins::arrays::call_user_func_array::LoadedArraySource::Result, - &arr_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); -} - -/// Returns how many temporary stack bytes the concat-offset save added. -fn concat_save_stack_bytes(emitter: &Emitter, ctx: &Context) -> usize { - match emitter.target.arch { - Arch::AArch64 => 16, - Arch::X86_64 if ctx.nested_concat_offset_offset.is_none() => 16, - Arch::X86_64 => 0, - } -} - -/// Returns the inner argument array when descriptor invocation forwards one spread segment. -fn single_spread_inner(args: &[Expr]) -> Option<&Expr> { - if let [arg] = args { - if let ExprKind::Spread(inner) = &arg.kind { - return Some(inner); - } - } - None -} - -/// Emits the descriptor call for one selected runtime static-method callable-array case. -fn emit_static_case_call( - args: &[Expr], - case: &RuntimeCallableCase, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let _ = super::emit_callable_array_descriptor_case_call( - &case.descriptor_label, - &case.sig, - args, - emitter, - ctx, - data, - ); -} - -/// Branches when the saved heterogeneous callable-array slots do not match an instance-method case. -fn emit_branch_if_mixed_instance_case_mismatch( - case: &RuntimeInstanceMethodCallableCase, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_branch_if_stack_tag_mismatch(MIXED_RECEIVER_TAG_OFFSET, 6, next_case, emitter); - emit_branch_if_stack_tag_mismatch(MIXED_METHOD_TAG_OFFSET, 1, next_case, emitter); - emit_branch_if_receiver_class_id_mismatch( - case.class_id, - MIXED_RECEIVER_PAYLOAD_OFFSET, - next_case, - emitter, - ); - emit_branch_if_stack_string_mismatch( - MIXED_METHOD_PAYLOAD_OFFSET, - MIXED_METHOD_PAYLOAD_OFFSET + 8, - case.method_name.as_bytes(), - next_case, - emitter, - ctx, - data, - ); -} - -/// Branches when the saved heterogeneous callable-array slots do not match a static-method case. -fn emit_branch_if_mixed_static_case_mismatch( - case: &RuntimeStaticMethodCallableCase, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_branch_if_stack_tag_mismatch(MIXED_RECEIVER_TAG_OFFSET, 1, next_case, emitter); - emit_branch_if_stack_tag_mismatch(MIXED_METHOD_TAG_OFFSET, 1, next_case, emitter); - emit_branch_if_static_class_string_mismatch( - MIXED_RECEIVER_PAYLOAD_OFFSET, - MIXED_RECEIVER_PAYLOAD_OFFSET + 8, - &case.class_name, - next_case, - emitter, - ctx, - data, - ); - emit_branch_if_stack_string_mismatch( - MIXED_METHOD_PAYLOAD_OFFSET, - MIXED_METHOD_PAYLOAD_OFFSET + 8, - case.method_name.as_bytes(), - next_case, - emitter, - ctx, - data, - ); -} - -/// Branches when the saved string callable-array slots do not match a static-method case. -fn emit_branch_if_string_static_case_mismatch( - case: &RuntimeStaticMethodCallableCase, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_branch_if_static_class_string_mismatch( - STRING_CLASS_OFFSET, - STRING_CLASS_OFFSET + 8, - &case.class_name, - next_case, - emitter, - ctx, - data, - ); - emit_branch_if_stack_string_mismatch( - STRING_METHOD_OFFSET, - STRING_METHOD_OFFSET + 8, - case.method_name.as_bytes(), - next_case, - emitter, - ctx, - data, - ); -} - -/// Branches when a saved Mixed tag stack slot does not equal `expected_tag`. -fn emit_branch_if_stack_tag_mismatch( - tag_offset: usize, - expected_tag: i64, - next_case: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x9", tag_offset); - emitter.instruction(&format!("cmp x9, #{}", expected_tag)); // compare the saved callable-array runtime tag against this descriptor shape - emitter.instruction(&format!("b.ne {}", next_case)); // try the next descriptor case when the callable-array slot shape differs - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "r10", tag_offset); - emitter.instruction(&format!("cmp r10, {}", expected_tag)); // compare the saved callable-array runtime tag against this descriptor shape - emitter.instruction(&format!("jne {}", next_case)); // try the next descriptor case when the callable-array slot shape differs - } - } -} - -/// Branches when the saved receiver object's class id does not match `class_id`. -fn emit_branch_if_receiver_class_id_mismatch( - class_id: u64, - receiver_offset: usize, - next_case: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x9", receiver_offset); - emitter.instruction(&format!("cbz x9, {}", next_case)); // 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 descriptor case - emitter.instruction(&format!("b.ne {}", next_case)); // try the next descriptor case when the receiver class differs - } - 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_case)); // try the next descriptor case 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 descriptor case - emitter.instruction(&format!("jne {}", next_case)); // try the next descriptor case when the receiver class differs - } - } -} - -/// Branches when a saved class string does not match either bare or leading-slash form. -#[allow(clippy::too_many_arguments)] -fn emit_branch_if_static_class_string_mismatch( - ptr_offset: usize, - len_offset: usize, - class_name: &str, - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let matched_label = ctx.next_label("callable_array_class_match"); - emit_stack_string_compare_branch( - ptr_offset, - len_offset, - class_name.as_bytes(), - &matched_label, - emitter, - data, - ); - let leading_slash = format!("\\{}", class_name); - emit_stack_string_compare_branch( - ptr_offset, - len_offset, - leading_slash.as_bytes(), - &matched_label, - emitter, - data, - ); - abi::emit_jump(emitter, next_case); - emitter.label(&matched_label); -} - -/// Branches when a saved stack string does not match the expected PHP name case-insensitively. -#[allow(clippy::too_many_arguments)] -fn emit_branch_if_stack_string_mismatch( - ptr_offset: usize, - len_offset: usize, - expected: &[u8], - next_case: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let matched_label = ctx.next_label("callable_array_string_match"); - emit_stack_string_compare_branch( - ptr_offset, - len_offset, - expected, - &matched_label, - emitter, - data, - ); - abi::emit_jump(emitter, next_case); - emitter.label(&matched_label); -} - -/// Compares a saved stack string with `expected` and branches to `matched_label` on equality. -fn emit_stack_string_compare_branch( - ptr_offset: usize, - len_offset: usize, - expected: &[u8], - matched_label: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (expected_label, expected_len) = data.add_string(expected); - match emitter.target.arch { - 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"); // did the callable-array runtime string match this descriptor name? - emitter.instruction(&format!("b.eq {}", matched_label)); // select this descriptor case when names match case-insensitively - } - 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"); // did the callable-array runtime string match this descriptor name? - emitter.instruction(&format!("je {}", matched_label)); // select this descriptor case when names match case-insensitively - } - } -} - -/// Releases the preserved callable-array literal while keeping the Mixed call result live. -fn release_preserved_literal_array_after_mixed_result(arr_ty: &PhpType, emitter: &mut Emitter) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed call result while releasing the temporary callable-array literal - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - abi::emit_decref_if_refcounted(emitter, arr_ty); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the boxed call result after callable-array literal cleanup - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callable-array literal slot -} - -/// Emits the fatal diagnostic for callable arrays that cannot be resolved to a descriptor. -fn emit_no_match_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: callable array did not resolve to an invokable target\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the callable-array diagnostic to stderr - abi::emit_symbol_address(emitter, "x1", &message_label); - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the callable-array diagnostic byte length to write() - emitter.syscall(4); - abi::emit_exit(emitter, 1); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the callable-array diagnostic to stderr - abi::emit_symbol_address(emitter, "rsi", &message_label); - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the callable-array diagnostic byte length to write() - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal callable-array diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Builds `$callback[$index]`, a positional slot stored inside a callable-array value. -fn callable_array_slot_expr(var: &str, index: i64) -> Expr { - Expr::new( - ExprKind::ArrayAccess { - array: Box::new(Expr::new(ExprKind::Variable(var.to_string()), Span::dummy())), - index: Box::new(Expr::new(ExprKind::IntLiteral(index), Span::dummy())), - }, - Span::dummy(), - ) -} - -/// Returns true when an expression is a two-element indexed-array literal. -fn is_two_slot_callable_array_literal(callee: &Expr) -> bool { - matches!(&callee.kind, ExprKind::ArrayLiteral(elems) if elems.len() == 2) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::codegen::platform::{Platform, Target}; - - /// Verifies x86_64 frame-slot concat saves do not shift saved callable-array selector offsets. - #[test] - fn test_concat_save_stack_bytes_tracks_actual_stack_pushes() { - let x86 = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); - let mut x86_frame_ctx = Context::new(); - x86_frame_ctx.nested_concat_offset_offset = Some(24); - assert_eq!(concat_save_stack_bytes(&x86, &x86_frame_ctx), 0); - - let x86_raw_ctx = Context::new(); - assert_eq!(concat_save_stack_bytes(&x86, &x86_raw_ctx), 16); - - let arm = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); - let arm_ctx = Context::new(); - assert_eq!(concat_save_stack_bytes(&arm, &arm_ctx), 16); - } -} diff --git a/src/codegen/expr/calls/closure.rs b/src/codegen/expr/calls/closure.rs deleted file mode 100644 index f6b9201898..0000000000 --- a/src/codegen/expr/calls/closure.rs +++ /dev/null @@ -1,679 +0,0 @@ -//! Purpose: -//! Lowers closure invocation expressions and captured environment handling. -//! Resolves the callable shape, prepares arguments, and leaves the call result for expression consumers. -//! -//! Called from: -//! - `crate::codegen::expr::calls` -//! -//! Key details: -//! - Callable metadata and argument signatures must stay synchronized with type checking and runtime dispatch. - -use crate::codegen::context::{Context, DeferredClosure}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::functions; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, Stmt, StmtKind, StaticReceiver, TypeExpr}; -use crate::span::Span; -use crate::types::{FunctionSig, PhpType}; - -use super::args; - -/// Infers the return type of a closure by collecting return statement types from its body -/// and widening them to a common supertype. Falls back to `Int` if no return statements exist. -/// Handles generators via `body_contains_yield`. -/// -/// - `body`: the closure body statements to inspect -/// - `sig`: preliminary function signature used to build local inference context -/// - `capture_types`: captured variables with their types and by-ref flags, used to allocate -/// fake local slots so captured vars are visible to local type inference -/// -/// Returns the inferred `PhpType`, or `PhpType::Object("Generator")` if yield is present. -fn infer_closure_return_type( - body: &[Stmt], - sig: &FunctionSig, - capture_types: &[(String, PhpType, bool)], -) -> PhpType { - if crate::types::checker::yield_validation::body_contains_yield(body) { - return PhpType::Object("Generator".to_string()); - } - - /// Recursively collects `PhpType` values from all return statements in `stmt`, - /// descending into if/while/do-while/for/foreach/try/switch blocks. - /// Uses `capture_ctx` to resolve local variable types for return expression inference. - fn collect_return_types( - stmt: &Stmt, - sig: &FunctionSig, - capture_ctx: &Context, - return_types: &mut Vec, - ) { - match &stmt.kind { - StmtKind::Return(Some(expr)) => { - return_types.push(crate::codegen::functions::infer_local_type_with_ctx( - expr, - sig, - capture_ctx, - )); - } - StmtKind::Return(None) => { - return_types.push(PhpType::Void); - } - StmtKind::If { - then_body, - elseif_clauses, - else_body, - .. - } => { - for stmt in then_body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - for (_, body) in elseif_clauses { - for stmt in body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - } - if let Some(body) = else_body { - for stmt in body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - } - } - StmtKind::While { body, .. } - | StmtKind::DoWhile { body, .. } - | StmtKind::For { body, .. } - | StmtKind::Foreach { body, .. } => { - for stmt in body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - } - StmtKind::Try { - try_body, - catches, - finally_body, - } => { - for stmt in try_body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - for catch_clause in catches { - for stmt in &catch_clause.body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - } - if let Some(body) = finally_body { - for stmt in body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - } - } - StmtKind::Switch { cases, default, .. } => { - for (_, body) in cases { - for stmt in body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - } - if let Some(body) = default { - for stmt in body { - collect_return_types(stmt, sig, capture_ctx, return_types); - } - } - } - _ => {} - } - } - - let mut capture_ctx = Context::new(); - for (name, ty, _) in capture_types { - capture_ctx.alloc_var_with_static_type(name, ty.codegen_repr(), ty.clone()); - } - - let mut return_types = Vec::new(); - for stmt in body { - collect_return_types(stmt, sig, &capture_ctx, &mut return_types); - } - if return_types.is_empty() { - return PhpType::Int; - } - let mut result = return_types[0].clone(); - for ty in &return_types[1..] { - result = super::super::widen_codegen_type(&result, ty); - } - result -} - -/// Emits a closure literal: captures, invokable frame, and return type inference. -pub(super) fn emit_closure( - params: &[(String, Option, Option, bool)], - variadic: &Option, - return_type: &Option, - body: &[Stmt], - captures: &[String], - capture_refs: &[String], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let closure_label = ctx.next_label("closure"); - - let mut capture_types: Vec<(String, PhpType, bool)> = Vec::new(); - for cap_name in captures { - let ty = ctx - .variables - .get(cap_name) - .map(|v| v.ty.clone()) - .unwrap_or(PhpType::Int); - let by_ref = capture_refs.iter().any(|name| name == cap_name); - capture_types.push((cap_name.clone(), ty, by_ref)); - } - - let mut param_types: Vec<(String, PhpType)> = params - .iter() - .map(|(p, type_ann, _, _)| { - let ty = type_ann - .as_ref() - .map(|type_ann| functions::codegen_declared_type(type_ann, ctx)) - .unwrap_or(PhpType::Int); - (p.clone(), ty) - }) - .collect(); - if let Some(variadic_name) = variadic { - param_types.push(( - variadic_name.clone(), - PhpType::Array(Box::new(PhpType::Int)), - )); - } - if let Some(expected_sig) = ctx.expected_first_class_callable_sig.as_ref() { - for (idx, (_, expected_ty)) in expected_sig.params.iter().enumerate() { - let Some((_, actual_ty)) = param_types.get_mut(idx) else { - break; - }; - let has_declared_type = params - .get(idx) - .and_then(|(_, type_ann, _, _)| type_ann.as_ref()) - .is_some(); - let is_by_ref = params.get(idx).map(|(_, _, _, is_ref)| *is_ref).unwrap_or(false); - if !has_declared_type && !is_by_ref && *actual_ty == PhpType::Int { - *actual_ty = expected_ty.clone(); - } - } - } - let mut defaults: Vec> = params - .iter() - .map(|(_, _, default, _)| default.clone()) - .collect(); - if variadic.is_some() { - defaults.push(None); - } - let mut ref_params: Vec = params.iter().map(|(_, _, _, is_ref)| *is_ref).collect(); - let mut declared_params: Vec = - params.iter().map(|(_, type_ann, _, _)| type_ann.is_some()).collect(); - if variadic.is_some() { - ref_params.push(false); - declared_params.push(false); - } - let preliminary_sig = FunctionSig { - params: param_types.clone(), - defaults: defaults.clone(), - return_type: PhpType::Int, - declared_return: false, - by_ref_return: false, - ref_params: ref_params.clone(), - declared_params: declared_params.clone(), - variadic: variadic.clone(), - deprecation: None, - }; - let resolved_return_type = return_type - .as_ref() - .map(|type_ann| functions::codegen_static_type(type_ann, ctx)) - .unwrap_or_else(|| infer_closure_return_type(body, &preliminary_sig, &capture_types)); - let sig = FunctionSig { - params: param_types, - defaults, - return_type: resolved_return_type, - declared_return: return_type.is_some(), - by_ref_return: false, - ref_params, - declared_params, - variadic: variadic.clone(), - deprecation: None, - }; - let hidden_params = capture_types.clone(); - - let param_names: Vec = params.iter().map(|(n, _, _, _)| n.clone()).collect(); - ctx.deferred_closures.push(DeferredClosure { - label: closure_label.clone(), - params: param_names, - body: body.to_vec(), - sig: sig.clone(), - captures: capture_types.clone(), - hidden_params: hidden_params.clone(), - current_class: ctx.current_class.clone(), - // Real closure literals are only reachable through their wrapper, so the - // dead-wrapper stub optimisation never applies here. - needed: true, - }); - - emitter.comment("closure: load callable descriptor"); - super::descriptor_value::emit_callable_descriptor_value( - &closure_label, - None, - crate::codegen::callable_descriptor::CALLABLE_DESC_KIND_CLOSURE, - &sig, - &capture_types, - &hidden_params, - crate::codegen::callable_descriptor::CallableDescriptorInvocation::new( - crate::codegen::callable_descriptor::CallableDescriptorShape::Closure, - ), - emitter, - ctx, - data, - ); - PhpType::Callable -} - -/// Emits a closure call expression, dispatching through __invoke or first-class callable rules. -pub(super) fn emit_closure_call( - var: &str, - args_exprs: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if let Some(class_name) = ctx - .variables - .get(var) - .and_then(|info| functions::singular_object_class(&info.static_ty)) - .map(str::to_string) - { - if ctx - .classes - .get(&class_name) - .is_some_and(|class_info| class_info.methods.contains_key("__invoke")) - { - if let Some(ret_ty) = super::emit_invokable_object_variable_call( - var, - &class_name, - args_exprs, - emitter, - ctx, - data, - ) { - return ret_ty; - } - let object = Expr::new(ExprKind::Variable(var.to_string()), Span::dummy()); - return crate::codegen::expr::objects::emit_method_call( - &object, "__invoke", args_exprs, emitter, ctx, data, - ); - } - } - - if closure_variable_is_runtime_string(var, ctx) { - let callback = Expr::new(ExprKind::Variable(var.to_string()), Span::dummy()); - let callback_ty = super::super::emit_expr(&callback, emitter, ctx, data); - debug_assert!(matches!(callback_ty.codegen_repr(), PhpType::Str)); - return super::emit_loaded_runtime_string_call(args_exprs, Span::dummy(), emitter, ctx, data); - } - - if let Some(ret_ty) = - super::emit_callable_array_variable_call(var, args_exprs, emitter, ctx, data) - { - return ret_ty; - } - - // First-class callable short-circuit: when the variable was last bound to a - // first-class callable, calling it as `$cb(args)` reaches the target directly - // instead of going through the closure wrapper. - // - // - `Function`: dispatch via extern → builtin → user-defined, mirroring - // `ExprKind::FunctionCall`. - // - `Method` with a simple object (`Variable` or `This`): the captured - // receiver is re-loaded from the original variable slot just like the - // closure wrapper does today, so `emit_method_call` preserves semantics. - // - `StaticMethod` with a `Named` receiver: late-static binding is already - // absent, so a direct static call is safe. - // - `Method` with a complex object expression or `StaticMethod` with a - // `Self_`/`Parent`/`Static` receiver fall through to the closure wrapper - // path; reconstituting their captured runtime context is left for a - // future refinement. - if let Some(target) = ctx.first_class_callable_targets.get(var).cloned() { - if first_class_callable_variable_needs_descriptor_env(&target) { - ctx.mark_fcc_used(var); - return emit_descriptor_invoker_variable_call(var, args_exprs, emitter, ctx, data); - } - match &target { - CallableTarget::Function(name) => { - let name_str = name.as_str(); - let span = args_exprs - .first() - .map(|e| e.span) - .unwrap_or_else(Span::dummy); - if ctx.extern_functions.contains_key(name_str) { - return crate::codegen::ffi::emit_extern_call( - name_str, args_exprs, span, emitter, ctx, data, - ); - } - if let Some(ty) = crate::codegen::builtins::emit_builtin_call( - name_str, args_exprs, span, emitter, ctx, data, - ) { - return ty; - } - return super::function::emit_function_call( - name_str, args_exprs, emitter, ctx, data, - ); - } - CallableTarget::Method { object, method } => { - if matches!(&object.kind, ExprKind::Variable(_) | ExprKind::This) { - return crate::codegen::expr::objects::emit_method_call( - object, method, args_exprs, emitter, ctx, data, - ); - } - } - CallableTarget::StaticMethod { receiver, method } => { - // `Named`: direct compile-time class. `Static`: late-static binding - // resolves via the caller scope's hidden `__elephc_called_class_id` / - // `$this` slot, the same chain `emit_forwarded_called_class_id` uses - // inside the closure wrapper — so calling here is equivalent without - // the wrapper trampoline. `Self_` / `Parent` are pre-resolved to - // `Named` at storage time and never reach this match arm. - if matches!(receiver, StaticReceiver::Named(_) | StaticReceiver::Static) { - return crate::codegen::expr::objects::emit_static_method_call( - receiver, method, args_exprs, emitter, ctx, data, - ); - } - } - } - } - - if closure_variable_needs_descriptor_invoker(var, ctx) { - return emit_descriptor_invoker_variable_call(var, args_exprs, emitter, ctx, data); - } - - // We reach this point only when the short-circuit above did not fire. The - // call is going to invoke the wrapper indirectly via `blr`, so the FCC - // wrapper (if any) must keep its body. This is also the path real closures - // take, where `mark_fcc_used` is a no-op. - ctx.mark_fcc_used(var); - - emitter.comment(&format!("call ${}()", var)); - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - - let sig = ctx.closure_sigs.get(var).cloned(); - let captures = ctx.closure_captures.get(var).cloned().unwrap_or_default(); - let visible_param_count = sig.as_ref().map(|s| s.params.len()).unwrap_or(args_exprs.len()); - let regular_param_count = sig - .as_ref() - .map(|s| if s.variadic.is_some() { visible_param_count.saturating_sub(1) } else { visible_param_count }) - .unwrap_or(args_exprs.len()); - let emitted_args = args::emit_pushed_call_args( - args_exprs, - sig.as_ref(), - regular_param_count, - "closure ref arg", - true, - true, - emitter, - ctx, - data, - ); - let mut arg_types = emitted_args.arg_types; - - if let Some(cached_sig) = ctx.closure_sigs.get(var).cloned() { - for deferred in &mut ctx.deferred_closures { - if deferred.sig.params == cached_sig.params && deferred.captures == captures { - for (i, ty) in arg_types.iter().enumerate() { - if i < deferred.sig.params.len() - && !deferred - .sig - .declared_params - .get(i) - .copied() - .unwrap_or(false) - && !deferred.sig.ref_params.get(i).copied().unwrap_or(false) - { - deferred.sig.params[i].1 = ty.clone(); - } - } - break; - } - } - if let Some(cached) = ctx.closure_sigs.get_mut(var) { - for (i, ty) in arg_types.iter().enumerate() { - if i < cached.params.len() - && !cached.declared_params.get(i).copied().unwrap_or(false) - && !cached.ref_params.get(i).copied().unwrap_or(false) - { - cached.params[i].1 = ty.clone(); - } - } - } - } - - let var_info = match ctx.variables.get(var) { - Some(v) => v, - None => { - emitter.comment(&format!("WARNING: undefined closure variable ${}", var)); - if save_concat_before_args { - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &PhpType::Int); - } - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - return PhpType::Int; - } - }; - let var_offset = var_info.stack_offset; - let call_reg = crate::codegen::abi::nested_call_reg(emitter); - if ctx.ref_params.contains(var) { - crate::codegen::abi::load_at_offset(emitter, call_reg, var_offset); // load the by-reference callable slot address into the nested-call scratch register - crate::codegen::abi::emit_load_from_address(emitter, call_reg, call_reg, 0); - } else { - crate::codegen::abi::load_at_offset(emitter, call_reg, var_offset); // load the callable descriptor into the nested-call scratch register - } - - for (idx, (cap_name, cap_ty, by_ref)) in captures.iter().enumerate() { - emitter.comment(&format!("push captured ${}", cap_name)); - if *by_ref { - crate::codegen::callable_descriptor::emit_load_runtime_capture_to_result( - emitter, - call_reg, - idx, - &PhpType::Int, - ); - super::args::push_arg_value(emitter, &PhpType::Int); - arg_types.push(PhpType::Int); - } else { - crate::codegen::callable_descriptor::emit_load_runtime_capture_to_result( - emitter, - call_reg, - idx, - cap_ty, - ); - super::args::push_arg_value(emitter, cap_ty); - arg_types.push(cap_ty.clone()); - } - } - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - call_reg, - call_reg, - ); - crate::codegen::abi::emit_push_reg(emitter, call_reg); - - let assignments = - crate::codegen::abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - - crate::codegen::abi::emit_pop_reg(emitter, call_reg); - let overflow_bytes = crate::codegen::abi::materialize_outgoing_args(emitter, &assignments); - - let ret_ty = ctx - .closure_sigs - .get(var) - .map(|s| s.return_type.clone()) - .unwrap_or(PhpType::Int); - - if !save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - crate::codegen::abi::emit_call_reg(emitter, call_reg); - if save_concat_before_args { - crate::codegen::abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - if ret_ty == PhpType::Str { - super::super::restore_concat_offset_after_owned_string_call(emitter, ctx); - } else { - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } - } else { - if ret_ty == PhpType::Str { - super::super::restore_concat_offset_after_owned_string_call(emitter, ctx); - } else { - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } - crate::codegen::abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - } - - ret_ty -} - -/// Returns true when a callable variable must rely on descriptor-owned metadata. -fn closure_variable_needs_descriptor_invoker(var: &str, ctx: &Context) -> bool { - if ctx.callable_param_names.contains(var) { - return true; - } - if ctx.runtime_callable_vars.contains(var) { - return true; - } - if ctx.closure_sigs.contains_key(var) { - return false; - } - ctx.variables - .get(var) - .is_some_and(|info| matches!(info.ty.codegen_repr(), PhpType::Callable)) -} - -/// Returns true when `$var(...)` should resolve the variable value as a PHP string callback. -fn closure_variable_is_runtime_string(var: &str, ctx: &Context) -> bool { - ctx.variables - .get(var) - .is_some_and(|info| matches!(info.ty.codegen_repr(), PhpType::Str)) -} - -/// Emits `$var(...)` through the callable descriptor's uniform runtime invoker. -fn emit_descriptor_invoker_variable_call( - var: &str, - args_exprs: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("call descriptor variable ${}()", var)); - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - - let Some(var_info) = ctx.variables.get(var) else { - emitter.comment(&format!("WARNING: undefined callable descriptor variable ${}", var)); - if save_concat_before_args { - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &PhpType::Mixed); - } - crate::codegen::abi::emit_load_int_immediate( - emitter, - crate::codegen::abi::int_result_reg(emitter), - 0, - ); - return PhpType::Mixed; - }; - let var_offset = var_info.stack_offset; - if ctx.ref_params.contains(var) { - crate::codegen::abi::load_at_offset( - emitter, - crate::codegen::abi::int_result_reg(emitter), - var_offset, - ); - crate::codegen::abi::emit_load_from_address( - emitter, - crate::codegen::abi::int_result_reg(emitter), - crate::codegen::abi::int_result_reg(emitter), - 0, - ); - } else { - crate::codegen::abi::load_at_offset( - emitter, - crate::codegen::abi::int_result_reg(emitter), - var_offset, - ); - } - crate::codegen::callable_descriptor::emit_retain_current_descriptor(emitter); - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the callable descriptor while building variable-call arguments - - let sig = ctx.closure_sigs.get(var).cloned(); - let arr_ty = super::descriptor_invoker_args::emit_descriptor_invoker_arg_array( - args_exprs, - sig.as_ref(), - Span::dummy(), - emitter, - ctx, - data, - ); - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the owned descriptor-invoker argument array - - let call_reg = crate::codegen::abi::nested_call_reg(emitter); - crate::codegen::abi::emit_load_temporary_stack_slot(emitter, call_reg, 16); - crate::codegen::builtins::arrays::call_user_func_array::emit_call_descriptor_array_invoker( - crate::codegen::builtins::arrays::call_user_func_array::LoadedArraySource::TemporaryStackSlot(0), - &arr_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - release_preserved_variable_call_arg_array_after_mixed_result(&arr_ty, emitter); - release_preserved_variable_call_descriptor_after_mixed_result(emitter); - PhpType::Mixed -} - -/// Returns true when a tracked first-class callable variable must use its descriptor -/// environment instead of re-reading source variables at the call site. -fn first_class_callable_variable_needs_descriptor_env(target: &CallableTarget) -> bool { - matches!( - target, - CallableTarget::Method { .. } - | CallableTarget::StaticMethod { - receiver: StaticReceiver::Static, - .. - } - ) -} - -/// Releases the synthetic variable-call argument array while preserving the Mixed result. -fn release_preserved_variable_call_arg_array_after_mixed_result( - arr_ty: &PhpType, - emitter: &mut Emitter, -) { - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the boxed call result while releasing the argument array - crate::codegen::abi::emit_load_temporary_stack_slot( - emitter, - crate::codegen::abi::int_result_reg(emitter), - 16, - ); - crate::codegen::abi::emit_decref_if_refcounted(emitter, arr_ty); - crate::codegen::abi::emit_pop_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // restore the boxed call result after argument-array cleanup - crate::codegen::abi::emit_release_temporary_stack(emitter, 16); // discard the preserved argument-array slot -} - -/// Releases the retained callable descriptor while preserving the Mixed result. -fn release_preserved_variable_call_descriptor_after_mixed_result(emitter: &mut Emitter) { - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the boxed call result while releasing the callable descriptor - crate::codegen::abi::emit_load_temporary_stack_slot( - emitter, - crate::codegen::abi::int_result_reg(emitter), - 16, - ); - crate::codegen::callable_descriptor::emit_release_current_descriptor(emitter); - crate::codegen::abi::emit_pop_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // restore the boxed call result after descriptor cleanup - crate::codegen::abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callable descriptor slot -} diff --git a/src/codegen/expr/calls/descriptor_invoker_args.rs b/src/codegen/expr/calls/descriptor_invoker_args.rs deleted file mode 100644 index fe4c564e00..0000000000 --- a/src/codegen/expr/calls/descriptor_invoker_args.rs +++ /dev/null @@ -1,916 +0,0 @@ -//! Purpose: -//! Builds runtime callable-descriptor invoker argument containers for direct expression calls. -//! Keeps synthetic indexed/associative arrays out of the indirect-call dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::calls::indirect` -//! -//! Key details: -//! - Descriptor invokers consume caller-visible argument containers and apply signature metadata themselves. -//! - Named+spread calls are lowered to a Mixed associative hash so defaults, parameter names, and variadics -//! stay behind the descriptor invoker instead of being normalized at the callsite. - -use crate::codegen::builtins::arrays::call_user_func_array; -use crate::codegen::builtins::arrays::descriptor_arg_builder; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::span::Span; -use crate::types::{call_args, FunctionSig, PhpType}; - -/// Emits the argument container passed to a descriptor invoker for a direct expression call. -pub(super) fn emit_descriptor_invoker_arg_array( - args_exprs: &[Expr], - sig: Option<&FunctionSig>, - span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let encode_ref_markers = should_encode_invoker_ref_args(sig, args_exprs); - if has_explicit_named_and_spread(args_exprs) { - if let Some(sig) = sig { - return emit_named_spread_invoker_arg_hash( - args_exprs, - sig, - span, - encode_ref_markers, - emitter, - ctx, - data, - ); - } - return emit_untyped_named_spread_invoker_arg_hash( - args_exprs, - span, - encode_ref_markers, - emitter, - ctx, - data, - ); - } - - if let Some(spread_inner) = single_spread_inner(args_exprs) { - return super::super::emit_expr(spread_inner, emitter, ctx, data); - } - - if has_explicit_named(args_exprs) && encode_ref_markers { - return emit_named_invoker_arg_hash(args_exprs, true, emitter, ctx, data); - } - - if plain_positional_args(args_exprs) && encode_ref_markers { - return descriptor_arg_builder::emit_indexed_invoker_arg_array( - args_exprs, - true, - emitter, - ctx, - data, - ); - } - - if has_spread(args_exprs) { - if let Some(ty) = descriptor_arg_builder::emit_positional_spread_invoker_arg_array( - &[], - args_exprs, - sig, - encode_ref_markers, - emitter, - ctx, - data, - ) { - return ty; - } - } - - let arg_array = descriptor_invoker_arg_array_expr(args_exprs, span); - super::super::emit_expr(&arg_array, emitter, ctx, data) -} - -/// Emits a descriptor-invoker argument container with a saved object receiver in descriptor slot zero. -#[allow(clippy::too_many_arguments)] -pub(super) fn emit_descriptor_invoker_arg_array_with_saved_object_prefix( - object_stack_offset: usize, - args_exprs: &[Expr], - sig: Option<&FunctionSig>, - span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let encode_ref_markers = should_encode_invoker_ref_args(sig, args_exprs); - if has_explicit_named_and_spread(args_exprs) { - if let Some(sig) = sig { - return emit_named_spread_invoker_arg_hash_with_saved_object_prefix( - object_stack_offset, - args_exprs, - sig, - span, - encode_ref_markers, - emitter, - ctx, - data, - ); - } - } - - if has_explicit_named(args_exprs) { - return emit_named_invoker_arg_hash_with_saved_object_prefix( - object_stack_offset, - args_exprs, - encode_ref_markers, - emitter, - ctx, - data, - ); - } - - if has_spread(args_exprs) { - if let Some(ty) = - descriptor_arg_builder::emit_positional_spread_invoker_arg_array_with_saved_object_prefix( - object_stack_offset, - args_exprs, - sig, - encode_ref_markers, - emitter, - ctx, - data, - ) - { - return ty; - } - } - - descriptor_arg_builder::emit_indexed_invoker_arg_array_with_saved_object_prefix( - object_stack_offset, - args_exprs, - sig, - encode_ref_markers, - emitter, - ctx, - data, - ) -} - -/// Returns true when a direct descriptor call mixes explicit named args with spread args. -fn has_explicit_named_and_spread(args_exprs: &[Expr]) -> bool { - let has_spread = args_exprs - .iter() - .any(|arg| matches!(arg.kind, ExprKind::Spread(_))); - has_explicit_named(args_exprs) && has_spread -} - -/// Returns true when any descriptor-call argument uses named-argument syntax. -fn has_explicit_named(args_exprs: &[Expr]) -> bool { - args_exprs - .iter() - .any(|arg| matches!(arg.kind, ExprKind::NamedArg { .. })) -} - -/// Returns true when any direct descriptor-call argument uses spread syntax. -fn has_spread(args_exprs: &[Expr]) -> bool { - args_exprs - .iter() - .any(|arg| matches!(arg.kind, ExprKind::Spread(_))) -} - -/// Returns true when the descriptor call has only positional, non-spread arguments. -fn plain_positional_args(args_exprs: &[Expr]) -> bool { - args_exprs - .iter() - .all(|arg| !matches!(arg.kind, ExprKind::NamedArg { .. } | ExprKind::Spread(_))) -} - -/// Returns whether variable arguments should be encoded for runtime by-ref decisions. -fn should_encode_invoker_ref_args(sig: Option<&FunctionSig>, args_exprs: &[Expr]) -> bool { - if !args_exprs.iter().any(arg_value_is_variable) { - return false; - } - sig.is_none_or(|sig| sig.ref_params.iter().any(|is_ref| *is_ref)) -} - -/// Returns true when an argument's runtime value is sourced from a variable. -fn arg_value_is_variable(arg: &Expr) -> bool { - match &arg.kind { - ExprKind::Variable(_) => true, - ExprKind::NamedArg { value, .. } => matches!(value.kind, ExprKind::Variable(_)), - _ => false, - } -} - -/// Returns the spread source when the entire descriptor call is `(...$args)`. -fn single_spread_inner(args_exprs: &[Expr]) -> Option<&Expr> { - if let [arg] = args_exprs { - if let ExprKind::Spread(inner) = &arg.kind { - return Some(inner); - } - } - None -} - -/// Builds the synthetic argument container passed to a descriptor invoker. -fn descriptor_invoker_arg_array_expr(args_exprs: &[Expr], span: Span) -> Expr { - let has_explicit_named = args_exprs - .iter() - .any(|arg| matches!(arg.kind, ExprKind::NamedArg { .. })); - if !has_explicit_named { - return Expr::new(ExprKind::ArrayLiteral(args_exprs.to_vec()), span); - } - - let mut next_positional_key = 0i64; - let mut entries = Vec::with_capacity(args_exprs.len()); - for arg in args_exprs { - match &arg.kind { - ExprKind::NamedArg { name, value } => { - entries.push(( - Expr::new(ExprKind::StringLiteral(name.clone()), arg.span), - (**value).clone(), - )); - } - _ => { - entries.push(( - Expr::new(ExprKind::IntLiteral(next_positional_key), arg.span), - arg.clone(), - )); - next_positional_key += 1; - } - } - } - - Expr::new(ExprKind::ArrayLiteralAssoc(entries), span) -} - -/// Emits a Mixed associative hash for direct descriptor calls with spread prefixes and named suffixes. -fn emit_named_spread_invoker_arg_hash( - args_exprs: &[Expr], - sig: &FunctionSig, - span: Span, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let regular_param_count = super::args::regular_param_count(Some(sig), args_exprs.len()); - let assoc_spread_sources = vec![false; args_exprs.len()]; - let plan = call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( - sig, - args_exprs, - span, - regular_param_count, - false, - true, - &assoc_spread_sources, - ) - .expect("codegen received invalid descriptor named+spread arguments after type checking"); - let first_named_pos = plan - .first_named_pos - .expect("named+spread descriptor plan must contain a named suffix"); - - emitter.comment("descriptor invoker named+spread argument hash"); - emit_descriptor_prefix_as_mixed_hash(&plan, sig, span, encode_ref_markers, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the descriptor argument hash alive while named suffix entries are inserted - - for arg in plan.source_args.iter().skip(first_named_pos) { - if let ExprKind::NamedArg { name, value } = &arg.kind { - emit_named_suffix_entry(name, value, encode_ref_markers, emitter, ctx, data); - } - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed descriptor argument hash - PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - } -} - -/// Emits a Mixed hash for named+spread descriptor calls with a saved object receiver prefix. -#[allow(clippy::too_many_arguments)] -fn emit_named_spread_invoker_arg_hash_with_saved_object_prefix( - object_stack_offset: usize, - args_exprs: &[Expr], - sig: &FunctionSig, - span: Span, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let mut source_args = Vec::with_capacity(args_exprs.len() + 1); - source_args.push(receiver_prefix_placeholder_expr(span)); - source_args.extend(args_exprs.iter().cloned()); - let regular_param_count = super::args::regular_param_count(Some(sig), source_args.len()); - let assoc_spread_sources = vec![false; source_args.len()]; - let plan = call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( - sig, - &source_args, - span, - regular_param_count, - false, - true, - &assoc_spread_sources, - ) - .expect("codegen received invalid receiver-prefixed descriptor named+spread arguments after type checking"); - let first_named_pos = plan - .first_named_pos - .expect("receiver-prefixed named+spread descriptor plan must contain a named suffix"); - - emitter.comment("descriptor invoker receiver-prefixed named+spread argument hash"); - emit_receiver_prefixed_descriptor_prefix_as_mixed_hash( - object_stack_offset, - &plan.source_args[1..first_named_pos], - sig, - span, - encode_ref_markers, - emitter, - ctx, - data, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the receiver-prefixed descriptor argument hash alive while named suffix entries are inserted - - for arg in plan.source_args.iter().skip(first_named_pos) { - if let ExprKind::NamedArg { name, value } = &arg.kind { - emit_named_suffix_entry(name, value, encode_ref_markers, emitter, ctx, data); - } - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed receiver-prefixed descriptor argument hash - PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - } -} - -/// Emits the positional prefix of a named+spread descriptor call as a Mixed hash. -fn emit_descriptor_prefix_as_mixed_hash( - plan: &call_args::CallArgPlan, - sig: &FunctionSig, - span: Span, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let prefix_expr = plan.positional_prefix_expr(span); - let first_named_pos = plan - .first_named_pos - .expect("descriptor named+spread prefix must know the first named source position"); - emit_descriptor_prefix_args_as_mixed_hash( - prefix_expr.as_ref(), - &plan.source_args[..first_named_pos], - Some(sig), - span, - encode_ref_markers, - emitter, - ctx, - data, - ); -} - -/// Emits positional-prefix args as a Mixed hash with a saved object receiver in numeric key zero. -#[allow(clippy::too_many_arguments)] -fn emit_receiver_prefixed_descriptor_prefix_as_mixed_hash( - object_stack_offset: usize, - prefix_args: &[Expr], - sig: &FunctionSig, - span: Span, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let prefix_ty = if prefix_args.iter().any(|arg| matches!(arg.kind, ExprKind::Spread(_))) { - descriptor_arg_builder::emit_positional_spread_invoker_arg_array_with_saved_object_prefix( - object_stack_offset, - prefix_args, - Some(sig), - encode_ref_markers, - emitter, - ctx, - data, - ) - .unwrap_or_else(|| { - descriptor_arg_builder::emit_indexed_invoker_arg_array_with_saved_object_prefix( - object_stack_offset, - prefix_args, - Some(sig), - encode_ref_markers, - emitter, - ctx, - data, - ) - }) - } else { - let _ = span; - descriptor_arg_builder::emit_indexed_invoker_arg_array_with_saved_object_prefix( - object_stack_offset, - prefix_args, - Some(sig), - encode_ref_markers, - emitter, - ctx, - data, - ) - }; - emit_indexed_prefix_as_mixed_hash(&prefix_ty, emitter); -} - -/// Emits positional-prefix source arguments as a Mixed hash, preserving variable ref markers when needed. -#[allow(clippy::too_many_arguments)] -fn emit_descriptor_prefix_args_as_mixed_hash( - prefix_expr: Option<&Expr>, - prefix_args: &[Expr], - sig: Option<&FunctionSig>, - span: Span, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if prefix_args.is_empty() { - emit_descriptor_prefix_expr_as_mixed_hash(None, emitter, ctx, data); - return; - } - - if encode_ref_markers { - if let Some(prefix_ty) = descriptor_arg_builder::emit_positional_spread_invoker_arg_array( - &[], - prefix_args, - sig, - true, - emitter, - ctx, - data, - ) { - emit_indexed_prefix_as_mixed_hash(&prefix_ty, emitter); - return; - } - - if plain_positional_args(prefix_args) { - let prefix_ty = descriptor_arg_builder::emit_indexed_invoker_arg_array( - prefix_args, - true, - emitter, - ctx, - data, - ); - emit_indexed_prefix_as_mixed_hash(&prefix_ty, emitter); - return; - } - } - - if let Some(prefix_expr) = prefix_expr { - emit_descriptor_prefix_expr_as_mixed_hash(Some(prefix_expr), emitter, ctx, data); - } else { - let fallback_prefix = Expr::new(ExprKind::ArrayLiteral(prefix_args.to_vec()), span); - emit_descriptor_prefix_expr_as_mixed_hash(Some(&fallback_prefix), emitter, ctx, data); - } -} - -/// Emits an optional positional prefix expression as a Mixed hash. -fn emit_descriptor_prefix_expr_as_mixed_hash( - prefix_expr: Option<&Expr>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let Some(prefix_expr) = prefix_expr else { - crate::codegen::expr::arrays::emit_empty_assoc_array_literal( - PhpType::Mixed, - PhpType::Mixed, - emitter, - ); - return; - }; - - let prefix_ty = super::super::emit_expr(&prefix_expr, emitter, ctx, data); - match prefix_ty { - PhpType::AssocArray { .. } => { - call_user_func_array::emit_clone_assoc_array_for_invoker( - abi::int_result_reg(emitter), - emitter, - ); - } - PhpType::Array(_) => { - emit_indexed_prefix_as_mixed_hash(&prefix_ty, emitter); - } - PhpType::Mixed | PhpType::Union(_) => { - emit_mixed_prefix_as_hash_or_abort(emitter, ctx, data); - } - _ => { - crate::codegen::expr::arrays::emit_empty_assoc_array_literal( - PhpType::Mixed, - PhpType::Mixed, - emitter, - ); - } - } -} - -/// Emits a Mixed hash for named+spread descriptor calls without a local signature. -fn emit_untyped_named_spread_invoker_arg_hash( - args_exprs: &[Expr], - span: Span, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let first_named_pos = args_exprs - .iter() - .position(|arg| matches!(arg.kind, ExprKind::NamedArg { .. })) - .expect("named+spread descriptor call must contain a named suffix"); - let prefix_expr = if first_named_pos == 0 { - None - } else { - Some(Expr::new( - ExprKind::ArrayLiteral(args_exprs[..first_named_pos].to_vec()), - span, - )) - }; - - emitter.comment("descriptor invoker untyped named+spread argument hash"); - emit_descriptor_prefix_args_as_mixed_hash( - prefix_expr.as_ref(), - &args_exprs[..first_named_pos], - None, - span, - encode_ref_markers, - emitter, - ctx, - data, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the descriptor argument hash alive while untyped named suffix entries are inserted - - for arg in args_exprs.iter().skip(first_named_pos) { - if let ExprKind::NamedArg { name, value } = &arg.kind { - emit_named_suffix_entry(name, value, encode_ref_markers, emitter, ctx, data); - } - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed untyped named+spread argument hash - PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - } -} - -/// Emits a Mixed associative hash for descriptor calls that use named arguments. -fn emit_named_invoker_arg_hash( - args_exprs: &[Expr], - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("descriptor invoker named argument hash"); - emit_descriptor_prefix_expr_as_mixed_hash(None, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the descriptor named-argument hash alive while entries are inserted - - let mut next_positional_key = 0usize; - for arg in args_exprs { - match &arg.kind { - ExprKind::NamedArg { name, value } => { - emit_named_suffix_entry(name, value, encode_ref_markers, emitter, ctx, data); - } - ExprKind::Spread(_) => {} - _ => { - emit_numeric_suffix_entry( - next_positional_key, - arg, - encode_ref_markers, - emitter, - ctx, - data, - ); - next_positional_key += 1; - } - } - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed descriptor named-argument hash - PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - } -} - -/// Emits a Mixed hash for descriptor calls with named args and a saved object receiver prefix. -fn emit_named_invoker_arg_hash_with_saved_object_prefix( - object_stack_offset: usize, - args_exprs: &[Expr], - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("descriptor invoker receiver-prefixed named argument hash"); - emit_descriptor_prefix_expr_as_mixed_hash(None, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // keep the receiver-prefixed descriptor named-argument hash alive while entries are inserted - emit_saved_object_prefix_numeric_entry(object_stack_offset + 16, emitter); - - let mut next_positional_key = 1usize; - for arg in args_exprs { - match &arg.kind { - ExprKind::NamedArg { name, value } => { - emit_named_suffix_entry(name, value, encode_ref_markers, emitter, ctx, data); - } - ExprKind::Spread(_) => {} - _ => { - emit_numeric_suffix_entry( - next_positional_key, - arg, - encode_ref_markers, - emitter, - ctx, - data, - ); - next_positional_key += 1; - } - } - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // return the completed receiver-prefixed descriptor named-argument hash - PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - } -} - -/// Converts the current indexed-array prefix result into a Mixed associative hash. -fn emit_indexed_prefix_as_mixed_hash(prefix_ty: &PhpType, emitter: &mut Emitter) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the evaluated positional prefix while allocating the destination hash - crate::codegen::expr::arrays::emit_empty_assoc_array_literal( - PhpType::Mixed, - PhpType::Mixed, - emitter, - ); - emit_hash_array_union_with_saved_right_operand(emitter); - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved positional-prefix array after copying its elements - if !matches!(prefix_ty, PhpType::Array(elem) if matches!(elem.codegen_repr(), PhpType::Mixed)) { - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // pass the merged descriptor argument hash to the Mixed conversion helper - } - abi::emit_call_label(emitter, "__rt_hash_to_mixed"); - } -} - -/// Merges the current hash result with the indexed array saved at the top of the temporary stack. -fn emit_hash_array_union_with_saved_right_operand(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x1", 0); - abi::emit_call_label(emitter, "__rt_hash_array_union"); - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // pass the empty descriptor argument hash as the left union operand - abi::emit_load_temporary_stack_slot(emitter, "rsi", 0); - abi::emit_call_label(emitter, "__rt_hash_array_union"); - } - } -} - -/// Boxes a saved object pointer and inserts it as numeric key zero in the current hash. -fn emit_saved_object_prefix_numeric_entry(object_stack_offset: usize, emitter: &mut Emitter) { - let object_reg = abi::secondary_scratch_reg(emitter); - let zero_reg = abi::tertiary_scratch_reg(emitter); - let tag_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, object_reg, object_stack_offset); - abi::emit_load_int_immediate(emitter, zero_reg, 0); - abi::emit_load_int_immediate( - emitter, - tag_reg, - crate::codegen::runtime_value_tag(&PhpType::Object(String::new())) as i64, - ); - crate::codegen::emit_box_runtime_payload_as_mixed(emitter, tag_reg, object_reg, zero_reg); - emit_hash_set_current_mixed_numeric_suffix(0, emitter); -} - -/// Builds a placeholder source argument that occupies descriptor slot zero during planning. -fn receiver_prefix_placeholder_expr(span: Span) -> Expr { - Expr::new(ExprKind::IntLiteral(0), span) -} - -/// Converts a runtime Mixed prefix container into a Mixed hash or aborts on invalid shape. -fn emit_mixed_prefix_as_hash_or_abort( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let tag_reg = abi::secondary_scratch_reg(emitter); - let payload_reg = abi::tertiary_scratch_reg(emitter); - let indexed_label = ctx.next_label("descriptor_prefix_indexed"); - let assoc_label = ctx.next_label("descriptor_prefix_assoc"); - let done_label = ctx.next_label("descriptor_prefix_done"); - let indexed_ty = PhpType::Array(Box::new(PhpType::Mixed)); - let assoc_ty = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; - - abi::emit_load_from_address(emitter, tag_reg, abi::int_result_reg(emitter), 0); - abi::emit_load_from_address(emitter, payload_reg, abi::int_result_reg(emitter), 8); - abi::emit_push_reg(emitter, payload_reg); // preserve the unboxed mixed prefix payload while dispatching by container tag - emit_branch_if_mixed_tag( - tag_reg, - crate::codegen::runtime_value_tag(&indexed_ty), - &indexed_label, - emitter, - ); - emit_branch_if_mixed_tag( - tag_reg, - crate::codegen::runtime_value_tag(&assoc_ty), - &assoc_label, - emitter, - ); - emit_invalid_descriptor_prefix_abort(emitter, data); - - emitter.label(&indexed_label); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 0); - call_user_func_array::emit_clone_indexed_array_for_invoker_with_runtime_tag( - abi::int_result_reg(emitter), - emitter, - ); - emit_indexed_prefix_as_mixed_hash(&indexed_ty, emitter); - abi::emit_jump(emitter, &done_label); - - emitter.label(&assoc_label); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 0); - call_user_func_array::emit_clone_assoc_array_for_invoker(abi::int_result_reg(emitter), emitter); - - emitter.label(&done_label); - abi::emit_release_temporary_stack(emitter, 16); // discard the preserved mixed prefix payload after normalization -} - -/// Branches to `label` when `tag_reg` matches the expected Mixed runtime tag. -fn emit_branch_if_mixed_tag(tag_reg: &str, expected_tag: u8, label: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", tag_reg, expected_tag)); // compare the mixed prefix payload tag with the expected container shape - emitter.instruction(&format!("b.eq {}", label)); // handle this prefix container shape when the tag matches - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", tag_reg, expected_tag)); // compare the mixed prefix payload tag with the expected container shape - emitter.instruction(&format!("je {}", label)); // handle this prefix container shape when the tag matches - } - } -} - -/// Emits the fatal diagnostic for an invalid mixed prefix argument container. -fn emit_invalid_descriptor_prefix_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: callable descriptor named-spread prefix must be an array\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the descriptor prefix diagnostic to stderr - abi::emit_symbol_address(emitter, "x1", &message_label); - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the descriptor prefix diagnostic byte length to write() - emitter.syscall(4); - abi::emit_exit(emitter, 1); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the descriptor prefix diagnostic to stderr - abi::emit_symbol_address(emitter, "rsi", &message_label); - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the descriptor prefix diagnostic byte length to write() - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the descriptor prefix diagnostic - abi::emit_exit(emitter, 1); - } - } -} - -/// Inserts one named suffix value into the descriptor invoker argument hash. -fn emit_named_suffix_entry( - name: &str, - value: &Expr, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_mixed_invoker_hash_value(value, encode_ref_markers, emitter, ctx, data); - emit_hash_set_current_mixed_named_suffix(name, emitter, data); -} - -/// Inserts one numeric positional-prefix value into the descriptor invoker argument hash. -fn emit_numeric_suffix_entry( - index: usize, - value: &Expr, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emit_mixed_invoker_hash_value(value, encode_ref_markers, emitter, ctx, data); - emit_hash_set_current_mixed_numeric_suffix(index, emitter); -} - -/// Emits one descriptor-invoker hash value as boxed Mixed or a variable ref-cell marker. -fn emit_mixed_invoker_hash_value( - value: &Expr, - encode_ref_markers: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if encode_ref_markers { - if let ExprKind::Variable(var_name) = &value.kind { - if !super::args::emit_ref_arg_variable_address( - var_name, - "descriptor invoker named arg", - emitter, - ctx, - ) { - panic!("descriptor invoker named argument variable not found"); - } - descriptor_arg_builder::emit_box_current_ref_arg_address_for_invoker( - var_name, - emitter, - ctx, - ); - return; - } - } - - let mut value_ty = super::super::emit_expr(value, emitter, ctx, data); - let boxed_iterable = - crate::codegen::emit_box_iterable_value_for_mixed_container(emitter, &mut value_ty); - if !matches!(value_ty, PhpType::Mixed | PhpType::Union(_)) { - crate::codegen::emit_box_current_expr_value_as_mixed_for_container( - emitter, - value, - &value_ty, - ); - } else if !boxed_iterable { - retain_borrowed_mixed_named_suffix(emitter, value, &value_ty); - } -} - -/// Retains a borrowed Mixed suffix value before storing it in the descriptor argument hash. -fn retain_borrowed_mixed_named_suffix(emitter: &mut Emitter, value: &Expr, value_ty: &PhpType) { - if value_ty.codegen_repr().is_refcounted() - && super::super::expr_result_heap_ownership(value) != HeapOwnership::Owned - { - abi::emit_incref_if_refcounted(emitter, &value_ty.codegen_repr()); - } -} - -/// Calls `__rt_hash_set` to store the current boxed Mixed value under a string key. -fn emit_hash_set_current_mixed_named_suffix( - name: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (key_label, key_len) = data.add_string(name.as_bytes()); - let result_reg = abi::int_result_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, x0"); // pass the boxed named argument as the hash value payload - emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use a high payload word - abi::emit_load_int_immediate(emitter, "x5", crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64); - abi::emit_load_temporary_stack_slot(emitter, "x0", 0); - abi::emit_symbol_address(emitter, "x1", &key_label); - abi::emit_load_int_immediate(emitter, "x2", key_len as i64); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, result_reg, "sp", 0); - } - Arch::X86_64 => { - emitter.instruction("mov rcx, rax"); // pass the boxed named argument as the hash value payload - abi::emit_load_int_immediate(emitter, "r8", 0); - abi::emit_load_int_immediate(emitter, "r9", crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64); - abi::emit_load_temporary_stack_slot(emitter, "rdi", 0); - abi::emit_symbol_address(emitter, "rsi", &key_label); - abi::emit_load_int_immediate(emitter, "rdx", key_len as i64); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, result_reg, "rsp", 0); - } - } -} - -/// Calls `__rt_hash_set` to store the current boxed Mixed value under an integer key. -fn emit_hash_set_current_mixed_numeric_suffix(index: usize, emitter: &mut Emitter) { - let result_reg = abi::int_result_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x3, x0"); // pass the boxed positional argument as the hash value payload - emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use a high payload word - abi::emit_load_int_immediate(emitter, "x5", crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64); - abi::emit_load_temporary_stack_slot(emitter, "x0", 0); - abi::emit_load_int_immediate(emitter, "x1", index as i64); - abi::emit_load_int_immediate(emitter, "x2", -1); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, result_reg, "sp", 0); - } - Arch::X86_64 => { - emitter.instruction("mov rcx, rax"); // pass the boxed positional argument as the hash value payload - abi::emit_load_int_immediate(emitter, "r8", 0); - abi::emit_load_int_immediate(emitter, "r9", crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64); - abi::emit_load_temporary_stack_slot(emitter, "rdi", 0); - abi::emit_load_int_immediate(emitter, "rsi", index as i64); - abi::emit_load_int_immediate(emitter, "rdx", -1); - abi::emit_call_label(emitter, "__rt_hash_set"); - abi::emit_store_to_address(emitter, result_reg, "rsp", 0); - } - } -} diff --git a/src/codegen/expr/calls/descriptor_value.rs b/src/codegen/expr/calls/descriptor_value.rs deleted file mode 100644 index a77b6dca06..0000000000 --- a/src/codegen/expr/calls/descriptor_value.rs +++ /dev/null @@ -1,323 +0,0 @@ -//! Purpose: -//! Materializes callable descriptor values for closures and first-class callables. -//! Handles static descriptor records, generated runtime invokers, and runtime -//! capture slots for callable environments. -//! -//! Called from: -//! - `crate::codegen::expr::calls::closure` -//! - `crate::codegen::expr::calls::first_class` -//! -//! Key details: -//! - Runtime descriptors preserve the static descriptor header, then append -//! 16-byte capture value slots consumed by uniform descriptor invokers. - -use crate::codegen::callable_descriptor::{ - self, CallableDescriptorInvocation, -}; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, callable_dispatch}; -use crate::types::{FunctionSig, PhpType}; - -use super::args; - -/// Emits a callable descriptor value, allocating runtime capture storage when needed. -#[allow(clippy::too_many_arguments)] -pub(super) fn emit_callable_descriptor_value( - entry_label: &str, - php_name: Option<&str>, - kind: u64, - sig: &FunctionSig, - captures: &[(String, PhpType, bool)], - hidden_params: &[(String, PhpType, bool)], - invocation: CallableDescriptorInvocation, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let invoker_label = callable_dispatch::ensure_runtime_descriptor_invoker(ctx, hidden_params, sig); - let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( - data, - entry_label, - php_name, - kind, - Some(sig), - captures, - hidden_params, - invocation, - invoker_label.as_deref(), - ); - - if captures.is_empty() { - abi::emit_symbol_address(emitter, abi::int_result_reg(emitter), &descriptor_label); - return; - } - - emit_runtime_descriptor_with_captures( - &descriptor_label, - captures, - emitter, - ctx, - ); -} - -/// Allocates a runtime descriptor, copies the static header, and stores capture values. -fn emit_runtime_descriptor_with_captures( - descriptor_label: &str, - captures: &[(String, PhpType, bool)], - emitter: &mut Emitter, - ctx: &mut Context, -) { - let descriptor_reg = abi::nested_call_reg(emitter); - let total_bytes = - callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + captures.len() * 16; - - emitter.comment("callable descriptor: allocate runtime capture storage"); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), total_bytes as i64); - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!("mov {}, {}", descriptor_reg, abi::int_result_reg(emitter))); // keep the runtime descriptor pointer while storing captures - callable_descriptor::emit_copy_static_descriptor_to_runtime( - emitter, - descriptor_reg, - descriptor_label, - ); - - for (idx, (capture_name, capture_ty, by_ref)) in captures.iter().enumerate() { - emitter.comment(&format!("callable descriptor: store capture ${}", capture_name)); - if matches!(capture_ty.codegen_repr(), PhpType::Callable) { - ctx.mark_fcc_used(capture_name); - } - if *by_ref { - promote_by_ref_capture_to_heap_cell(capture_name, capture_ty, emitter, ctx); - if !args::emit_ref_arg_variable_address( - capture_name, - "callable descriptor capture ref", - emitter, - ctx, - ) { - emitter.comment(&format!( - "WARNING: captured callable variable ${} not found", - capture_name - )); - continue; - } - callable_descriptor::emit_store_current_result_to_runtime_capture( - emitter, - descriptor_reg, - idx, - &PhpType::Int, - ); - continue; - } - - let Some(capture_info) = ctx.variables.get(capture_name) else { - emitter.comment(&format!( - "WARNING: captured callable variable ${} not found", - capture_name - )); - continue; - }; - abi::emit_load(emitter, capture_ty, capture_info.stack_offset); - if matches!(capture_ty.codegen_repr(), PhpType::Str) { - abi::emit_call_label(emitter, "__rt_str_persist"); - callable_descriptor::emit_store_current_result_to_runtime_capture( - emitter, - descriptor_reg, - idx, - capture_ty, - ); - continue; - } - callable_descriptor::emit_store_current_result_to_runtime_capture( - emitter, - descriptor_reg, - idx, - capture_ty, - ); - retain_runtime_capture_result(emitter, capture_ty); - } - - if descriptor_reg != abi::int_result_reg(emitter) { - emitter.instruction(&format!("mov {}, {}", abi::int_result_reg(emitter), descriptor_reg)); // return the runtime callable descriptor pointer - } -} - -/// Promotes a local by-reference capture into a stable heap cell before descriptor storage. -/// -/// Plain locals normally live in frame slots, but an escaped closure can outlive that -/// frame. The promotion copies the current local value into a 16-byte heap reference -/// cell, rewrites the local slot to hold the cell address, and marks the variable as a -/// reference so later writes update the same storage captured by the closure. -fn promote_by_ref_capture_to_heap_cell( - capture_name: &str, - capture_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) { - if ctx.global_vars.contains(capture_name) - || ctx.static_vars.contains(capture_name) - || ctx.ref_params.contains(capture_name) - { - return; - } - let Some((slot_offset, current_ty, current_static_ty, current_ownership)) = - ctx.variables.get(capture_name).map(|var| { - ( - var.stack_offset, - var.ty.clone(), - var.static_ty.clone(), - var.ownership, - ) - }) - else { - emitter.comment(&format!( - "WARNING: captured callable variable ${} not found", - capture_name - )); - return; - }; - - emitter.comment(&format!( - "callable descriptor: promote by-ref capture ${} to heap cell", - capture_name - )); - 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))); // keep the promoted capture cell while moving the local value into it - copy_local_value_to_ref_cell(¤t_ty, slot_offset, cell_reg, emitter); - release_owned_local_value_after_ref_cell_copy( - ¤t_ty, - current_ownership, - slot_offset, - cell_reg, - emitter, - ); - abi::store_at_offset_scratch( - emitter, - cell_reg, - slot_offset, - abi::temp_int_reg(emitter.target), - ); - ctx.ref_params.insert(capture_name.to_string()); - ctx.update_var_type_static_and_ownership( - capture_name, - capture_ty.codegen_repr(), - current_static_ty, - HeapOwnership::borrowed_alias_for_type(capture_ty), - ); -} - -/// Copies the current local value into a promoted heap reference cell. -/// -/// Strings are persisted so the cell owns stable storage, callable descriptors are -/// retained, and refcounted heap payloads are incref'd. The local slot is not mutated -/// here; the caller stores the cell pointer after any old local owner is released. -fn copy_local_value_to_ref_cell( - value_ty: &PhpType, - slot_offset: usize, - cell_reg: &str, - emitter: &mut Emitter, -) { - let temp_reg = abi::temp_int_reg(emitter.target); - match value_ty.codegen_repr() { - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::load_at_offset_scratch(emitter, ptr_reg, slot_offset, temp_reg); - abi::load_at_offset_scratch(emitter, len_reg, slot_offset - 8, temp_reg); - abi::emit_push_reg(emitter, cell_reg); // preserve the promoted capture cell across string persistence - abi::emit_call_label(emitter, "__rt_str_persist"); // detach the captured string before storing it in the reference cell - abi::emit_pop_reg(emitter, cell_reg); // restore the promoted capture cell after string persistence - abi::emit_store_to_address(emitter, ptr_reg, cell_reg, 0); - abi::emit_store_to_address(emitter, len_reg, cell_reg, 8); - } - PhpType::Float => { - abi::load_at_offset_scratch( - emitter, - abi::float_result_reg(emitter), - slot_offset, - temp_reg, - ); - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), cell_reg, 0); - abi::emit_store_zero_to_address(emitter, cell_reg, 8); - } - PhpType::Callable => { - abi::load_at_offset_scratch(emitter, abi::int_result_reg(emitter), slot_offset, temp_reg); - abi::emit_push_reg(emitter, cell_reg); // preserve the promoted capture cell while retaining the callable descriptor - callable_descriptor::emit_retain_current_descriptor(emitter); - abi::emit_pop_reg(emitter, cell_reg); // restore the promoted capture cell after retaining the callable descriptor - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), cell_reg, 0); - abi::emit_store_zero_to_address(emitter, cell_reg, 8); - } - ty if ty.is_refcounted() => { - abi::load_at_offset_scratch(emitter, abi::int_result_reg(emitter), slot_offset, temp_reg); - abi::emit_push_reg(emitter, cell_reg); // preserve the promoted capture cell across the payload retain - abi::emit_incref_if_refcounted(emitter, &ty); - abi::emit_pop_reg(emitter, cell_reg); // restore the promoted capture cell after retaining the payload - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), cell_reg, 0); - abi::emit_store_zero_to_address(emitter, cell_reg, 8); - } - _ => { - abi::load_at_offset_scratch( - emitter, - temp_reg, - slot_offset, - abi::secondary_scratch_reg(emitter), - ); - abi::emit_store_to_address(emitter, temp_reg, cell_reg, 0); - abi::emit_store_zero_to_address(emitter, cell_reg, 8); - } - } -} - -/// Releases a replaced owned local value after copying it into a reference cell. -/// -/// Only owned strings, callable descriptors, and refcounted heap payloads need release. -/// Borrowed or scalar locals remain untouched because the new cell owns its copy/retain. -fn release_owned_local_value_after_ref_cell_copy( - value_ty: &PhpType, - ownership: HeapOwnership, - slot_offset: usize, - cell_reg: &str, - emitter: &mut Emitter, -) { - if ownership != HeapOwnership::Owned { - return; - } - if !matches!(value_ty.codegen_repr(), PhpType::Str | PhpType::Callable) - && !value_ty.is_refcounted() - { - return; - } - - abi::emit_push_reg(emitter, cell_reg); // preserve the promoted capture cell while releasing the replaced local owner - abi::load_at_offset_scratch( - emitter, - abi::int_result_reg(emitter), - slot_offset, - abi::temp_int_reg(emitter.target), - ); - if matches!(value_ty.codegen_repr(), PhpType::Str) { - abi::emit_call_label(emitter, "__rt_heap_free_safe"); // release the old local string now that the capture cell owns a persisted copy - } else if matches!(value_ty.codegen_repr(), PhpType::Callable) { - callable_descriptor::emit_release_current_descriptor(emitter); - } else { - abi::emit_decref_if_refcounted(emitter, value_ty); - } - abi::emit_pop_reg(emitter, cell_reg); // restore the promoted capture cell for storage in the local slot -} - -/// Retains a by-value capture stored in a runtime descriptor. -fn retain_runtime_capture_result(emitter: &mut Emitter, capture_ty: &PhpType) { - match capture_ty.codegen_repr() { - PhpType::Str => {} - PhpType::Callable => { - callable_descriptor::emit_retain_current_descriptor(emitter); - } - other if other.is_refcounted() => { - abi::emit_incref_if_refcounted(emitter, &other); - } - _ => {} - } -} diff --git a/src/codegen/expr/calls/first_class.rs b/src/codegen/expr/calls/first_class.rs deleted file mode 100644 index 3d2b12fa72..0000000000 --- a/src/codegen/expr/calls/first_class.rs +++ /dev/null @@ -1,456 +0,0 @@ -//! Purpose: -//! Lowers first-class callable creation for functions, methods, and builtins. -//! Resolves the callable shape, prepares arguments, and leaves the call result for expression consumers. -//! -//! Called from: -//! - `crate::codegen::expr::calls` -//! -//! Key details: -//! - Callable metadata and argument signatures must stay synchronized with type checking and runtime dispatch. - -use crate::codegen::abi; -use crate::codegen::callable_descriptor::{ - CallableDescriptorInvocation, CallableDescriptorShape, -}; -use crate::codegen::context::{Context, DeferredClosure, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::names::Name; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver, Stmt, StmtKind}; -use crate::span::Span; -use crate::types::{callable_wrapper_sig, first_class_callable_builtin_sig, FunctionSig, PhpType}; - -const FCC_CALLED_CLASS_ID_PARAM: &str = "__elephc_fcc_called_class_id"; -const FCC_THIS_PARAM: &str = "__elephc_fcc_this"; -const FCC_RECEIVER_PARAM: &str = "__elephc_fcc_receiver"; - -/// Returns a unique temporary name for first-class callable method receiver storage. -pub(crate) fn method_receiver_temp_name(span: Span) -> String { - format!("__elephc_fcc_receiver_{}_{}", span.line, span.col) -} - -/// Provides the Resolved static callable target helper used by the first class module. -fn resolved_static_callable_target(receiver: &StaticReceiver, ctx: &Context) -> Option { - // Resolves `self` and `parent` static receivers to their concrete class names. - // Returns `None` for `Static` receiver, which relies on late-static binding and - // cannot be resolved at compile time. - match receiver { - StaticReceiver::Named(name) => Some(StaticReceiver::Named(name.clone())), - StaticReceiver::Self_ => ctx - .current_class - .as_ref() - .map(|name| StaticReceiver::Named(Name::from(name.clone()))), - StaticReceiver::Parent => { - let current_class = ctx.current_class.as_ref()?; - let parent = ctx.classes.get(current_class)?.parent.clone()?; - Some(StaticReceiver::Named(Name::from(parent))) - } - StaticReceiver::Static => None, - } -} - -/// Provides the Static callable lookup class helper used by the first class module. -fn static_callable_lookup_class(receiver: &StaticReceiver, ctx: &Context) -> Option { - // Looks up the concrete class name for a static callable receiver. - // - `Named`: returns the explicit class name. - // - `Self_` / `Static`: returns the current class from context. - // - `Parent`: returns the parent class of the current class. - // Returns `None` when the current class is unset or the parent chain is exhausted. - match receiver { - StaticReceiver::Named(name) => Some(name.as_str().to_string()), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => { - let current_class = ctx.current_class.as_ref()?; - ctx.classes.get(current_class)?.parent.clone() - } - } -} - -/// Returns the wrapper signature for a first-class callable from a callable target, if resolvable. -pub(super) fn first_class_callable_sig(target: &CallableTarget, ctx: &Context) -> Option { - let sig = match target { - CallableTarget::Function(name) => ctx - .functions - .get(name.as_str()) - .cloned() - .or_else(|| first_class_callable_builtin_sig(name.as_str())), - CallableTarget::StaticMethod { receiver, method } => { - let class_name = static_callable_lookup_class(receiver, ctx)?; - ctx.classes - .get(&class_name) - .and_then(|class_info| class_info.static_methods.get(method)) - .cloned() - } - CallableTarget::Method { object, method } => { - let object_ty = crate::codegen::functions::infer_contextual_type(object, ctx); - let class_name = crate::codegen::functions::singular_object_class(&object_ty)?; - ctx.classes - .get(class_name) - .and_then(|class_info| class_info.methods.get(method)) - .cloned() - } - }?; - - Some(callable_wrapper_sig(&sig)) -} - -/// Builds the parameter metadata for unique hidden. -fn unique_hidden_param(base: &str, sig: &FunctionSig) -> String { - // Generates a unique hidden parameter name by appending an index suffix if `base` - // already exists in `sig.params`. Checks all existing parameter names to avoid - // collisions when multiple hidden params are needed. - if !sig.params.iter().any(|(name, _)| name == base) { - return base.to_string(); - } - let mut idx = 0usize; - loop { - let candidate = format!("{}_{}", base, idx); - if !sig.params.iter().any(|(name, _)| name == &candidate) { - return candidate; - } - idx += 1; - } -} - -/// Provides the Capture for static target helper used by the first class module. -fn capture_for_static_target(ctx: &Context) -> Option<(String, PhpType)> { - // Captures the late-static binding context for a static method first-class callable. - // Prefers `__elephc_called_class_id` (set when `static::` is used) over `this`. - // Returns `None` if neither is available in the variable scope. - if ctx.variables.contains_key("__elephc_called_class_id") { - return Some(("__elephc_called_class_id".to_string(), PhpType::Int)); - } - ctx.variables - .get("this") - .map(|var| ("this".to_string(), var.ty.clone())) -} - -/// Builds the codegen diagnostic shown when first-class callable creation -/// rejects a target. Pinpoints the specific limitation (complex method -/// receiver, missing late-static binding context, etc.) so the developer -/// understands which form is unsupported instead of seeing a generic warning. -fn unsupported_fcc_diagnostic(target: &CallableTarget) -> String { - match target { - CallableTarget::Method { object, method } => match &object.kind { - ExprKind::Variable(_) | ExprKind::This => format!( - "WARNING: unsupported first-class callable target for method ->{}() (internal: capture failed)", - method - ), - _ => format!( - "WARNING: first-class callable creation for ->{}() requires a simple receiver (\\$variable or \\$this); complex receiver expressions are not captured yet", - method - ), - }, - CallableTarget::StaticMethod { method, .. } => format!( - "WARNING: unsupported first-class callable target for static method ::{}() (late-static binding requires \\$this or __elephc_called_class_id in the enclosing frame)", - method - ), - CallableTarget::Function(name) => format!( - "WARNING: unsupported first-class callable target for function {}()", - name.as_str() - ), - } -} - -/// Provides the Capture for method receiver helper used by the first class module. -fn capture_for_method_receiver( - object: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option<(String, PhpType)> { - // Captures the receiver expression for a method first-class callable. - // - For variables and `this`, captures the existing variable directly. - // - For complex expressions, creates a temporary storage slot, emits the - // receiver expression, increments its refcount if needed, and stores the - // result to the temporary. Returns the temporary name and its inferred type. - // Returns `None` if the variable lookup or type inference fails. - match &object.kind { - ExprKind::Variable(name) => { - let ty = ctx - .variables - .get(name) - .map(|var| var.ty.clone()) - .unwrap_or_else(|| crate::codegen::functions::infer_contextual_type(object, ctx)); - Some((name.clone(), ty)) - } - ExprKind::This => ctx - .variables - .get("this") - .map(|var| ("this".to_string(), var.ty.clone())), - _ => { - let temp_name = method_receiver_temp_name(object.span); - let receiver_static_ty = crate::codegen::functions::infer_contextual_type(object, ctx); - let receiver_ty = crate::codegen::expr::emit_expr(object, emitter, ctx, data); - if receiver_ty.is_refcounted() - && super::super::expr_result_heap_ownership(object) != HeapOwnership::Owned - { - abi::emit_incref_if_refcounted(emitter, &receiver_ty); - } - let Some(temp_offset) = ctx.variables.get(&temp_name).map(|info| info.stack_offset) else { - emitter.comment(&format!( - "WARNING: missing first-class callable receiver temp ${}", - temp_name - )); - return None; - }; - abi::emit_store(emitter, &receiver_ty, temp_offset); - ctx.update_var_type_static_and_ownership( - &temp_name, - receiver_ty.codegen_repr(), - receiver_static_ty, - HeapOwnership::local_owner_for_type(&receiver_ty), - ); - Some((temp_name, receiver_ty)) - } - } -} - -/// Provides the Normalized target and captures helper used by the first class module. -fn normalized_target_and_captures( - target: &CallableTarget, - sig: &FunctionSig, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option<( - CallableTarget, - Vec<(String, PhpType, bool)>, - Vec<(String, PhpType, bool)>, -)> { - // Normalizes a callable target and computes its captures and hidden parameters. - // - For `Static` receiver with late-static binding: captures `__elephc_called_class_id` - // or `this` as the visible capture and adds a hidden param for the receiver. - // - For `Self_`/`Parent` receiver: resolves to concrete class name, no captures needed. - // - For instance method: captures the receiver variable or temporary, adds a hidden - // param to pass the captured value when the wrapper is called. - // Returns `None` if static resolution fails or receiver capture is impossible. - match target { - CallableTarget::StaticMethod { receiver, method } => match receiver { - StaticReceiver::Static => { - let capture = capture_for_static_target(ctx)?; - let hidden_name = if capture.0 == "this" { - FCC_THIS_PARAM.to_string() - } else { - FCC_CALLED_CLASS_ID_PARAM.to_string() - }; - let hidden_ty = capture.1.clone(); - Some(( - CallableTarget::StaticMethod { - receiver: StaticReceiver::Static, - method: method.clone(), - }, - vec![(capture.0, capture.1, false)], - vec![(hidden_name, hidden_ty, false)], - )) - } - _ => { - let receiver = resolved_static_callable_target(receiver, ctx)?; - Some(( - CallableTarget::StaticMethod { - receiver, - method: method.clone(), - }, - Vec::new(), - Vec::new(), - )) - } - }, - CallableTarget::Method { object, method } => { - let capture = capture_for_method_receiver(object, emitter, ctx, data)?; - let hidden_name = unique_hidden_param(FCC_RECEIVER_PARAM, sig); - let hidden_ty = capture.1.clone(); - Some(( - CallableTarget::Method { - object: Box::new(Expr::new( - ExprKind::Variable(hidden_name.clone()), - object.span, - )), - method: method.clone(), - }, - vec![(capture.0, capture.1, false)], - vec![(hidden_name, hidden_ty, false)], - )) - } - other => Some((other.clone(), Vec::new(), Vec::new())), - } -} - -/// Builds the synthetic method body for wrapper. -fn wrapper_body(target: &CallableTarget, sig: &FunctionSig) -> Vec { - // Builds the AST body for a first-class callable wrapper function. - // Creates parameter variables, forwards them as arguments to the underlying call, - // and wraps the result in a return statement. For void return types, emits an - // expression statement followed by an empty return to satisfy the ABI. - let last_param_idx = sig.params.len().saturating_sub(1); - let args: Vec = sig - .params - .iter() - .enumerate() - .map(|(idx, (name, _))| { - let var_expr = Expr::new(ExprKind::Variable(name.clone()), crate::span::Span::dummy()); - if sig.variadic.is_some() && idx == last_param_idx { - Expr::new( - ExprKind::Spread(Box::new(var_expr)), - crate::span::Span::dummy(), - ) - } else { - var_expr - } - }) - .collect(); - - let call_expr = match target { - CallableTarget::Function(name) => Expr::new( - ExprKind::FunctionCall { - name: name.clone(), - args, - }, - crate::span::Span::dummy(), - ), - CallableTarget::StaticMethod { receiver, method } => Expr::new( - ExprKind::StaticMethodCall { - receiver: receiver.clone(), - method: method.clone(), - args, - }, - crate::span::Span::dummy(), - ), - CallableTarget::Method { object, method } => Expr::new( - ExprKind::MethodCall { - object: object.clone(), - method: method.clone(), - args, - }, - crate::span::Span::dummy(), - ), - }; - - if sig.return_type == PhpType::Void { - vec![ - Stmt::new(StmtKind::ExprStmt(call_expr), crate::span::Span::dummy()), - Stmt::new(StmtKind::Return(None), crate::span::Span::dummy()), - ] - } else { - vec![Stmt::new( - StmtKind::Return(Some(call_expr)), - crate::span::Span::dummy(), - )] - } -} - -/// Builds descriptor invocation metadata for a first-class callable target. -fn descriptor_invocation_for_target( - target: &CallableTarget, - source_target: &CallableTarget, - ctx: &Context, -) -> CallableDescriptorInvocation { - match target { - CallableTarget::Function(name) => { - let function_name = name.as_str(); - let shape = if ctx.extern_functions.contains_key(function_name) { - CallableDescriptorShape::Extern - } else if !ctx.functions.contains_key(function_name) - && first_class_callable_builtin_sig(function_name).is_some() - { - CallableDescriptorShape::Builtin - } else { - CallableDescriptorShape::Function - }; - CallableDescriptorInvocation::named(shape, function_name) - } - CallableTarget::StaticMethod { receiver, method } => { - CallableDescriptorInvocation::method( - CallableDescriptorShape::StaticMethod, - static_receiver_descriptor_name(receiver), - method.as_str(), - ) - } - CallableTarget::Method { object, method } => { - let receiver_object = match source_target { - CallableTarget::Method { object, .. } => object, - _ => object, - }; - let receiver_name = crate::codegen::functions::singular_object_class( - &crate::codegen::functions::infer_contextual_type(receiver_object, ctx), - ) - .map(str::to_string); - CallableDescriptorInvocation::method( - CallableDescriptorShape::InstanceMethod, - receiver_name, - method.as_str(), - ) - } - } -} - -/// Returns the descriptor-visible receiver name for static callable metadata. -fn static_receiver_descriptor_name(receiver: &StaticReceiver) -> Option { - match receiver { - StaticReceiver::Named(name) => Some(name.as_str().to_string()), - StaticReceiver::Self_ => Some("self".to_string()), - StaticReceiver::Parent => Some("parent".to_string()), - StaticReceiver::Static => Some("static".to_string()), - } -} - -/// Emits first-class callable creation for functions, methods, and builtins. -pub(super) fn emit_first_class_callable( - target: &CallableTarget, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let Some(base_sig) = first_class_callable_sig(target, ctx) else { - emitter.comment("WARNING: unsupported first-class callable target"); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - return PhpType::Callable; - }; - let sig = ctx - .expected_first_class_callable_sig - .clone() - .unwrap_or(base_sig); - - let Some((normalized_target, captures, hidden_params)) = - normalized_target_and_captures(target, &sig, emitter, ctx, data) - else { - emitter.comment(&unsupported_fcc_diagnostic(target)); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - return PhpType::Callable; - }; - - let wrapper_label = ctx.next_label("fcc"); - let param_names: Vec = sig.params.iter().map(|(name, _)| name.clone()).collect(); - let body = wrapper_body(&normalized_target, &sig); - let descriptor_invocation = descriptor_invocation_for_target(&normalized_target, target, ctx); - - ctx.deferred_closures.push(DeferredClosure { - label: wrapper_label.clone(), - params: param_names, - body, - sig: sig.clone(), - captures: captures.clone(), - hidden_params: hidden_params.clone(), - current_class: ctx.current_class.clone(), - // Safe default: assume the wrapper is reached at runtime. The local - // assignment site downgrades this to `false` when it can prove the - // FCC value cannot escape, and `emit_variable` flips it back to `true` - // if the variable's value is read outside the short-circuit. - needed: true, - }); - - emitter.comment("first-class callable: load descriptor"); - super::descriptor_value::emit_callable_descriptor_value( - &wrapper_label, - None, - crate::codegen::callable_descriptor::CALLABLE_DESC_KIND_FIRST_CLASS, - &sig, - &captures, - &hidden_params, - descriptor_invocation, - emitter, - ctx, - data, - ); - PhpType::Callable -} diff --git a/src/codegen/expr/calls/function.rs b/src/codegen/expr/calls/function.rs deleted file mode 100644 index 7b386a2d2a..0000000000 --- a/src/codegen/expr/calls/function.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Purpose: -//! Lowers direct user-defined and builtin function calls. -//! Resolves the callable shape, prepares arguments, and leaves the call result for expression consumers. -//! -//! Called from: -//! - `crate::codegen::expr::calls` -//! -//! Key details: -//! - Callable metadata and argument signatures must stay synchronized with type checking and runtime dispatch. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::names::function_symbol; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; - -use super::args; - -/// Emits a direct user-defined or builtin function call. -/// -/// Saves concat offset on x86_64 before nested calls, prepares arguments via -/// `emit_pushed_call_args`, materializes outgoing ABI arguments, emits the call -/// label, and restores concat offset for string returns. Returns the function's -/// return type from the signature lookup, or `PhpType::Void` if not found. -pub(super) fn emit_function_call( - name: &str, - args_exprs: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("call {}()", name)); - - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - - let sig = ctx.functions.get(name).cloned(); - if let Some(sig) = sig.as_ref() { - specialize_callable_arguments(name, args_exprs, sig, ctx); - } - let emitted_args = args::emit_pushed_call_args( - args_exprs, - sig.as_ref(), - args::regular_param_count(sig.as_ref(), args_exprs.len()), - "ref arg", - false, - true, - emitter, - ctx, - data, - ); - let arg_types = emitted_args.arg_types; - - let assignments = - crate::codegen::abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - let overflow_bytes = crate::codegen::abi::materialize_outgoing_args(emitter, &assignments); - - let ret_ty = ctx - .functions - .get(name) - .map(|sig| sig.return_type.clone()) - .unwrap_or(PhpType::Void); - - if !save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - crate::codegen::abi::emit_call_label(emitter, &function_symbol(name)); - if save_concat_before_args { - crate::codegen::abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - if ret_ty == PhpType::Str { - super::super::restore_concat_offset_after_owned_string_call(emitter, ctx); - } else { - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } - } else { - if ret_ty == PhpType::Str { - super::super::restore_concat_offset_after_owned_string_call(emitter, ctx); - } else { - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } - crate::codegen::abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - } - - ret_ty -} - -/// For each argument whose corresponding parameter has type `PhpType::Callable`, looks -/// up the concrete callable signature from `callable_param_sigs` and registers it on -/// the argument expression tree so the callee can be specialized at emit time. -/// -/// Iterates positional and named arguments, skipping spreads, maps them to -/// parameter indices, and calls `specialize_callable_expr` for eligible arguments. -fn specialize_callable_arguments( - function_name: &str, - args_exprs: &[Expr], - sig: &FunctionSig, - ctx: &mut Context, -) { - let mut positional_idx = 0usize; - for arg in args_exprs { - let (param_idx, value) = match &arg.kind { - ExprKind::NamedArg { name, value } => { - let Some(param_idx) = sig - .params - .iter() - .position(|(param_name, _)| param_name == name) - else { - continue; - }; - (param_idx, value.as_ref()) - } - ExprKind::Spread(_) => { - continue; - } - _ => { - let param_idx = positional_idx; - positional_idx += 1; - (param_idx, arg) - } - }; - let Some((param_name, param_ty)) = sig.params.get(param_idx) else { - continue; - }; - if param_ty != &PhpType::Callable { - continue; - } - if let Some(callable_sig) = ctx - .callable_param_sigs - .get(&(function_name.to_string(), param_name.clone())) - .cloned() - { - specialize_callable_expr(value, &callable_sig, ctx); - } - } -} - -/// Recursively traverses `expr` to find the storage location that will hold a -/// callable and delegates to `specialize_callable_var`. -/// -/// Handles `Variable`, `ArrayAccess` (when the array is a `Variable`), and -/// `Assignment` (traverses the value side). Other expression kinds are ignored. -fn specialize_callable_expr(expr: &Expr, callable_sig: &FunctionSig, ctx: &mut Context) { - match &expr.kind { - ExprKind::Variable(name) => specialize_callable_var(name, callable_sig, ctx), - ExprKind::ArrayAccess { array, .. } => { - if let ExprKind::Variable(name) = &array.kind { - specialize_callable_var(name, callable_sig, ctx); - } - } - ExprKind::Assignment { value, .. } => specialize_callable_expr(value, callable_sig, ctx), - _ => {} - } -} - -/// Associates `callable_sig` with the variable `name` in `ctx.closure_sigs`, then -/// updates any previously deferred closures with matching parameter signatures and -/// closure captures to use the new signature. -/// -/// This propagates callable type information forward through deferred closure -/// bodies that were already queued before the concrete signature was known. -fn specialize_callable_var(name: &str, callable_sig: &FunctionSig, ctx: &mut Context) { - let previous_sig = ctx - .closure_sigs - .insert(name.to_string(), callable_sig.clone()); - let Some(previous_sig) = previous_sig else { - return; - }; - let captures = ctx.closure_captures.get(name).cloned().unwrap_or_default(); - for deferred in ctx.deferred_closures.iter_mut().rev() { - if deferred.sig.params == previous_sig.params && deferred.captures == captures { - deferred.sig = callable_sig.clone(); - break; - } - } -} diff --git a/src/codegen/expr/calls/indirect.rs b/src/codegen/expr/calls/indirect.rs deleted file mode 100644 index 978ed81231..0000000000 --- a/src/codegen/expr/calls/indirect.rs +++ /dev/null @@ -1,599 +0,0 @@ -//! Purpose: -//! Lowers variable and callable-indirect invocation paths. -//! Resolves the callable shape, prepares arguments, and leaves the call result for expression consumers. -//! -//! Called from: -//! - `crate::codegen::expr::calls` -//! -//! Key details: -//! - Callable metadata and argument signatures must stay synchronized with type checking and runtime dispatch. -//! - Once a callable expression has produced a descriptor pointer, hidden capture values must come -//! from that descriptor instead of from the caller's current lexical variables. - -use crate::codegen::builtins::arrays::call_user_func_array::{self, LoadedArraySource}; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver}; -use crate::types::PhpType; - -use super::{args, descriptor_invoker_args}; - -/// Emits a callable-indirect call where the callee expression has already been evaluated -/// and placed in the result register. Handles `__invoke` on objects, closure captures as -/// hidden arguments, and ABI-compliant argument materialization on x86_64. -/// -/// - On x86_64: saves the result register to the stack before args, then restores it after -/// to work around the limited argument-passing registers. -/// - If the callee is a known class with `__invoke`, delegates to method call codegen. -/// - Otherwise: resolves the signature from `ctx.closure_sigs` or infers from closure AST, -/// pushes arguments, and emits the call via `nested_call_reg`. -/// -/// Returns the PHP return type of the callee (inferred from signature or closure return annotation). -pub(super) fn emit_loaded_expr_call( - callee: &Expr, - args_exprs: &[Expr], - loaded_callee_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("call loaded expression result"); - if let Some(class_name) = - crate::codegen::functions::singular_object_class(loaded_callee_ty).map(str::to_string) - { - if ctx - .classes - .get(&class_name) - .is_some_and(|class_info| class_info.methods.contains_key("__invoke")) - { - if let ExprKind::Variable(var_name) = &callee.kind { - if let Some(ret_ty) = super::emit_invokable_object_variable_call( - var_name, - &class_name, - args_exprs, - emitter, - ctx, - data, - ) { - return ret_ty; - } - } - if let Some(ret_ty) = emit_loaded_invokable_object_descriptor_call( - callee, - &class_name, - args_exprs, - loaded_callee_ty, - emitter, - ctx, - data, - ) { - return ret_ty; - } - if matches!(loaded_callee_ty.codegen_repr(), PhpType::Mixed) { - crate::codegen::expr::objects::emit_unbox_mixed_object_or_fatal( - b"Fatal error: Value of type null is not callable\n", - emitter, - ctx, - data, - ); - } - crate::codegen::abi::emit_push_reg( - emitter, - crate::codegen::abi::int_result_reg(emitter), - ); // save the loaded invokable object below later method arguments - let sig = ctx - .classes - .get(&class_name) - .and_then(|class_info| class_info.methods.get("__invoke")) - .cloned(); - let emitted_args = crate::codegen::expr::objects::emit_pushed_method_args( - args_exprs, - sig.as_ref(), - emitter, - ctx, - data, - ); - return crate::codegen::expr::objects::emit_method_call_with_saved_receiver_below_args( - &class_name, - "__invoke", - &emitted_args.arg_types, - emitted_args.source_temp_bytes, - emitter, - ctx, - ); - } - } - if matches!(loaded_callee_ty.codegen_repr(), PhpType::Str) { - return super::emit_loaded_runtime_string_call(args_exprs, callee.span, emitter, ctx, data); - } - if let ExprKind::Variable(var_name) = &callee.kind { - if let Some(ret_ty) = - super::emit_callable_array_variable_call(var_name, args_exprs, emitter, ctx, data) - { - return ret_ty; - } - } - if expr_call_needs_descriptor_invoker(callee, loaded_callee_ty, ctx) { - if let Some(ret_ty) = - emit_descriptor_invoker_expr_call(callee, args_exprs, emitter, ctx, data) - { - return ret_ty; - } - } - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - - let callee_sig = callee_sig_for_expr(callee, ctx); - let captures = crate::codegen::callables::callable_captures(callee, ctx); - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // save the already-evaluated callable below later arguments - - let emitted_args = args::emit_pushed_call_args( - args_exprs, - callee_sig.as_ref(), - args::regular_param_count(callee_sig.as_ref(), args_exprs.len()), - "indirect ref arg", - true, - false, - emitter, - ctx, - data, - ); - let mut arg_types = emitted_args.arg_types; - respecialize_indirect_callable_signature(callee, callee_sig.as_ref(), &captures, &arg_types, ctx); - - let call_reg = crate::codegen::abi::nested_call_reg(emitter); - let arg_temp_bytes = args::pushed_temp_bytes(&arg_types) + emitted_args.source_temp_bytes; - crate::codegen::abi::emit_load_temporary_stack_slot(emitter, call_reg, arg_temp_bytes); - push_descriptor_captures_as_hidden_args(&captures, emitter, call_reg, &mut arg_types); - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - call_reg, - call_reg, - ); - - let assignments = - crate::codegen::abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - let overflow_bytes = crate::codegen::abi::materialize_outgoing_args(emitter, &assignments); - - let ret_ty = callee_sig - .as_ref() - .map(|sig| sig.return_type.clone()) - .unwrap_or_else(|| match &callee.kind { - ExprKind::Closure { - return_type: Some(type_ann), - .. - } => crate::codegen::functions::codegen_static_type(type_ann, ctx), - ExprKind::Closure { body, .. } => { - crate::types::checker::infer_return_type_syntactic(body) - } - _ => PhpType::Int, - }); - - if save_concat_before_args { - crate::codegen::abi::emit_call_reg(emitter, call_reg); - crate::codegen::abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, 16); - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } else { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - crate::codegen::abi::emit_call_reg(emitter, call_reg); - super::super::restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - crate::codegen::abi::emit_release_temporary_stack(emitter, overflow_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); - crate::codegen::abi::emit_release_temporary_stack(emitter, 16); - } - - ret_ty -} - -/// Emits a direct expression call through the callable descriptor's uniform invoker. -/// -/// This is used when the callee expression can select a captured callable branch at -/// runtime. In that shape, captures and receiver environments live in the descriptor, -/// so direct ABI calls cannot reconstruct the hidden arguments safely. -fn emit_descriptor_invoker_expr_call( - callee: &Expr, - args_exprs: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let ownership = crate::codegen::expr::expr_result_heap_ownership(callee); - if !matches!(ownership, HeapOwnership::Owned | HeapOwnership::Borrowed) { - return None; - } - - let concat_saved_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if concat_saved_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - if matches!(ownership, HeapOwnership::Borrowed) { - crate::codegen::callable_descriptor::emit_retain_current_descriptor(emitter); - } - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the callable descriptor while building direct-call arguments - - let callee_sig = callee_sig_for_expr(callee, ctx); - let arr_ty = descriptor_invoker_args::emit_descriptor_invoker_arg_array( - args_exprs, - callee_sig.as_ref(), - callee.span, - emitter, - ctx, - data, - ); - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the owned descriptor-invoker argument array - - let call_reg = crate::codegen::abi::nested_call_reg(emitter); - crate::codegen::abi::emit_load_temporary_stack_slot(emitter, call_reg, 16); - call_user_func_array::emit_call_descriptor_array_invoker( - LoadedArraySource::TemporaryStackSlot(0), - &arr_ty, - call_reg, - concat_saved_before_args, - emitter, - ctx, - data, - ); - release_preserved_expr_call_arg_array_after_mixed_result(&arr_ty, emitter); - release_preserved_expr_call_descriptor_after_mixed_result(emitter); - Some(PhpType::Mixed) -} - -/// Emits an already-loaded invokable object expression through the descriptor invoker. -/// -/// The callee object is on the result register when this function starts. It is -/// saved as descriptor slot zero, visible arguments are evaluated afterward, and -/// the object-invoke descriptor applies named/default/by-reference metadata. -fn emit_loaded_invokable_object_descriptor_call( - callee: &Expr, - class_name: &str, - args_exprs: &[Expr], - loaded_callee_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let case = crate::codegen::callable_dispatch::runtime_instance_method_case( - ctx, - data, - class_name, - "__invoke", - crate::codegen::callable_dispatch::RuntimeInstanceCallableShape::ObjectInvoke, - )?; - if !case.has_invoker { - return None; - } - - emitter.comment("call loaded invokable object descriptor"); - let save_concat_before_args = - emitter.target.arch == crate::codegen::platform::Arch::X86_64; - if save_concat_before_args { - super::super::save_concat_offset_before_nested_call(emitter, ctx); - } - - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the loaded invokable object below later descriptor arguments - let arr_ty = descriptor_invoker_args::emit_descriptor_invoker_arg_array_with_saved_object_prefix( - 0, - args_exprs, - Some(&case.sig), - callee.span, - emitter, - ctx, - data, - ); - let call_reg = crate::codegen::abi::nested_call_reg(emitter); - crate::codegen::abi::emit_symbol_address(emitter, call_reg, &case.descriptor_label); - call_user_func_array::emit_call_descriptor_array_invoker( - LoadedArraySource::Result, - &arr_ty, - call_reg, - save_concat_before_args, - emitter, - ctx, - data, - ); - release_preserved_loaded_invokable_object_after_mixed_result( - callee, - loaded_callee_ty, - emitter, - ); - Some(PhpType::Mixed) -} - -/// Releases the saved invokable object expression while preserving the boxed call result. -fn release_preserved_loaded_invokable_object_after_mixed_result( - callee: &Expr, - loaded_callee_ty: &PhpType, - emitter: &mut Emitter, -) { - if crate::codegen::expr::expr_result_heap_ownership(callee) == HeapOwnership::Owned { - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the boxed call result while releasing the loaded invokable object - crate::codegen::abi::emit_load_temporary_stack_slot( - emitter, - crate::codegen::abi::int_result_reg(emitter), - 16, - ); - crate::codegen::abi::emit_decref_if_refcounted(emitter, loaded_callee_ty); - crate::codegen::abi::emit_pop_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // restore the boxed call result after object cleanup - } - crate::codegen::abi::emit_release_temporary_stack(emitter, 16); // discard the saved loaded invokable object slot -} - -/// Releases the synthetic direct-call argument array while preserving the Mixed result. -fn release_preserved_expr_call_arg_array_after_mixed_result( - arr_ty: &PhpType, - emitter: &mut Emitter, -) { - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the boxed call result while releasing the argument array - crate::codegen::abi::emit_load_temporary_stack_slot( - emitter, - crate::codegen::abi::int_result_reg(emitter), - 16, - ); - crate::codegen::abi::emit_decref_if_refcounted(emitter, arr_ty); - crate::codegen::abi::emit_pop_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // restore the boxed call result after argument-array cleanup - crate::codegen::abi::emit_release_temporary_stack(emitter, 16); // discard the preserved argument-array slot -} - -/// Releases the retained callable descriptor while preserving the Mixed result. -fn release_preserved_expr_call_descriptor_after_mixed_result(emitter: &mut Emitter) { - crate::codegen::abi::emit_push_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // preserve the boxed call result while releasing the callable descriptor - crate::codegen::abi::emit_load_temporary_stack_slot( - emitter, - crate::codegen::abi::int_result_reg(emitter), - 16, - ); - crate::codegen::callable_descriptor::emit_release_current_descriptor(emitter); - crate::codegen::abi::emit_pop_reg(emitter, crate::codegen::abi::int_result_reg(emitter)); // restore the boxed call result after descriptor cleanup - crate::codegen::abi::emit_release_temporary_stack(emitter, 16); // discard the preserved callable descriptor slot -} - -/// Resolves the function signature for a callee expression in an indirect call context. -/// -/// Looks up the signature in `ctx.closure_sigs` for `Variable` and `ArrayAccess` nodes -/// (where the array is a variable, e.g., `$arr()`). For `FirstClassCallable`, delegates to -/// `first_class_callable_sig`. Returns `None` for unsupported expression kinds, in which -/// case the caller defaults to `PhpType::Int`. -fn callee_sig_for_expr( - callee: &Expr, - ctx: &Context, -) -> Option { - if let Some(sig) = crate::codegen::callables::callable_sig(callee, ctx) { - return Some(sig); - } - match &callee.kind { - ExprKind::Closure { .. } => ctx.deferred_closures.last().map(|closure| closure.sig.clone()), - ExprKind::Assignment { value, .. } => callee_sig_for_expr(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => matching_expr_call_branch_sig(then_expr, else_expr, ctx), - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - matching_expr_call_branch_sig(value, default, ctx) - } - _ => None, - } -} - -/// Returns a branch callable signature only when both branches have the same contract. -fn matching_expr_call_branch_sig( - left: &Expr, - right: &Expr, - ctx: &Context, -) -> Option { - let left_sig = callee_sig_for_expr(left, ctx)?; - let right_sig = callee_sig_for_expr(right, ctx)?; - if left_sig == right_sig { - Some(left_sig) - } else { - None - } -} - -/// Returns true when a direct expression call needs descriptor-owned metadata. -fn expr_call_needs_descriptor_invoker( - callee: &Expr, - loaded_callee_ty: &PhpType, - ctx: &Context, -) -> bool { - if unknown_callable_value_needs_descriptor_invoker(callee, loaded_callee_ty, ctx) { - return true; - } - - match &callee.kind { - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) | ExprKind::Variable(_) => false, - ExprKind::Assignment { value, .. } => expr_produces_captured_callable(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => { - expr_produces_captured_callable(then_expr, ctx) - || expr_produces_captured_callable(else_expr, ctx) - } - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - expr_produces_captured_callable(value, ctx) - || expr_produces_captured_callable(default, ctx) - } - _ => false, - } -} - -/// Returns true for callable values whose signature/capture environment is only known at runtime. -fn unknown_callable_value_needs_descriptor_invoker( - callee: &Expr, - loaded_callee_ty: &PhpType, - ctx: &Context, -) -> bool { - if !matches!(loaded_callee_ty.codegen_repr(), PhpType::Callable) { - return false; - } - match &callee.kind { - ExprKind::Variable(name) => { - ctx.runtime_callable_vars.contains(name) || callee_sig_for_expr(callee, ctx).is_none() - } - ExprKind::ArrayAccess { .. } - | ExprKind::PropertyAccess { .. } - | ExprKind::DynamicPropertyAccess { .. } - | ExprKind::StaticPropertyAccess { .. } - | ExprKind::Assignment { .. } - | ExprKind::Ternary { .. } - | ExprKind::ShortTernary { .. } - | ExprKind::NullCoalesce { .. } - | ExprKind::FunctionCall { .. } - | ExprKind::MethodCall { .. } - | ExprKind::StaticMethodCall { .. } - | ExprKind::ExprCall { .. } => true, - _ => false, - } -} - -/// Returns true if an expression produces a callable with descriptor-owned environment. -fn expr_produces_captured_callable(expr: &Expr, ctx: &Context) -> bool { - match &expr.kind { - ExprKind::Closure { captures, .. } => !captures.is_empty(), - ExprKind::FirstClassCallable(target) => first_class_target_needs_runtime_capture(target), - ExprKind::Variable(name) => { - ctx.closure_captures - .get(name) - .is_some_and(|captures| !captures.is_empty()) - || ctx - .first_class_callable_targets - .get(name) - .is_some_and(first_class_target_needs_runtime_capture) - } - ExprKind::Assignment { value, .. } => expr_produces_captured_callable(value, ctx), - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => { - expr_produces_captured_callable(then_expr, ctx) - || expr_produces_captured_callable(else_expr, ctx) - } - ExprKind::ShortTernary { value, default } - | ExprKind::NullCoalesce { value, default } => { - expr_produces_captured_callable(value, ctx) - || expr_produces_captured_callable(default, ctx) - } - _ => false, - } -} - -/// Returns true when a first-class callable target carries receiver environment. -fn first_class_target_needs_runtime_capture(target: &CallableTarget) -> bool { - matches!( - target, - CallableTarget::Method { .. } - | CallableTarget::StaticMethod { - receiver: StaticReceiver::Static, - .. - } - ) -} - -/// Updates stored callable signatures after argument emission reveals concrete types. -fn respecialize_indirect_callable_signature( - callee: &Expr, - callee_sig: Option<&crate::types::FunctionSig>, - captures: &[(String, PhpType, bool)], - arg_types: &[PhpType], - ctx: &mut Context, -) { - let Some(cached_sig) = callee_sig.cloned() else { - return; - }; - let Some(storage_name) = callable_signature_storage_name(callee) else { - return; - }; - - for deferred in &mut ctx.deferred_closures { - if deferred.sig.params == cached_sig.params && deferred.captures == captures { - for (i, ty) in arg_types.iter().enumerate() { - if i < deferred.sig.params.len() - && !deferred - .sig - .declared_params - .get(i) - .copied() - .unwrap_or(false) - && !deferred.sig.ref_params.get(i).copied().unwrap_or(false) - { - deferred.sig.params[i].1 = ty.clone(); - } - } - break; - } - } - - if let Some(cached) = ctx.closure_sigs.get_mut(storage_name) { - for (i, ty) in arg_types.iter().enumerate() { - if i < cached.params.len() - && !cached.declared_params.get(i).copied().unwrap_or(false) - && !cached.ref_params.get(i).copied().unwrap_or(false) - { - cached.params[i].1 = ty.clone(); - } - } - } -} - -/// Returns the context metadata key for a callable expression, when one exists. -fn callable_signature_storage_name(callee: &Expr) -> Option<&str> { - match &callee.kind { - ExprKind::Variable(name) => Some(name.as_str()), - ExprKind::ArrayAccess { array, .. } => { - if let ExprKind::Variable(name) = &array.kind { - Some(name.as_str()) - } else { - None - } - } - _ => None, - } -} - -/// Pushes hidden capture arguments from a materialized callable descriptor. -/// -/// The indirect callee has already evaluated to a descriptor pointer. Reading -/// captures from descriptor slots preserves by-value snapshot semantics for -/// callables stored in arrays, returned from expressions, or parenthesized -/// before invocation. -fn push_descriptor_captures_as_hidden_args( - captures: &[(String, PhpType, bool)], - emitter: &mut Emitter, - descriptor_reg: &str, - arg_types: &mut Vec, -) { - for (idx, (capture_name, capture_ty, by_ref)) in captures.iter().enumerate() { - emitter.comment(&format!("push callable capture ${}", capture_name)); - if *by_ref { - crate::codegen::callable_descriptor::emit_load_runtime_capture_to_result( - emitter, - descriptor_reg, - idx, - &PhpType::Int, - ); - args::push_arg_value(emitter, &PhpType::Int); - arg_types.push(PhpType::Int); - } else { - crate::codegen::callable_descriptor::emit_load_runtime_capture_to_result( - emitter, - descriptor_reg, - idx, - capture_ty, - ); - args::push_arg_value(emitter, capture_ty); - arg_types.push(capture_ty.clone()); - } - } -} diff --git a/src/codegen/expr/calls/pipe.rs b/src/codegen/expr/calls/pipe.rs deleted file mode 100644 index 9e9317a042..0000000000 --- a/src/codegen/expr/calls/pipe.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Purpose: -//! Lowers the PHP 8.5 pipe operator (`value |> callable`) into the equivalent direct call. -//! Delegates to the most specific existing call emitter based on the static shape of the RHS. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` via `super::calls::emit_pipe`. -//! -//! Key details: -//! - The synthesized call carries the pipe operator's span so diagnostics point at `|>`. -//! - Argument planning, ABI materialization, and ownership are handled by the downstream emitter; this module does not duplicate that logic. - -use crate::parser::ast::{CallableTarget, Expr, ExprKind}; -use crate::span::Span; -use crate::types::PhpType; - -use super::super::super::context::Context; -use super::super::super::data_section::DataSection; -use super::super::super::emit::Emitter; - -/// Lowers `value |> callable` into a direct call by first storing `value` to a temporary, -/// then synthesizing a call expression where the temporary is the sole argument to `callable`. -/// The synthesized call carries `span` so diagnostics point at the `|>` token. -/// Delegates to `emit_expr` for ABI materialization and ownership handling of the resulting call. -pub(super) fn emit_pipe( - value: &Expr, - callable: &Expr, - span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let temp_name = super::pipe_value_temp_name(span); - crate::codegen::stmt::emit_assign_stmt(&temp_name, value, emitter, ctx, data); - let temp_value = Expr::new(ExprKind::Variable(temp_name), value.span); - let synth_args = vec![temp_value]; - let synthetic = match &callable.kind { - ExprKind::FirstClassCallable(CallableTarget::Function(name)) => Expr::new( - ExprKind::FunctionCall { - name: name.clone(), - args: synth_args, - }, - span, - ), - ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { - Expr::new( - ExprKind::StaticMethodCall { - receiver: receiver.clone(), - method: method.clone(), - args: synth_args, - }, - span, - ) - } - ExprKind::FirstClassCallable(CallableTarget::Method { object, method }) => Expr::new( - ExprKind::MethodCall { - object: object.clone(), - method: method.clone(), - args: synth_args, - }, - span, - ), - ExprKind::Variable(var) => Expr::new( - ExprKind::ClosureCall { - var: var.clone(), - args: synth_args, - }, - span, - ), - _ => Expr::new( - ExprKind::ExprCall { - callee: Box::new(callable.clone()), - args: synth_args, - }, - span, - ), - }; - super::super::emit_expr(&synthetic, emitter, ctx, data) -} diff --git a/src/codegen/expr/chains.rs b/src/codegen/expr/chains.rs deleted file mode 100644 index c9a6226c75..0000000000 --- a/src/codegen/expr/chains.rs +++ /dev/null @@ -1,520 +0,0 @@ -//! Purpose: -//! Lowers chained property, method, and array accesses that share a left-to-right receiver path. -//! Maintains intermediate receiver values while walking nested PHP access expressions. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Each chain step must preserve nullability, ownership, and side-effect order for subsequent steps. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::functions; -use crate::codegen::platform::Arch; -use crate::names::php_symbol_key; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::PhpType; - -use super::{arrays, calls, objects}; - -/// Represents a flattened left-to-right chain of property, method, array, and callable segments. -/// The `base` is the leftmost receiver; segments are ordered from innermost to outermost access. -struct Chain<'a> { - /// The leftmost expression in the chain (e.g., `$obj` in `$obj->foo()->bar`). - base: &'a Expr, - /// Ordered list of access segments from innermost to outermost. - segments: Vec>, -} - -/// A single step in a postfix access chain (property, method, array, or callable). -enum Segment<'a> { - /// Ordinary (`->property`) or nullsafe (`?->property`) property access. - Property { - /// The full expression for this segment. - expr: &'a Expr, - /// The receiver expression on the left side of the access. - receiver: &'a Expr, - /// The property name as a string. - property: &'a str, - /// Whether this is a nullsafe (`?.`) access. - nullsafe: bool, - }, - /// Ordinary (`->method()`) or nullsafe (`?->method()`) method call. - Method { - /// The full expression for this segment. - expr: &'a Expr, - /// The receiver expression on the left side of the call. - receiver: &'a Expr, - /// The method name as a string. - method: &'a str, - /// The call arguments. - args: &'a [Expr], - /// Whether this is a nullsafe (`?.`) call. - nullsafe: bool, - }, - /// Array element access (`$arr[$idx]`). - Array { - /// The full expression for this segment. - expr: &'a Expr, - /// The array expression on the left side of the access. - array: &'a Expr, - /// The index expression. - index: &'a Expr, - }, - /// Expression-callable invocation (`$fn($args)`). - ExprCall { - /// The full expression for this segment. - expr: &'a Expr, - /// The callee expression (must resolve to a callable). - callee: &'a Expr, - /// The call arguments. - args: &'a [Expr], - }, -} - -/// Emits a postfix chain that contains at least one nullsafe (`?.`) property or method access. -pub(super) fn emit_nullsafe_postfix_chain( - expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - let chain = flatten_postfix_chain(expr)?; - if !chain.segments.iter().any(Segment::is_nullsafe_member) { - return None; - } - - emitter.comment("nullsafe postfix chain"); - let null_label = ctx.next_label("nullsafe_chain_null"); - let done_label = ctx.next_label("nullsafe_chain_done"); - let mut current_ty = super::emit_expr(chain.base, emitter, ctx, data); - let mut chain_can_short_circuit = false; - let mut normal_path_terminated = false; - - for segment in chain.segments { - if normal_path_terminated { - break; - } - match segment { - Segment::Property { - expr, - receiver, - property, - nullsafe, - } => { - let outcome = emit_property_segment( - expr, - receiver, - property, - nullsafe, - ¤t_ty, - &null_label, - emitter, - ctx, - data, - ); - current_ty = outcome.ty; - chain_can_short_circuit |= outcome.can_short_circuit; - normal_path_terminated |= outcome.terminated_normal_path; - } - Segment::Method { - expr, - receiver, - method, - args, - nullsafe, - } => { - let outcome = emit_method_segment( - expr, - receiver, - method, - args, - nullsafe, - ¤t_ty, - &null_label, - emitter, - ctx, - data, - ); - current_ty = outcome.ty; - chain_can_short_circuit |= outcome.can_short_circuit; - normal_path_terminated |= outcome.terminated_normal_path; - } - Segment::Array { expr, array, index } => { - let _ = (expr, array); - current_ty = arrays::emit_array_access_with_loaded_base( - ¤t_ty, - index, - emitter, - ctx, - data, - true, - ); - } - Segment::ExprCall { expr, callee, args } => { - let _ = expr; - current_ty = - calls::emit_loaded_expr_call(callee, args, ¤t_ty, emitter, ctx, data); - } - } - } - - if chain_can_short_circuit { - objects::box_nullable_result(¤t_ty, emitter); - abi::emit_jump(emitter, &done_label); - emitter.label(&null_label); - objects::emit_boxed_null(emitter); - emitter.label(&done_label); - Some(PhpType::Mixed) - } else { - Some(current_ty) - } -} - -/// Outcome of emitting a single chain segment, used to track type, short-circuiting, and control flow. -struct SegmentOutcome { - /// The PHP type produced by this segment. - ty: PhpType, - /// Whether this segment can short-circuit (null-propagate) the chain. - can_short_circuit: bool, - /// Whether the normal (non-null) path is guaranteed to terminate (e.g., always returns null). - terminated_normal_path: bool, -} - -impl SegmentOutcome { - /// Creates a normal (non-terminating) outcome with the given type and short-circuit flag. - fn normal(ty: PhpType, can_short_circuit: bool) -> Self { - Self { - ty, - can_short_circuit, - terminated_normal_path: false, - } - } - - /// Creates an outcome indicating the normal path always yields null. - /// The chain will short-circuit and `terminated_normal_path` is set to true. - fn always_null() -> Self { - Self { - ty: PhpType::Void, - can_short_circuit: true, - terminated_normal_path: true, - } - } -} - -impl Segment<'_> { - /// Returns true if this segment is a nullsafe property or method access. - fn is_nullsafe_member(&self) -> bool { - matches!( - self, - Segment::Property { - nullsafe: true, - .. - } | Segment::Method { - nullsafe: true, - .. - } - ) - } -} - -/// Flattens a postfix expression tree into a `Chain` by walking from the outermost access -/// inward, collecting property, method, array, and callable segments. Returns `None` if the -/// expression contains no postfix accesses. Segments are stored in reverse order (innermost -/// first) and reversed after the walk so they are ordered from base to outermost. -fn flatten_postfix_chain(expr: &Expr) -> Option> { - let mut base = expr; - let mut segments = Vec::new(); - - loop { - match &base.kind { - ExprKind::PropertyAccess { object, property } => { - segments.push(Segment::Property { - expr: base, - receiver: object, - property, - nullsafe: false, - }); - base = object; - } - ExprKind::NullsafePropertyAccess { object, property } => { - segments.push(Segment::Property { - expr: base, - receiver: object, - property, - nullsafe: true, - }); - base = object; - } - ExprKind::MethodCall { - object, - method, - args, - } => { - segments.push(Segment::Method { - expr: base, - receiver: object, - method, - args, - nullsafe: false, - }); - base = object; - } - ExprKind::NullsafeMethodCall { - object, - method, - args, - } => { - segments.push(Segment::Method { - expr: base, - receiver: object, - method, - args, - nullsafe: true, - }); - base = object; - } - ExprKind::ArrayAccess { array, index } => { - segments.push(Segment::Array { - expr: base, - array, - index, - }); - base = array; - } - ExprKind::ExprCall { callee, args } => { - segments.push(Segment::ExprCall { - expr: base, - callee, - args, - }); - base = callee; - } - _ => break, - } - } - - if segments.is_empty() { - return None; - } - - segments.reverse(); - Some(Chain { base, segments }) -} - -/// Emits a single property-access segment, handling both ordinary and nullsafe (`?.`) forms. -/// Updates the current type, short-circuit flag, and normal-path termination flag in the -/// returned `SegmentOutcome`. May emit a jump to `null_label` for nullsafe chains when the -/// receiver is statically null or the access fails at runtime. -fn emit_property_segment( - expr: &Expr, - receiver: &Expr, - property: &str, - nullsafe: bool, - current_ty: &PhpType, - null_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> SegmentOutcome { - let receiver_ty = functions::infer_contextual_type(receiver, ctx); - if nullsafe { - if receiver_is_static_null(current_ty, &receiver_ty) { - abi::emit_jump(emitter, null_label); - return SegmentOutcome::always_null(); - } - let Some(class_name) = singular_receiver_class(&receiver_ty, current_ty) else { - if matches!(current_ty.codegen_repr(), PhpType::Mixed) { - let ty = objects::emit_mixed_property_access(property, emitter, ctx, data); - return SegmentOutcome::normal(ty, false); - } - emitter.comment("WARNING: nullsafe property access on non-object"); - abi::emit_jump(emitter, null_label); - return SegmentOutcome::always_null(); - }; - let can_short_circuit = - emit_nullsafe_receiver_check(current_ty, null_label, emitter); - let property_ty = objects::emit_loaded_object_property_access( - &class_name, - property, - emitter, - ctx, - data, - ); - return SegmentOutcome::normal(property_ty, can_short_circuit); - } - - let Some(class_name) = singular_receiver_class(&receiver_ty, current_ty) else { - if matches!(current_ty.codegen_repr(), PhpType::Mixed) { - let ty = objects::emit_mixed_property_access(property, emitter, ctx, data); - return SegmentOutcome::normal(ty, false); - } - emitter.comment("WARNING: property access on non-object"); - return SegmentOutcome::normal(functions::infer_contextual_type(expr, ctx), false); - }; - if matches!(current_ty.codegen_repr(), PhpType::Mixed) { - let ty = objects::emit_nullable_object_property_access( - &class_name, - property, - emitter, - ctx, - data, - ); - SegmentOutcome::normal(ty, false) - } else { - let ty = objects::emit_loaded_object_property_access( - &class_name, - property, - emitter, - ctx, - data, - ); - SegmentOutcome::normal(ty, false) - } -} - -/// Emits a single method-call segment, handling both ordinary and nullsafe (`?.`) forms. -/// On nullsafe calls with a statically-null receiver, jumps to `null_label`. For ordinary -/// calls on `Mixed` types, emits a fatal error if the receiver is null. Returns the method's -/// return type and short-circuit flag in `SegmentOutcome`. -fn emit_method_segment( - expr: &Expr, - receiver: &Expr, - method: &str, - args: &[Expr], - nullsafe: bool, - current_ty: &PhpType, - null_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> SegmentOutcome { - let receiver_ty = functions::infer_contextual_type(receiver, ctx); - if nullsafe { - if receiver_is_static_null(current_ty, &receiver_ty) { - abi::emit_jump(emitter, null_label); - return SegmentOutcome::always_null(); - } - let Some(class_name) = singular_receiver_class(&receiver_ty, current_ty) else { - emitter.comment("WARNING: nullsafe method call on non-object"); - abi::emit_jump(emitter, null_label); - return SegmentOutcome::always_null(); - }; - let can_short_circuit = - emit_nullsafe_receiver_check(current_ty, null_label, emitter); - let return_ty = - emit_loaded_method_call(&class_name, method, args, emitter, ctx, data); - return SegmentOutcome::normal(return_ty, can_short_circuit); - } - - let Some(class_name) = singular_receiver_class(&receiver_ty, current_ty) else { - emitter.comment("WARNING: method call on non-object"); - return SegmentOutcome::normal(functions::infer_contextual_type(expr, ctx), false); - }; - if matches!(current_ty.codegen_repr(), PhpType::Mixed) { - let message = format!( - "Fatal error: Call to a member function {}() on null\n", - method - ); - objects::emit_unbox_mixed_object_or_fatal(message.as_bytes(), emitter, ctx, data); - } - let return_ty = emit_loaded_method_call(&class_name, method, args, emitter, ctx, data); - SegmentOutcome::normal(return_ty, false) -} - -/// Emits a method call where the receiver is already loaded on the ABI's result register. -/// Saves the receiver register before emitting arguments, then dispatches to the method. -/// Falls back to `__call` if the method is not defined, passing the original method name as -/// the first magic argument. Returns the method's return type. -fn emit_loaded_method_call( - class_name: &str, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the loaded receiver below later method arguments - let method_key = php_symbol_key(method); - let mut dispatch_method = method_key.as_str(); - let mut magic_args = None; - let sig = ctx.classes.get(class_name).and_then(|class_info| { - if let Some(sig) = class_info.methods.get(&method_key) { - return Some(sig.clone()); - } - if let Some(sig) = class_info.methods.get("__call") { - dispatch_method = "__call"; - let span = args - .first() - .map(|arg| arg.span) - .unwrap_or_else(crate::span::Span::dummy); - magic_args = Some(objects::magic_method_args(method, args, span)); - return Some(sig.clone()); - } - None - }); - let args_to_emit = magic_args.as_deref().unwrap_or(args); - let emitted_args = - objects::emit_pushed_method_args(args_to_emit, sig.as_ref(), emitter, ctx, data); - objects::emit_method_call_with_saved_receiver_below_args( - class_name, - dispatch_method, - &emitted_args.arg_types, - emitted_args.source_temp_bytes, - emitter, - ctx, - ) -} - -/// Emits a runtime null check for a nullsafe chain's receiver. Unboxes the receiver if it is -/// `Mixed`, compares it against the null tag, and jumps to `null_label` if it is null. Returns -/// `true` if a short-circuit jump was emitted; `false` if the type cannot be null at this point. -fn emit_nullsafe_receiver_check( - current_ty: &PhpType, - null_label: &str, - emitter: &mut Emitter, -) -> bool { - match current_ty.codegen_repr() { - PhpType::Void => { - abi::emit_jump(emitter, null_label); - true - } - PhpType::Mixed => { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // unwrap nullable receiver before the nullsafe chain segment - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #8"); // runtime tag 8 means the receiver is null - emitter.instruction(&format!("b.eq {}", null_label)); // short-circuit the remaining postfix chain on null - emitter.instruction("mov x0, x1"); // move the unboxed object pointer into the normal result register - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 8"); // runtime tag 8 means the receiver is null - emitter.instruction(&format!("je {}", null_label)); // short-circuit the remaining postfix chain on null - emitter.instruction("mov rax, rdi"); // move the unboxed object pointer into the normal result register - } - } - true - } - _ => false, - } -} - -/// Returns true if either the current runtime type or the statically-inferred receiver type -/// is `Void` (PHP null). Used to determine if a nullsafe chain can short-circuit at compile -/// time without emitting a runtime null check. -fn receiver_is_static_null(current_ty: &PhpType, receiver_ty: &PhpType) -> bool { - matches!(current_ty.codegen_repr(), PhpType::Void) || matches!(receiver_ty, PhpType::Void) -} - -/// Attempts to resolve the receiver class name from either the statically-inferred receiver -/// type or the current runtime type. Tries `receiver_ty` first, then falls back to `current_ty`. -/// Returns the class name as an `Option`, or `None` if the type cannot be resolved to a -/// single concrete class (e.g., `Mixed` or a union type). -fn singular_receiver_class(receiver_ty: &PhpType, current_ty: &PhpType) -> Option { - functions::singular_object_class(receiver_ty) - .or_else(|| functions::singular_object_class(current_ty)) - .map(str::to_string) -} diff --git a/src/codegen/expr/coerce.rs b/src/codegen/expr/coerce.rs deleted file mode 100644 index e0bdedf04c..0000000000 --- a/src/codegen/expr/coerce.rs +++ /dev/null @@ -1,421 +0,0 @@ -//! Purpose: -//! Performs expression-result coercions between PHP scalar, string, object, array, nullable, and Mixed shapes. -//! Used when assignment, calls, or operators need a value in a declared target type. -//! -//! Called from: -//! - `crate::codegen::expr` and statement assignment emitters -//! -//! Key details: -//! - Coercions may allocate, retain, or box values, so ownership state must be updated with the result. - -use crate::codegen::context::Context; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, platform::Arch}; -use crate::types::PhpType; - -/// Coerce a value to a PHP string for concatenation (`x1`/`x2` on ARM64, `rsi`/`rdx` on x86_64). -/// -/// Dispatches to runtime helpers based on type: -/// - `Int` → `__rt_itoa` (integer-to-ASCII) -/// - `Float` → `__rt_ftoa` (float-to-ASCII) -/// - `Bool` → true `"1"` / false `""` (zero-length) -/// - `Void`/`Never` → `""` (zero-length) -/// - `Resource` → `__rt_resource_to_string` -/// - `Mixed`/`Union` → `__rt_mixed_cast_string` (runtime dispatch on boxed payload) -/// - `Iterable` → literal `"Array"` string -/// - `Object` → invokes `__toString()` if present, otherwise emits a fatal error and terminates -/// -/// ABI: places string pointer in first string-result register and length in second. -/// Ownership: callers must treat the returned string as owned (runtime may allocate). -pub fn coerce_to_string( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - ty: &PhpType, -) { - coerce_to_string_inner(emitter, ctx, data, ty, false); -} - -/// Like [`coerce_to_string`], but when `release_owned_object` is set and `ty` is an object -/// stringified via `__toString`, the owned object temporary is released after conversion. -/// -/// Callers pass `true` only when the source expression produced an owned object temporary -/// (e.g. `new C()` or a call result); a borrowed object (a variable or property) must pass -/// `false` so its owner — not this coercion — releases it, avoiding a double free. -pub fn coerce_to_string_releasing_owned( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - ty: &PhpType, - release_owned_object: bool, -) { - coerce_to_string_inner(emitter, ctx, data, ty, release_owned_object); -} - -/// Shared body of the string coercion. `release_owned_object` controls whether an owned -/// object temporary that is stringified via `__toString` is released after conversion. -fn coerce_to_string_inner( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - ty: &PhpType, - release_owned_object: bool, -) { - match ty { - PhpType::Int => { - // -- convert integer in x0 to string in x1/x2 -- - abi::emit_call_label(emitter, "__rt_itoa"); // runtime: integer-to-ASCII string conversion - } - PhpType::Resource(_) => { - // -- convert resource in x0/rax to PHP's display string -- - abi::emit_call_label(emitter, "__rt_resource_to_string"); // runtime: resource-to-display-string conversion - } - PhpType::Float => { - // -- convert float in d0 to string in x1/x2 -- - abi::emit_call_label(emitter, "__rt_ftoa"); // runtime: float-to-ASCII string conversion - } - PhpType::Bool => { - // true -> "1" (via itoa), false -> "" (len=0) - // -- convert bool to string: true="1", false="" -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cbz x0, 1f"); // if false (zero), skip to empty string path - abi::emit_call_label(emitter, "__rt_itoa"); // convert true (1) to string "1" - emitter.instruction("b 2f"); // skip over the empty-string fallback - emitter.raw("1:"); - emitter.instruction("mov x2, #0"); // false produces empty string (length = 0) - emitter.raw("2:"); - } - Arch::X86_64 => { - let false_label = ctx.next_label("bool_to_str_false"); - let done_label = ctx.next_label("bool_to_str_done"); - emitter.instruction("test rax, rax"); // test whether the boolean payload is false - emitter.instruction(&format!("je {}", false_label)); // skip to the empty-string path when the boolean is false - abi::emit_call_label(emitter, "__rt_itoa"); // convert true (1) to string "1" - emitter.instruction(&format!("jmp {}", done_label)); // skip over the empty-string fallback after conversion - emitter.label(&false_label); - emitter.instruction("mov rdx, 0"); // false produces empty string (length = 0) - emitter.label(&done_label); - } - } - } - PhpType::Void | PhpType::Never => { - // -- null coerces to empty string in PHP -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x2, #0"); // null produces empty string (length = 0) - } - Arch::X86_64 => { - emitter.instruction("mov rdx, 0"); // null produces empty string (length = 0) - } - } - } - PhpType::TaggedScalar => { - // -- tagged scalar: null -> empty string, int payload -> decimal text -- - let null_label = ctx.next_label("tagged_to_str_null"); - let done_label = ctx.next_label("tagged_to_str_done"); - crate::codegen::sentinels::emit_branch_if_tagged_scalar_null(emitter, &null_label); - abi::emit_call_label(emitter, "__rt_itoa"); // convert the non-null tagged scalar payload to decimal text - abi::emit_jump(emitter, &done_label); // skip the empty-string fallback after conversion - emitter.label(&null_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x2, #0"); // null produces empty string (length = 0) - } - Arch::X86_64 => { - emitter.instruction("mov rdx, 0"); // null produces empty string (length = 0) - } - } - emitter.label(&done_label); - } - PhpType::Mixed | PhpType::Union(_) => { - // -- mixed strings dispatch on the boxed payload at runtime -- - abi::emit_call_label(emitter, "__rt_mixed_cast_string"); // cast the boxed mixed payload to string in the ABI string result registers - } - PhpType::Iterable | PhpType::Array(_) | PhpType::AssocArray { .. } => { - // -- iterable and array values stringify to the literal "Array", matching PHP -- - let (label, len) = data.add_string(b"Array"); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_symbol_address(emitter, ptr_reg, &label); // materialize the literal "Array" address in the active string-pointer result register - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, #{}", len_reg, len)); // load the literal "Array" byte length into the active AArch64 string-length result register - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", len_reg, len)); // load the literal "Array" byte length into the active x86_64 string-length result register - } - } - } - PhpType::Object(class_name) => { - if ctx - .classes - .get(class_name) - .is_some_and(|class_info| class_info.methods.contains_key("__tostring")) - { - if release_owned_object { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the owned object temp below $this so it can be released after __toString borrows it - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // push $this pointer for __toString dispatch using the active target ABI - super::objects::emit_method_call_with_pushed_args( - class_name, - "__tostring", - &[], - emitter, - ctx, - ); - if release_owned_object { - emit_release_saved_object_temp(emitter, ty); - } - } else { - emit_missing_tostring_fatal(emitter, data, class_name); - } - } - PhpType::Str - | PhpType::Callable - | PhpType::Buffer(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => {} - } -} - -/// Releases an owned object temporary that was just stringified via `__toString`. -/// -/// On entry the produced string is in the string result registers, and the object pointer -/// was saved on the temporary stack below the (already-popped) `$this` slot. The string -/// result is preserved across the object decref, then restored, and the saved object slot -/// is discarded — leaving the string result in place and the object temporary freed. -fn emit_release_saved_object_temp(emitter: &mut Emitter, ty: &PhpType) { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the __toString result string across the object decref call - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); // reload the saved owned object pointer from below the 16-byte string slot - abi::emit_decref_if_refcounted(emitter, ty); // release the owned object temporary now that __toString produced its string - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); // restore the preserved __toString result string - abi::emit_release_temporary_stack(emitter, 16); // discard the saved owned object slot -} - -/// Emit a fatal error and terminate when an object without `__toString()` is coerced to string. -/// -/// Writes the error message to stderr using platform syscalls, then exits with code 1. -/// This function does not return. -fn emit_missing_tostring_fatal(emitter: &mut Emitter, data: &mut DataSection, class_name: &str) { - let message = format!( - "Fatal error: Object of class {} could not be converted to string\n", - class_name - ); - let (label, len) = data.add_string(message.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // fd = stderr for fatal conversion diagnostics - abi::emit_symbol_address(emitter, "x1", &label); // load the page that contains the fatal conversion message - emitter.instruction(&format!("mov x2, #{}", len)); // pass the fatal conversion message length to write() - emitter.syscall(4); - emitter.instruction("mov x0, #1"); // exit status 1 indicates abnormal termination - emitter.syscall(1); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", &label); // point the Linux write() buffer register at the fatal conversion message - emitter.instruction(&format!("mov edx, {}", len)); // pass the fatal conversion message length to write() - emitter.instruction("mov edi, 2"); // fd = stderr for fatal conversion diagnostics - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal conversion message before terminating - emitter.instruction("mov edi, 1"); // exit status 1 indicates abnormal termination - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate the process after reporting the failed string conversion - } - } -} - -/// Replace null sentinel with 0 in the integer result register after `coerce_null_to_zero`. -/// -/// Two cases are handled: -/// - **Compile-time null** (`Void`/`Never` type): directly emit `mov x0/rax, #0/0`. -/// - **Runtime null** (Int payload that may hold the sentinel `0x7FFFFFFFFFFFFFFF_FFFE`): compare -/// against the sentinel and select zero when equal; ordinary integers are unchanged. -/// -/// `Bool` and `Float` are no-ops because their representations already match integer zero -/// (bool is 0/1 in x0, float is in d0). -pub fn coerce_null_to_zero(emitter: &mut Emitter, ty: &PhpType) { - if *ty == PhpType::Void { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // null is zero in arithmetic/comparison context - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // null is zero in arithmetic/comparison context - } - } - } else if *ty == PhpType::Bool { - // Bool is already 0/1 in x0, compatible with Int arithmetic - } else if *ty == PhpType::Float { - // Float is already in d0, no null sentinel to check - } else if *ty == PhpType::TaggedScalar { - crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(emitter); - } else if *ty == PhpType::Int && crate::codegen::sentinels::null_repr_is_tagged() { - // Under the tagged representation a plain Int can never hold the null sentinel - } else if *ty == PhpType::Int { - match emitter.target.arch { - Arch::AArch64 => { - let sentinel = NULL_SENTINEL as u64; - emitter.instruction(&format!("movz x9, #0x{:X}", sentinel & 0xFFFF)); // build null sentinel in x9: bits 0-15 - emitter.instruction(&format!("movk x9, #0x{:X}, lsl #16", (sentinel >> 16) & 0xFFFF)); // null sentinel bits 16-31 - emitter.instruction(&format!("movk x9, #0x{:X}, lsl #32", (sentinel >> 32) & 0xFFFF)); // null sentinel bits 32-47 - emitter.instruction(&format!("movk x9, #0x{:X}, lsl #48", (sentinel >> 48) & 0xFFFF)); // null sentinel bits 48-63, completing value - emitter.instruction("cmp x0, x9"); // compare value against null sentinel - emitter.instruction("csel x0, xzr, x0, eq"); // if x0 == sentinel, replace with zero - } - Arch::X86_64 => { - let sentinel_reg = abi::temp_int_reg(emitter.target); - let zero_reg = abi::symbol_scratch_reg(emitter); - emitter.instruction(&format!("mov {}, {}", sentinel_reg, NULL_SENTINEL)); // materialize the runtime null sentinel in a scratch register - emitter.instruction(&format!("xor {}, {}", zero_reg, zero_reg)); // materialize an integer zero in a second scratch register - emitter.instruction(&format!("cmp {}, {}", abi::int_result_reg(emitter), sentinel_reg)); // compare the current integer result against the runtime null sentinel - emitter.instruction(&format!("cmove {}, {}", abi::int_result_reg(emitter), zero_reg)); // replace the sentinel with zero while leaving ordinary integers unchanged - } - } - } -} - -/// Coerce a typed expression result to a raw PHP integer in the integer result register. -/// -/// First normalizes null via [`coerce_null_to_zero`], then for `Mixed`/`Union` values calls -/// `__rt_mixed_cast_int` to unbox the boxed payload (int|bool|string|float) into a plain `i64`. -/// `Int`/`Bool`/`Float` already occupy the right register and are left unchanged. -/// -/// This is the shared coercion used by arithmetic, bitwise, comparison, and integer-argument -/// builtins (e.g. `intdiv`), so a boxed Mixed operand is never consumed as a raw integer. -pub fn coerce_to_int(emitter: &mut Emitter, ty: &PhpType) { - coerce_null_to_zero(emitter, ty); - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_call_label(emitter, "__rt_mixed_cast_int"); // normalize boxed int|bool|string values into a raw integer - } else if *ty == PhpType::Str { - // A string operand lives in the string-result registers; on x86_64 the pointer also - // occupies the integer result register, so an unconverted string would be compared as a - // raw pointer. Parse it through PHP string-to-int rules into the integer register. - abi::emit_call_label(emitter, "__rt_str_to_int"); // numeric value of the string operand in the int register - } -} - -/// Coerce any type to a PHP truthiness value in the integer result register. -/// -/// Handles all PHP truthiness rules: -/// - Calls `coerce_null_to_zero` first to normalize null for all types. -/// - `Str`: `""` and `"0"` are falsy; all other strings (including `"1"`, `"-1"`) are truthy. -/// - `Float`: 0.0 is falsy; non-zero (including negative zero) is truthy. -/// - `Int`/`Bool`/`Void`/`Callable`/`Object`/`Buffer`/`Packed`/`Pointer`: non-zero is truthy. -/// - `Resource`: always truthy (regardless of native handle value). -/// - `Array`/`AssocArray`/`Iterable`: non-empty (runtime length > 0) is truthy. -/// - `Mixed`/`Union`: delegates to `__rt_mixed_cast_bool` for runtime dispatch. -/// -/// Result is placed in the canonical integer result register (`x0`/`rax`). -pub fn coerce_to_truthiness(emitter: &mut Emitter, ctx: &mut Context, ty: &PhpType) { - coerce_null_to_zero(emitter, ty); - if *ty == PhpType::Str { - // -- PHP string truthiness: "" and "0" are falsy, everything else truthy -- - let falsy_label = ctx.next_label("str_falsy"); - let truthy_label = ctx.next_label("str_truthy"); - let done_label = ctx.next_label("str_truth_done"); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbz {}, {falsy_label}", len_reg)); // empty string is falsy - emitter.instruction(&format!("cmp {}, #1", len_reg)); // check if length is 1 - emitter.instruction(&format!("b.ne {truthy_label}")); // length != 1 means truthy - emitter.instruction(&format!("ldrb w9, [{}]", ptr_reg)); // load first byte of string - emitter.instruction("cmp w9, #48"); // compare with ASCII '0' - emitter.instruction(&format!("b.eq {falsy_label}")); // string "0" is falsy - emitter.label(&truthy_label); - emitter.instruction(&format!("mov {}, #1", abi::int_result_reg(emitter))); // truthy: set result = 1 - emitter.instruction(&format!("b {done_label}")); // skip falsy path - emitter.label(&falsy_label); - emitter.instruction(&format!("mov {}, #0", abi::int_result_reg(emitter))); // falsy: set result = 0 - emitter.label(&done_label); - } - Arch::X86_64 => { - let scratch = abi::temp_int_reg(emitter.target); - emitter.instruction(&format!("test {}, {}", len_reg, len_reg)); // empty string is falsy - emitter.instruction(&format!("je {}", falsy_label)); // branch to falsy path when the string length is zero - emitter.instruction(&format!("cmp {}, 1", len_reg)); // check if length is 1 - emitter.instruction(&format!("jne {}", truthy_label)); // any other non-empty length is truthy - emitter.instruction(&format!("movzx {}d, BYTE PTR [{}]", scratch, ptr_reg)); // load the first byte of the one-character string - emitter.instruction(&format!("cmp {}d, 48", scratch)); // compare against ASCII '0' - emitter.instruction(&format!("je {}", falsy_label)); // the string \"0\" is falsy in PHP - emitter.label(&truthy_label); - emitter.instruction(&format!("mov {}, 1", abi::int_result_reg(emitter))); // truthy: set result = 1 - emitter.instruction(&format!("jmp {}", done_label)); // skip the falsy path once the result is known - emitter.label(&falsy_label); - emitter.instruction(&format!("mov {}, 0", abi::int_result_reg(emitter))); // falsy: set result = 0 - emitter.label(&done_label); - } - } - } else if *ty == PhpType::Float { - // -- float truthiness: 0.0 is falsy -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("fcmp d0, #0.0"); // compare float against zero - emitter.instruction("cset x0, ne"); // x0=1 if nonzero (truthy), 0 if zero - } - Arch::X86_64 => { - let bits_reg = abi::temp_int_reg(emitter.target); - emitter.instruction(&format!("movq {}, xmm0", bits_reg)); // move the current float bits into a scratch integer register - emitter.instruction(&format!("shl {}, 1", bits_reg)); // discard the sign bit so +0.0 and -0.0 both normalize to zero - emitter.instruction(&format!("cmp {}, 0", bits_reg)); // compare the signless float bits against zero - emitter.instruction("setne al"); // set al when the float payload is non-zero - emitter.instruction("movzx rax, al"); // widen the boolean byte into the full integer result register - } - } - } else if matches!( - ty, - PhpType::Int - | PhpType::Bool - | PhpType::Void - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Buffer(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) - ) { - // -- scalars and pointer-like values are truthy when non-zero -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // compare the normalized scalar/pointer value against zero - emitter.instruction("cset x0, ne"); // produce 1 when the scalar/pointer value is non-zero, else 0 - } - Arch::X86_64 => { - let result_reg = abi::int_result_reg(emitter); - emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // compare the normalized scalar/pointer value against zero - emitter.instruction("setne al"); // produce a boolean byte when the scalar/pointer value is non-zero - emitter.instruction("movzx rax, al"); // widen the boolean byte into the canonical integer result register - } - } - } else if matches!(ty, PhpType::Resource(_)) { - // -- PHP resources are truthy regardless of their underlying native handle value -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #1"); // resources always coerce to true - } - Arch::X86_64 => { - emitter.instruction("mov rax, 1"); // resources always coerce to true - } - } - } else if matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Iterable) { - // -- arrays and iterable hash payloads are truthy when their runtime length is non-zero -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [x0]"); // load the runtime array length from the header - emitter.instruction("cmp x0, #0"); // compare the array length against zero - emitter.instruction("cset x0, ne"); // produce 1 for non-empty arrays, else 0 - } - Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rax]"); // load the runtime array length from the header - emitter.instruction("test rax, rax"); // compare the array length against zero - emitter.instruction("setne al"); // produce a boolean byte when the array is non-empty - emitter.instruction("movzx rax, al"); // widen the boolean byte into the canonical integer result register - } - } - } else if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - // -- mixed/union truthiness dispatches on the boxed payload at runtime -- - abi::emit_call_label(emitter, "__rt_mixed_cast_bool"); // normalize the boxed mixed payload to PHP truthiness - } -} diff --git a/src/codegen/expr/compare/casts.rs b/src/codegen/expr/compare/casts.rs deleted file mode 100644 index bc98fa5bac..0000000000 --- a/src/codegen/expr/compare/casts.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! Purpose: -//! Lowers comparison-time casts and truthiness conversions. -//! Keeps comparison-specific branching and register normalization out of generic expression code. -//! -//! Called from: -//! - `crate::codegen::expr::compare` -//! -//! Key details: -//! - Null, type-tag, and string comparisons must follow PHP semantics before emitting boolean results. - -use crate::codegen::abi; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::super::{ - coerce_to_string_releasing_owned, coerce_to_truthiness, emit_expr, expr_result_heap_ownership, -}; - -/// Emits a PHP cast expression (`(int)`, `(float)`, `(string)`, `(bool)`, `(array)`). -/// -/// # Arguments -/// - `target` — the cast kind (Int, Float, String, Bool, Array) -/// - `expr` — the expression to cast; must already be emitted so `ctx` holds the result type in `src_ty` -/// - `emitter` — target-aware instruction emitter -/// - `ctx` — codegen context; receives the cast result type -/// - `data` — data section for relocations and static data -/// -/// # Returns -/// The `PhpType` that results from the cast (e.g., `PhpType::Int` for `(int)`). -/// -/// # PHP cast semantics -/// - `(int)` from string → calls `__rt_str_to_int`; from resource → native payload + 1; from array → container length -/// - `(float)` from string → null-terminates via `__rt_cstr` then calls `atof`; from resource → id + conversion -/// - `(bool)` → uses shared truthiness coercion (`coerce_to_truthiness`) -/// - `(string)` → delegates to `coerce_to_string` -/// - `(array)` from scalar → allocates 1-element array via `__rt_array_new` / `__rt_array_push_int`; otherwise empty 4-capacity array -pub(in crate::codegen::expr) fn emit_cast( - target: &crate::parser::ast::CastType, - expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - use crate::parser::ast::CastType; - let src_ty = emit_expr(expr, emitter, ctx, data); - emitter.comment(&format!("cast to {:?}", target)); - match target { - CastType::Int => { - match &src_ty { - PhpType::Int => {} - PhpType::Float => { - abi::emit_float_result_to_int_result(emitter); // convert double to signed 64-bit int (toward zero) - } - PhpType::Bool => {} - PhpType::Void | PhpType::Never => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_str_to_int"); // parse the current string result through PHP string-to-int cast rules - } - PhpType::Resource(_) => match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("add x0, x0, #1"); // convert the native resource payload into the 1-based display id - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("add rax, 1"); // convert the native resource payload into the 1-based display id - } - }, - PhpType::Array(_) | PhpType::AssocArray { .. } => { - emitter.instruction("ldr x0, [x0]"); // load array/hash container length from header (first field; iterable hash kind shares this layout) - } - PhpType::Iterable => { - emit_iterable_nonempty_as_int(emitter, ctx); // PHP casts array-backed iterables to 0/1 based on emptiness - } - PhpType::Mixed | PhpType::Union(_) => { - abi::emit_call_label(emitter, "__rt_mixed_cast_int"); // cast the boxed mixed payload to int through the target-aware helper - } - PhpType::TaggedScalar => { - crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(emitter); - } - PhpType::Callable - | PhpType::Object(_) - | PhpType::Buffer(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => {} - } - PhpType::Int - } - CastType::Float => { - match &src_ty { - PhpType::Float => {} - PhpType::TaggedScalar => { - crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(emitter); - abi::emit_int_result_to_float_result(emitter); // signed int to double conversion - } - PhpType::Int | PhpType::Bool => { - abi::emit_int_result_to_float_result(emitter); // signed int to double conversion - } - PhpType::Resource(_) => { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("add x0, x0, #1"); // convert the native resource payload into the 1-based display id - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("add rax, 1"); // convert the native resource payload into the 1-based display id - } - } - abi::emit_int_result_to_float_result(emitter); // convert the resource display id to double - } - PhpType::Void | PhpType::Never => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_int_result_to_float_result(emitter); // convert to 0.0 double - } - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_cstr"); // null-terminate the current string result through the target-aware C-string helper - if emitter.target.arch == crate::codegen::platform::Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // pass the null-terminated C string in the SysV first-argument register before atof() - } - emitter.bl_c("atof"); // parse C string as double → d0=result - } - PhpType::Mixed | PhpType::Union(_) => { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // cast the boxed mixed payload to float through the target-aware helper - } - PhpType::Iterable => { - emit_iterable_nonempty_as_int(emitter, ctx); // PHP casts array-backed iterables to 0/1 before float widening - abi::emit_int_result_to_float_result(emitter); // convert the normalized iterable integer cast to double - } - PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Buffer(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - abi::emit_int_result_to_float_result(emitter); // convert to 0.0 double (iterable joins the array group for elephc cast semantics) - } - } - PhpType::Float - } - CastType::String => { - coerce_to_string_releasing_owned( - emitter, - ctx, - data, - &src_ty, - expr_result_heap_ownership(expr) == HeapOwnership::Owned, - ); - PhpType::Str - } - CastType::Bool => { - coerce_to_truthiness(emitter, ctx, &src_ty); // normalize any source value to PHP truthiness using the shared target-aware helper path - PhpType::Bool - } - CastType::Array => { - match &src_ty { - PhpType::Array(_) | PhpType::AssocArray { .. } => { - return src_ty; - } - PhpType::Int - | PhpType::Bool - | PhpType::Resource(_) - | PhpType::Callable - | PhpType::Buffer(_) - | PhpType::Packed(_) => { - emitter.instruction("str x0, [sp, #-16]!"); // save scalar value during allocation - emitter.instruction("mov x0, #1"); // capacity: 1 element (exact fit) - emitter.instruction("mov x1, #8"); // element size: 8 bytes - emitter.instruction("bl __rt_array_new"); // allocate new array struct - emitter.instruction("ldr x1, [sp], #16"); // pop saved scalar value - emitter.instruction("bl __rt_array_push_int"); // push scalar as first element - } - _ => { - emitter.instruction("mov x0, #4"); // capacity: 4 (grows dynamically) - emitter.instruction("mov x1, #8"); // element size: 8 bytes - emitter.instruction("bl __rt_array_new"); // allocate empty array struct - } - } - PhpType::Array(Box::new(PhpType::Int)) - } - } -} - -/// Emits PHP iterable-to-int casting for `(int)` and `(float)` on `PhpType::Iterable`. -/// -/// Classifies the iterable's heap kind (array/hash/object/null) then emits -/// the PHP-appropriate integer: arrays/hashes → 1 if non-empty else 0; objects → 1; null → 0. -/// -/// # Arguments -/// - `emitter` — target-aware instruction emitter; must have the iterable pointer in `int_result_reg` -/// - `ctx` — codegen context; used to allocate local labels -/// -/// # Side effects -/// - Caller must preserve the iterable pointer before calling (function pushes it on the stack) -/// - Consumes the preserved pointer and stack space before branching to `done` -fn emit_iterable_nonempty_as_int(emitter: &mut Emitter, ctx: &mut Context) { - let array_case = ctx.next_label("iterable_cast_array"); - let true_case = ctx.next_label("iterable_cast_true"); - let false_case = ctx.next_label("iterable_cast_false"); - let done = ctx.next_label("iterable_cast_done"); - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the erased iterable pointer while checking its heap kind - abi::emit_call_label(emitter, "__rt_heap_kind"); // classify the iterable payload by heap kind before reading its layout - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("cmp x0, #2"); // is the iterable backed by an indexed array? - emitter.instruction(&format!("b.eq {}", array_case)); // arrays cast by checking whether their length is non-zero - emitter.instruction("cmp x0, #3"); // is the iterable backed by an associative array? - emitter.instruction(&format!("b.eq {}", array_case)); // hashes cast by checking whether their length is non-zero - emitter.instruction("cmp x0, #4"); // is the iterable backed by an object? - emitter.instruction(&format!("b.eq {}", true_case)); // objects cast to 1 like PHP object values - emitter.instruction(&format!("b {}", false_case)); // null or unknown payloads cast to 0 - - emitter.label(&array_case); - abi::emit_pop_reg(emitter, "x9"); // restore the array/hash pointer for the length read - emitter.instruction("ldr x0, [x9]"); // load the runtime container length from the shared header - emitter.instruction("cmp x0, #0"); // check whether the iterable container is empty - emitter.instruction("cset x0, ne"); // PHP numeric array casts return 1 for non-empty arrays - emitter.instruction(&format!("b {}", done)); // finish after materializing the array-backed cast result - - emitter.label(&true_case); - emitter.instruction("add sp, sp, #16"); // discard the preserved iterable pointer before returning 1 - emitter.instruction("mov x0, #1"); // object-backed iterables cast to integer 1 - emitter.instruction(&format!("b {}", done)); // finish after materializing the truthy object cast - - emitter.label(&false_case); - emitter.instruction("add sp, sp, #16"); // discard the preserved iterable pointer before returning 0 - emitter.instruction("mov x0, #0"); // null or unknown iterable payloads cast to integer 0 - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("cmp rax, 2"); // is the iterable backed by an indexed array? - emitter.instruction(&format!("je {}", array_case)); // arrays cast by checking whether their length is non-zero - emitter.instruction("cmp rax, 3"); // is the iterable backed by an associative array? - emitter.instruction(&format!("je {}", array_case)); // hashes cast by checking whether their length is non-zero - emitter.instruction("cmp rax, 4"); // is the iterable backed by an object? - emitter.instruction(&format!("je {}", true_case)); // objects cast to 1 like PHP object values - emitter.instruction(&format!("jmp {}", false_case)); // null or unknown payloads cast to 0 - - emitter.label(&array_case); - abi::emit_pop_reg(emitter, "r10"); // restore the array/hash pointer for the length read - emitter.instruction("mov rax, QWORD PTR [r10]"); // load the runtime container length from the shared header - emitter.instruction("test rax, rax"); // check whether the iterable container is empty - emitter.instruction("setne al"); // PHP numeric array casts return 1 for non-empty arrays - emitter.instruction("movzx rax, al"); // widen the boolean byte to the canonical integer result - emitter.instruction(&format!("jmp {}", done)); // finish after materializing the array-backed cast result - - emitter.label(&true_case); - abi::emit_pop_reg(emitter, "r10"); // discard the preserved iterable pointer before returning 1 - emitter.instruction("mov rax, 1"); // object-backed iterables cast to integer 1 - emitter.instruction(&format!("jmp {}", done)); // finish after materializing the truthy object cast - - emitter.label(&false_case); - abi::emit_pop_reg(emitter, "r10"); // discard the preserved iterable pointer before returning 0 - emitter.instruction("xor eax, eax"); // null or unknown iterable payloads cast to integer 0 - } - } - emitter.label(&done); -} diff --git a/src/codegen/expr/compare/mod.rs b/src/codegen/expr/compare/mod.rs deleted file mode 100644 index 8389dede6b..0000000000 --- a/src/codegen/expr/compare/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Groups comparison-specific expression helpers for strict comparison, casts, and null coalescing. -//! Keeps PHP comparison semantics separate from general binary operator dispatch. -//! -//! Called from: -//! - `crate::codegen::expr::binops` and `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Loose, strict, and null-sensitive paths have different runtime and register conventions. - -mod casts; -mod null_coalesce; -mod strict; - -pub(super) use casts::emit_cast; -pub(super) use null_coalesce::emit_null_coalesce; -pub(super) use strict::emit_strict_compare; diff --git a/src/codegen/expr/compare/null_coalesce.rs b/src/codegen/expr/compare/null_coalesce.rs deleted file mode 100644 index 7c5e3a18e9..0000000000 --- a/src/codegen/expr/compare/null_coalesce.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Purpose: -//! Lowers null coalescing expressions with short-circuit reads. -//! Keeps comparison-specific branching and register normalization out of generic expression code. -//! -//! Called from: -//! - `crate::codegen::expr::compare` -//! -//! Key details: -//! - Null, type-tag, and string comparisons must follow PHP semantics before emitting boolean results. - -use crate::codegen::abi; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::super::{coerce_result_to_type, emit_expr, widen_codegen_type}; - -/// Emits the null coalescing operator (`??`) for PHP expressions with short-circuit evaluation. -/// -/// # Arguments -/// - `value` — the left-hand side expression to check for null -/// - `default` — the right-hand side expression evaluated when `value` is null -/// -/// # Returns -/// The `PhpType` of the result after applying PHP type coercion rules to the operands. -/// -/// # Behavior -/// - If `value` evaluates to null (or a boxed Mixed/Union tagged null), the `default` expression -/// is evaluated and returned. -/// - If `value` is non-null, the original value is kept (subject to type coercion). -/// - For `PhpType::Mixed` and `PhpType::Union`, a runtime unbox routine inspects the payload tag. -/// - For scalar types, a sentinel value (0x7fff_ffff_ffff_fffe) is used as the null marker. -/// - The result type is widened according to PHP's type coercion rules (`widen_codegen_type`). -pub(in crate::codegen::expr) fn emit_null_coalesce( - value: &Expr, - default: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("null coalesce ??"); - let val_ty = emit_expr(value, emitter, ctx, data); - - if val_ty == PhpType::Void { - return emit_expr(default, emitter, ctx, data); - } - - let default_ty = crate::codegen::functions::infer_contextual_type(default, ctx); - let result_ty = widen_codegen_type(&val_ty, &default_ty); - - if matches!(val_ty, PhpType::Int) && crate::codegen::sentinels::null_repr_is_tagged() { - // Under the tagged representation a plain Int is never null: ?? always keeps it. - return val_ty; - } - - let use_value_label = ctx.next_label("nc_keep"); - let end_label = ctx.next_label("nc_end"); - if matches!(val_ty, PhpType::TaggedScalar) { - crate::codegen::sentinels::emit_branch_if_tagged_scalar_not_null(emitter, &use_value_label); - } else if matches!(val_ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the boxed mixed/union value across the null check and fallback evaluation - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect the boxed payload tag before deciding whether ?? should fall back - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("cmp x0, #8"); // runtime tag 8 = null - emitter.instruction(&format!("b.ne {}", use_value_label)); // non-null mixed payload keeps the original boxed value - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("cmp rax, 8"); // runtime tag 8 = null - emitter.instruction(&format!("jne {}", use_value_label)); // non-null mixed payload keeps the original boxed value - } - } - } else { - let null_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_int_immediate(emitter, null_reg, NULL_SENTINEL); // materialize the shared null sentinel for the direct null test - if val_ty == PhpType::Float { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("fmov x0, d0"); // copy float bits into x0 for the null-sentinel check on AArch64 - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("movq rax, xmm0"); // copy float bits into rax for the null-sentinel check on x86_64 - } - } - } - let cmp_reg = if val_ty == PhpType::Str { abi::string_result_regs(emitter).0 } else { abi::int_result_reg(emitter) }; - emitter.instruction(&format!("cmp {}, {}", cmp_reg, null_reg)); // compare value against the null sentinel - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("b.ne {}", use_value_label)); // if not null, skip default branch and keep value - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("jne {}", use_value_label)); // if not null, skip default branch and keep value - } - } - } - - let default_runtime_ty = emit_expr(default, emitter, ctx, data); - coerce_result_to_type(emitter, ctx, data, &default_runtime_ty, &result_ty); - if matches!(val_ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_release_temporary_stack(emitter, 16); // discard the saved original boxed mixed/union value on the null fallback path - } - abi::emit_jump(emitter, &end_label); // skip the non-null branch after evaluating the default expression - emitter.label(&use_value_label); - if matches!(val_ty, PhpType::Mixed | PhpType::Union(_)) { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the original boxed mixed/union payload for the keep-left branch - } - coerce_result_to_type(emitter, ctx, data, &val_ty, &result_ty); - emitter.label(&end_label); - - result_ty -} diff --git a/src/codegen/expr/compare/strict.rs b/src/codegen/expr/compare/strict.rs deleted file mode 100644 index 1e116f8c18..0000000000 --- a/src/codegen/expr/compare/strict.rs +++ /dev/null @@ -1,393 +0,0 @@ -//! Purpose: -//! Lowers strict equality and identity comparison helpers. -//! Keeps comparison-specific branching and register normalization out of generic expression code. -//! -//! Called from: -//! - `crate::codegen::expr::compare` -//! -//! Key details: -//! - Null, type-tag, and string comparisons must follow PHP semantics before emitting boolean results. - -use crate::codegen::abi; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use crate::parser::ast::{BinOp, ExprKind}; - -use super::super::{emit_expr, expr_result_heap_ownership}; - -/// Emits strict equality (`===`) or inequality (`!==`) comparison. -/// -/// Both operands are evaluated and pushed to the temporary comparison stack before -/// comparison. The function dispatches based on the static type of the left operand: -/// - **Int/Bool/Void/Never/Resource**: integer comparison via `cmp` + `cset`/`set` -/// - **Float**: floating-point comparison via `fcmp`/`ucomisd` + `cset`/`set` -/// - **Str**: byte-by-byte comparison via `__rt_str_eq` runtime helper -/// - **Array/AssocArray/Iterable/Callable/Object/Buffer/Pointer**: pointer identity comparison -/// - **Mixed/Union**: boxed mixed comparison via `__rt_mixed_strict_eq` runtime helper -/// -/// When `types_match` is false (incompatible static types), returns false for `===` and -/// true for `!==` without emitting comparison code. The function preserves PHP semantics -/// where a compile-time type mismatch means the comparison result is known at compile time. -/// -/// Returns `PhpType::Bool` as the result type. The function handles ownership cleanup for -/// boxed mixed operands via `__rt_decref_mixed` after the comparison helper returns. -pub(in crate::codegen::expr) fn emit_strict_compare( - left: &Expr, - op: &BinOp, - right: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let is_eq = *op == BinOp::StrictEq; - emitter.comment(if is_eq { "===" } else { "!==" }); - - let lt_peek = peek_expr_type(left, ctx); - let rt_peek = peek_expr_type(right, ctx); - - let types_match = match (<_peek, &rt_peek) { - (Some(PhpType::Pointer(_)), Some(PhpType::Pointer(_))) => true, - (Some(l), Some(r)) - if matches!(l, PhpType::Mixed | PhpType::Union(_)) - || matches!(r, PhpType::Mixed | PhpType::Union(_)) => - { - true - } - // Under the tagged representation an Int-peeked expression can evaluate to a - // TaggedScalar holding null (array-miss reads), so int/null combinations must be - // compared at runtime instead of being constant-folded to a type mismatch. - (Some(l), Some(r)) - if crate::codegen::sentinels::null_repr_is_tagged() - && matches!(l, PhpType::Int | PhpType::Void | PhpType::TaggedScalar) - && matches!(r, PhpType::Int | PhpType::Void | PhpType::TaggedScalar) => - { - true - } - (Some(l), Some(r)) => l == r, - _ => true, - }; - - let lt = emit_expr(left, emitter, ctx, data); - - if types_match { - match < { - PhpType::Float => { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // push the left float for later comparison through the target-aware helper - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // push the left string pointer/length pair for later comparison through the target-aware helper - } - PhpType::Mixed => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // push the left boxed mixed pointer for payload-aware strict comparison through the target-aware helper - } - PhpType::TaggedScalar => { - let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(emitter); - abi::emit_push_reg_pair(emitter, abi::int_result_reg(emitter), tag_reg); // push the left tagged scalar payload/tag pair for later comparison - } - _ => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // push the left scalar or pointer-like value for later comparison through the target-aware helper - } - } - - let rt = emit_expr(right, emitter, ctx, data); - - if matches!(lt, PhpType::Mixed | PhpType::Union(_) | PhpType::TaggedScalar) - || matches!(rt, PhpType::Mixed | PhpType::Union(_) | PhpType::TaggedScalar) - { - let left_box_temp = !matches!(lt, PhpType::Mixed | PhpType::Union(_)); - let right_box_temp = !matches!(rt, PhpType::Mixed | PhpType::Union(_)); - let release_left_mixed = left_box_temp || owned_mixed_operand(left, <); - let release_right_mixed = right_box_temp || owned_mixed_operand(right, &rt); - - match &rt { - PhpType::Float => { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // spill the right float before reloading the left operand into the same register - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // spill the right string payload before reloading the left operand into the same registers - } - PhpType::TaggedScalar => { - let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(emitter); - abi::emit_push_reg_pair(emitter, abi::int_result_reg(emitter), tag_reg); // spill the right tagged scalar payload/tag pair before reloading the left operand - } - _ => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // spill the right scalar/pointer/mixed box before reloading the left operand into the same register - } - } - - match < { - PhpType::Float => { - abi::emit_load_temporary_stack_slot(emitter, abi::float_result_reg(emitter), 16); // reload the saved left float operand from the lower comparison stack slot - crate::codegen::emit_box_current_value_as_mixed(emitter, <); // box the left float operand so mixed comparison can inspect its runtime tag - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("str x0, [sp, #16]"); // replace the old left comparison slot with the boxed left mixed pointer - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // replace the old left comparison slot with the boxed left mixed pointer - } - } - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, 16); // reload the saved left string pointer from the lower comparison stack slot - abi::emit_load_temporary_stack_slot(emitter, len_reg, 24); // reload the saved left string length from the lower comparison stack slot - crate::codegen::emit_box_current_value_as_mixed(emitter, <); // box the left string payload so mixed comparison can inspect its runtime tag - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("str x0, [sp, #16]"); // replace the old left comparison slot with the boxed left mixed pointer - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // replace the old left comparison slot with the boxed left mixed pointer - } - } - } - PhpType::TaggedScalar => { - let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); // reload the saved left tagged scalar payload from the lower comparison stack slot - abi::emit_load_temporary_stack_slot(emitter, tag_reg, 24); // reload the saved left tagged scalar tag from the lower comparison stack slot - crate::codegen::emit_box_current_value_as_mixed(emitter, <); // box the left tagged scalar so mixed comparison can inspect its runtime tag - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("str x0, [sp, #16]"); // replace the old left comparison slot with the boxed left mixed pointer - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // replace the old left comparison slot with the boxed left mixed pointer - } - } - } - _ => { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); // reload the saved left scalar/pointer operand from the lower comparison stack slot - crate::codegen::emit_box_current_value_as_mixed(emitter, <); // box the left operand when it is not already mixed - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("str x0, [sp, #16]"); // replace the old left comparison slot with the boxed left mixed pointer - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // replace the old left comparison slot with the boxed left mixed pointer - } - } - } - } - - match &rt { - PhpType::Float => { - abi::emit_load_temporary_stack_slot(emitter, abi::float_result_reg(emitter), 0); // restore the spilled right float operand after boxing the left operand - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, 0); // restore the spilled right string pointer after boxing the left operand - abi::emit_load_temporary_stack_slot(emitter, len_reg, 8); // restore the spilled right string length after boxing the left operand - } - PhpType::TaggedScalar => { - let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 0); // restore the spilled right tagged scalar payload after boxing the left operand - abi::emit_load_temporary_stack_slot(emitter, tag_reg, 8); // restore the spilled right tagged scalar tag after boxing the left operand - } - _ => { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 0); // restore the spilled right scalar/pointer/mixed box after boxing the left operand - } - } - crate::codegen::emit_box_current_value_as_mixed(emitter, &rt); // box the right operand when it is not already mixed - abi::emit_reserve_temporary_stack(emitter, 32); // reserve scratch space for boxed operands and the boolean result - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldr x10, [sp, #48]"); // reload the boxed left mixed pointer from the lower saved-comparison slot - emitter.instruction("str x10, [sp, #0]"); // save the left boxed mixed pointer for cleanup after the helper call - emitter.instruction("str x0, [sp, #8]"); // save the right boxed mixed pointer for cleanup after the helper call - emitter.instruction("mov x1, x0"); // move the right boxed mixed pointer into the second helper argument - emitter.instruction("mov x0, x10"); // move the left boxed mixed pointer into the first helper argument - abi::emit_call_label(emitter, "__rt_mixed_strict_eq"); // compare mixed values by runtime tag and payload - if !is_eq { - emitter.instruction("eor x0, x0, #1"); // invert the helper result for strict inequality - } - emitter.instruction("str x0, [sp, #16]"); // preserve the boolean comparison result across decref cleanup - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rsp + 48]"); // reload the boxed left mixed pointer from the lower saved-comparison slot - emitter.instruction("mov QWORD PTR [rsp], r10"); // save the left boxed mixed pointer for cleanup after the helper call - emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // save the right boxed mixed pointer for cleanup after the helper call - emitter.instruction("mov rsi, rax"); // move the right boxed mixed pointer into the second helper argument register - emitter.instruction("mov rdi, r10"); // move the left boxed mixed pointer into the first helper argument register - abi::emit_call_label(emitter, "__rt_mixed_strict_eq"); // compare mixed values by runtime tag and payload - if !is_eq { - emitter.instruction("xor rax, 1"); // invert the helper result for strict inequality - } - emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // preserve the boolean comparison result across decref cleanup - } - } - if release_left_mixed { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #0]"); // reload the left mixed operand that comparison cleanup owns - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rsp]"); // reload the left mixed operand that comparison cleanup owns - } - } - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the left mixed operand owned by this comparison expression - } - if release_right_mixed { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #8]"); // reload the right mixed operand that comparison cleanup owns - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rsp + 8]"); // reload the right mixed operand that comparison cleanup owns - } - } - abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the right mixed operand owned by this comparison expression - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #16]"); // restore the boolean comparison result after cleanup - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // restore the boolean comparison result after cleanup - } - } - abi::emit_release_temporary_stack(emitter, 64); // release the boxed-operand scratch space plus the two comparison spill slots - return PhpType::Bool; - } - - if lt != rt - && !matches!( - (<, &rt), - (PhpType::Pointer(_), PhpType::Pointer(_)) - | (PhpType::Buffer(_), PhpType::Buffer(_)) - ) - { - abi::emit_release_temporary_stack(emitter, 16); // discard the saved left operand from the temporary comparison stack - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), if is_eq { 0 } else { 1 }); // === yields false and !== yields true when the codegen types cannot match - return PhpType::Bool; - } - - match < { - PhpType::TaggedScalar => { - unreachable!("TaggedScalar strict comparison is routed through the mixed boxing path") - } - PhpType::Int | PhpType::Bool | PhpType::Void | PhpType::Never | PhpType::Resource(_) => { - let left_reg = abi::symbol_scratch_reg(emitter); - abi::emit_pop_reg(emitter, left_reg); // pop the saved left scalar or pointer-like value from the temporary comparison stack - emitter.instruction(&format!("cmp {}, {}", left_reg, abi::int_result_reg(emitter))); // compare the left and right scalar values - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("cset x0, {}", if is_eq { "eq" } else { "ne" })); // materialize the scalar strict-comparison result on AArch64 - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("set{} al", if is_eq { "e" } else { "ne" })); // materialize the scalar strict-comparison result in the low result byte on x86_64 - emitter.instruction("movzx rax, al"); // widen the x86_64 comparison byte back into the full integer result register - } - } - } - PhpType::Float => { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - abi::emit_pop_float_reg(emitter, "d1"); // pop the saved left float operand from the temporary comparison stack - emitter.instruction("fcmp d1, d0"); // compare the two doubles, setting NZCV flags - emitter.instruction(&format!("cset x0, {}", if is_eq { "eq" } else { "ne" })); // materialize the floating-point strict-comparison result on AArch64 - } - crate::codegen::platform::Arch::X86_64 => { - abi::emit_pop_float_reg(emitter, "xmm1"); // pop the saved left float operand from the temporary comparison stack - emitter.instruction("ucomisd xmm1, xmm0"); // compare the two doubles in the native x86_64 floating-point registers - emitter.instruction(&format!("set{} al", if is_eq { "e" } else { "ne" })); // materialize the floating-point strict-comparison result in the low result byte on x86_64 - emitter.instruction("movzx rax, al"); // widen the x86_64 comparison byte back into the full integer result register - } - } - } - PhpType::Str => { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("mov x3, x1"); // move the right string pointer into the third runtime argument register - emitter.instruction("mov x4, x2"); // move the right string length into the fourth runtime argument register - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // pop the left string pointer/length pair into the first runtime argument registers - abi::emit_call_label(emitter, "__rt_str_eq"); // compare the two strings byte-by-byte through the shared runtime helper - if !is_eq { - emitter.instruction("eor x0, x0, #1"); // invert the string equality result for strict inequality on AArch64 - } - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov rcx, rdx"); // move the right string length into the fourth SysV integer argument register - emitter.instruction("mov rdx, rax"); // move the right string pointer into the third SysV integer argument register - abi::emit_pop_reg_pair(emitter, "rdi", "rsi"); // pop the left string pointer/length pair into the first two SysV integer argument registers - abi::emit_call_label(emitter, "__rt_str_eq"); // compare the two strings byte-by-byte through the shared runtime helper - if !is_eq { - emitter.instruction("xor rax, 1"); // invert the string equality result for strict inequality on x86_64 - } - } - } - } - PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Iterable - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Buffer(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - let left_reg = abi::symbol_scratch_reg(emitter); - abi::emit_pop_reg(emitter, left_reg); // pop the saved left array/callable/object/iterable pointer from the temporary comparison stack - emitter.instruction(&format!("cmp {}, {}", left_reg, abi::int_result_reg(emitter))); // compare the two pointers for reference equality - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("cset x0, {}", if is_eq { "eq" } else { "ne" })); // materialize the pointer strict-comparison result on AArch64 - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("set{} al", if is_eq { "e" } else { "ne" })); // materialize the pointer strict-comparison result in the low result byte on x86_64 - emitter.instruction("movzx rax, al"); // widen the x86_64 comparison byte back into the full integer result register - } - } - } - PhpType::Mixed | PhpType::Union(_) => { - emitter.instruction("ldr x1, [sp], #16"); // pop the saved left boxed mixed pointer into the second helper argument - emitter.instruction("mov x9, x0"); // preserve the right boxed mixed pointer across the register shuffle - emitter.instruction("mov x0, x1"); // move the left boxed mixed pointer into the first helper argument - emitter.instruction("mov x1, x9"); // move the right boxed mixed pointer into the second helper argument - emitter.instruction("bl __rt_mixed_strict_eq"); // compare mixed values by runtime tag and payload instead of box identity - if !is_eq { - emitter.instruction("eor x0, x0, #1"); // invert the helper result for strict inequality - } - } - } - } else { - emit_expr(right, emitter, ctx, data); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), if is_eq { 0 } else { 1 }); // === always false and !== always true when the codegen types can never match - } - - PhpType::Bool -} - -/// Peeks the static `PhpType` of an expression from its compile-time type environment. -/// -/// Returns `Some(PhpType)` for expressions with known static types (literals, variables). -/// Returns `None` for expressions whose type cannot be determined at compile time -/// (function calls, complex binops, etc.). This is used to decide whether two operands -/// have "types match" at codegen time, enabling direct comparison instead of the -/// general mixed comparison path. -fn peek_expr_type(expr: &Expr, ctx: &Context) -> Option { - match &expr.kind { - ExprKind::IntLiteral(_) => Some(PhpType::Int), - ExprKind::FloatLiteral(_) => Some(PhpType::Float), - ExprKind::StringLiteral(_) => Some(PhpType::Str), - ExprKind::BoolLiteral(_) => Some(PhpType::Bool), - ExprKind::Null => Some(PhpType::Void), - ExprKind::Variable(name) => ctx.variables.get(name).map(|v| v.ty.clone()), - _ => None, - } -} - -/// Returns true if the expression produces an owned `Mixed` or `Union` result. -/// -/// An operand is "owned" when the comparison expression is responsible for releasing -/// its refcount after the comparison helper returns. This determines whether -/// `__rt_decref_mixed` must be emitted during cleanup for each operand. -fn owned_mixed_operand(expr: &Expr, ty: &PhpType) -> bool { - matches!(ty, PhpType::Mixed | PhpType::Union(_)) - && expr_result_heap_ownership(expr) == HeapOwnership::Owned -} diff --git a/src/codegen/expr/diagnostics.rs b/src/codegen/expr/diagnostics.rs deleted file mode 100644 index 6a74b5917c..0000000000 --- a/src/codegen/expr/diagnostics.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Purpose: -//! Emits runtime diagnostic setup used by expression paths that can fail or throw. -//! Keeps line, file, and fatal-message preparation close to the lowering sites that need it. -//! -//! Called from: -//! - `crate::codegen::expr` and runtime error call sites -//! -//! Key details: -//! - Diagnostic state must be set before helper calls that can unwind or terminate execution. - -use super::super::abi; -use super::super::context::Context; -use super::super::data_section::DataSection; -use super::super::emit::Emitter; -use super::emit_expr; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -/// Emits the `@` error-control operator around an expression. -/// -/// Pushes a suppression scope, evaluates the inner expression while preserving its -/// result across the pop call, then restores the result after leaving the scope. -/// Returns the type of the inner expression. -pub(super) fn emit_error_suppress( - inner: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("@ error-control scope"); - abi::emit_call_label(emitter, "__rt_diag_push_suppression"); // enter a runtime diagnostic-suppression scope before evaluating the operand - let ty = emit_expr(inner, emitter, ctx, data); - preserve_result(emitter, &ty); - abi::emit_call_label(emitter, "__rt_diag_pop_suppression"); // leave the diagnostic-suppression scope after the operand result is saved - restore_result(emitter, &ty); - ty -} - -/// Pushes the current expression result onto the stack to preserve it across a call. -fn preserve_result(emitter: &mut Emitter, ty: &PhpType) { - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); - } - _ => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - } -} - -/// Pops the preserved expression result from the stack back into the appropriate result register. -fn restore_result(emitter: &mut Emitter, ty: &PhpType) { - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - } - _ => { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - } - } -} diff --git a/src/codegen/expr/helpers.rs b/src/codegen/expr/helpers.rs deleted file mode 100644 index d750350548..0000000000 --- a/src/codegen/expr/helpers.rs +++ /dev/null @@ -1,214 +0,0 @@ -//! Purpose: -//! Provides shared expression lowering utilities for strings, arrays, nullable values, and runtime checks. -//! Keeps repeated assembly snippets out of individual expression feature emitters. -//! -//! Called from: -//! - `crate::codegen::expr` submodules -//! -//! Key details: -//! - Helpers must document and preserve the result registers and scratch registers they clobber. - -use super::super::context::{Context, HeapOwnership}; -use super::super::data_section::DataSection; -use super::super::emit::Emitter; -use super::{expr_result_heap_ownership, Expr, PhpType}; - -/// Increments the refcount of a borrowed heap argument if the expression result is not already owned. -pub(super) fn retain_borrowed_heap_arg(emitter: &mut Emitter, expr: &Expr, ty: &PhpType) { - if expr_result_heap_ownership(expr) == HeapOwnership::Owned { - return; - } - if matches!(ty, PhpType::Callable) { - crate::codegen::callable_descriptor::emit_retain_current_descriptor(emitter); - } else if ty.is_refcounted() { - crate::codegen::abi::emit_incref_if_refcounted(emitter, ty); - } -} - -/// Returns the wider of two PhpType for mixed-type expression results. -/// -/// The type priority is: Mixed > Union > Str > Float > (Int/Bool/Null) > Void. -/// When one operand is Void, returns the other type. Otherwise returns the -/// higher-priority type, or `a` if both are lower-priority types with no Void. -pub(super) fn widen_codegen_type(a: &PhpType, b: &PhpType) -> PhpType { - if a == b { - return a.clone(); - } - if matches!(a, PhpType::Mixed | PhpType::Union(_)) - || matches!(b, PhpType::Mixed | PhpType::Union(_)) - { - return PhpType::Mixed; - } - if *a == PhpType::Str || *b == PhpType::Str { - return PhpType::Str; - } - if matches!(a, PhpType::TaggedScalar) || matches!(b, PhpType::TaggedScalar) { - let other = if matches!(a, PhpType::TaggedScalar) { b } else { a }; - return match other { - PhpType::Int | PhpType::Bool | PhpType::Void | PhpType::TaggedScalar => { - PhpType::TaggedScalar - } - _ => PhpType::Mixed, - }; - } - if *a == PhpType::Float || *b == PhpType::Float { - return PhpType::Float; - } - if *a == PhpType::Void { - return b.clone(); - } - if *b == PhpType::Void { - return a.clone(); - } - a.clone() -} - -/// Emits runtime coercion from source_ty to target_ty using appropriate __rt_* helpers. -pub(crate) fn coerce_result_to_type( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - source_ty: &PhpType, - target_ty: &PhpType, -) { - if source_ty == target_ty { - return; - } - if matches!(source_ty, PhpType::Mixed | PhpType::Union(_)) { - match target_ty.codegen_repr() { - PhpType::Int | PhpType::Resource(_) => { - crate::codegen::abi::emit_call_label(emitter, "__rt_mixed_cast_int"); - } - PhpType::Pointer(_) => { - crate::codegen::abi::emit_call_label(emitter, "__rt_mixed_cast_int"); - } - PhpType::Bool => { - crate::codegen::abi::emit_call_label(emitter, "__rt_mixed_cast_bool"); - } - PhpType::Float => { - crate::codegen::abi::emit_call_label(emitter, "__rt_mixed_cast_float"); - } - PhpType::Str => { - super::coerce_to_string(emitter, ctx, data, source_ty); - } - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - crate::codegen::abi::emit_call_label(emitter, "__rt_mixed_unbox"); - emitter.instruction("mov x0, x1"); // use the unboxed heap payload word as the coerced pointer - } - crate::codegen::platform::Arch::X86_64 => { - crate::codegen::abi::emit_call_label(emitter, "__rt_mixed_unbox"); - emitter.instruction("mov rax, rdi"); // use the unboxed heap payload word as the coerced pointer - } - }, - PhpType::Mixed | PhpType::Union(_) => {} - PhpType::TaggedScalar => match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - crate::codegen::abi::emit_call_label(emitter, "__rt_mixed_unbox"); - emitter.instruction("mov x9, x1"); // stage the unboxed payload while the tag moves into the tag register - emitter.instruction("mov x1, x0"); // place the unboxed runtime tag in the tagged scalar tag register - emitter.instruction("mov x0, x9"); // place the unboxed payload in the tagged scalar payload register - } - crate::codegen::platform::Arch::X86_64 => { - crate::codegen::abi::emit_call_label(emitter, "__rt_mixed_unbox"); - emitter.instruction("mov rdx, rax"); // place the unboxed runtime tag in the tagged scalar tag register - emitter.instruction("mov rax, rdi"); // place the unboxed payload in the tagged scalar payload register - } - }, - _ => {} - } - } else if matches!(source_ty, PhpType::TaggedScalar) { - match target_ty.codegen_repr() { - PhpType::Int | PhpType::Bool | PhpType::Resource(_) | PhpType::Pointer(_) => { - crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(emitter); - } - PhpType::Float => { - crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(emitter); - crate::codegen::abi::emit_int_result_to_float_result(emitter); // widen the narrowed payload into the float result register - } - PhpType::Str => { - super::coerce_to_string(emitter, ctx, data, source_ty); - } - PhpType::Mixed | PhpType::Union(_) => { - crate::codegen::emit_box_current_value_as_mixed(emitter, source_ty); - } - _ => {} - } - } else if matches!(target_ty, PhpType::TaggedScalar) { - match source_ty { - PhpType::Int | PhpType::Bool => { - crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - } - PhpType::Void | PhpType::Never => { - crate::codegen::sentinels::emit_tagged_scalar_null(emitter); - } - _ => {} - } - } else if matches!(target_ty, PhpType::Mixed | PhpType::Union(_)) { - crate::codegen::emit_box_current_value_as_mixed(emitter, source_ty); - } else if *target_ty == PhpType::Str { - super::coerce_to_string(emitter, ctx, data, source_ty); - } else if *target_ty == PhpType::Float - && matches!(source_ty, PhpType::Int | PhpType::Bool | PhpType::Void) - { - if *source_ty == PhpType::Void { - emitter.instruction("mov x0, #0"); // null widens to numeric zero before float coercion - } - crate::codegen::abi::emit_int_result_to_float_result(emitter); // convert the integer-like result into the active target float-result register - } else if *target_ty == PhpType::Int && *source_ty == PhpType::Float { - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("fcvtzs x0, d0"); // truncate the float result to an integer for PHP coercion - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("cvttsd2si rax, xmm0"); // truncate the float result to an integer for PHP coercion - } - } - } -} - -/// Returns true if coerce_result_to_type would succeed for the given source/target pair. -pub(crate) fn can_coerce_result_to_type(source_ty: &PhpType, target_ty: &PhpType) -> bool { - if source_ty == target_ty { - return true; - } - if matches!(source_ty, PhpType::Mixed | PhpType::Union(_)) { - return matches!( - target_ty.codegen_repr(), - PhpType::Int - | PhpType::Resource(_) - | PhpType::Pointer(_) - | PhpType::Bool - | PhpType::Float - | PhpType::Str - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Object(_) - | PhpType::Mixed - | PhpType::Union(_) - ); - } - if matches!(source_ty, PhpType::TaggedScalar) { - return matches!( - target_ty.codegen_repr(), - PhpType::Int - | PhpType::Bool - | PhpType::Resource(_) - | PhpType::Pointer(_) - | PhpType::Float - | PhpType::Str - | PhpType::Mixed - | PhpType::Union(_) - ); - } - if matches!(target_ty, PhpType::TaggedScalar) { - return matches!( - source_ty, - PhpType::Int | PhpType::Bool | PhpType::Void | PhpType::Never - ); - } - matches!(target_ty, PhpType::Mixed | PhpType::Union(_)) - || *target_ty == PhpType::Str - || (*target_ty == PhpType::Float - && matches!(source_ty, PhpType::Int | PhpType::Bool | PhpType::Void)) -} diff --git a/src/codegen/expr/objects.rs b/src/codegen/expr/objects.rs deleted file mode 100644 index d719b7bc9b..0000000000 --- a/src/codegen/expr/objects.rs +++ /dev/null @@ -1,1087 +0,0 @@ -//! Purpose: -//! Groups object expression lowering for allocation, access, dispatch, static properties, nullsafe, and instanceof. -//! Provides the object-facing API used by the main expression dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Object results are refcounted handles whose metadata must match class tables and vtable layout. - -mod access; -mod allocation; -/// dispatch -pub(crate) mod dispatch; -mod fiber_callable; -mod fiber_wrapper; -mod instanceof; -mod nullsafe; -mod reflection; -mod static_properties; - -use super::super::context::Context; -use super::super::data_section::DataSection; -use super::super::emit::Emitter; -use super::scalars; -use crate::codegen::abi; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::platform::Arch; -use crate::names::php_symbol_key; -use crate::parser::ast::{Expr, ExprKind, InstanceOfTarget, StaticReceiver}; -use crate::types::PhpType; - -/// Emits `new ClassName(...)` for a known class with constructor args. -pub(crate) fn emit_new_object( - class_name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - allocation::emit_new_object(class_name, args, emitter, ctx, data) -} - -/// Emits `new $variable(...)` by resolving the runtime class-string to an AOT -/// allocation path. -/// -/// Known classes branch back into `allocation::emit_new_object`, so constructors -/// and builtin/SPL storage initialization follow the same path as `new Class`. -/// Misses still fall back to `__rt_new_by_name` to preserve the current null-on- -/// unknown behavior until the unsupported-class fatal path is tightened. -pub(crate) fn emit_new_dynamic( - name_expr: &Expr, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if let Some(class_name) = resolve_literal_dynamic_new_class_name(name_expr, ctx) { - return allocation::emit_new_object(&class_name, args, emitter, ctx, data); - } - - emitter.comment("new $variable()"); - crate::codegen::expr::emit_expr(name_expr, emitter, ctx, data); - let done_label = ctx.next_label("new_dynamic_done"); - let fallback_label = ctx.next_label("new_dynamic_fallback"); - let mut cases = Vec::new(); - abi::emit_push_result_value(emitter, &PhpType::Str); - - for class_name in sorted_dynamic_new_class_names(ctx) { - let label = ctx.next_label("new_dynamic_case"); - emit_branch_if_dynamic_new_class_name_matches(&class_name, &label, emitter, data); - cases.push((class_name, label)); - } - - abi::emit_jump(emitter, &fallback_label); // no AOT class-string case matched, so use the legacy registry fallback - - for (class_name, label) in cases { - emitter.label(&label); - abi::emit_release_temporary_stack(emitter, 16); // discard the saved dynamic class-string before constructing the selected class - allocation::emit_new_object(&class_name, args, emitter, ctx, data); - emit_box_current_object_result(emitter); - abi::emit_jump(emitter, &done_label); // skip the remaining dynamic-new cases after the selected allocation path succeeds - } - - emitter.label(&fallback_label); - emit_new_dynamic_fallback(emitter, ctx); - emitter.label(&done_label); - PhpType::Mixed -} - -/// Resolves a literal dynamic class-string to a known canonical class name. -fn resolve_literal_dynamic_new_class_name(name_expr: &Expr, ctx: &Context) -> Option { - let ExprKind::StringLiteral(class_name) = &name_expr.kind else { - return None; - }; - let class_key = php_symbol_key(class_name.trim_start_matches('\\')); - ctx.classes - .keys() - .find(|existing| php_symbol_key(existing) == class_key) - .cloned() -} - -/// Returns class names in stable class-id order for deterministic dynamic-new dispatch. -fn sorted_dynamic_new_class_names(ctx: &Context) -> Vec { - let mut classes: Vec<(u64, String)> = ctx - .classes - .iter() - .filter(|(name, _)| is_dynamic_new_aot_candidate(name)) - .map(|(name, info)| (info.class_id, name.clone())) - .collect(); - classes.sort_by_key(|(class_id, _)| *class_id); - classes.into_iter().map(|(_, name)| name).collect() -} - -/// Returns true when `class_name` can safely use the static allocation path for `new $name`. -fn is_dynamic_new_aot_candidate(class_name: &str) -> bool { - if class_name.starts_with("__Elephc") { - return false; - } - if supported_dynamic_new_builtin_class_names().contains(&class_name) { - return true; - } - !known_dynamic_new_builtin_class_names().contains(&class_name) -} - -/// Returns builtin class names with allocation paths that are safe for dynamic `new`. -pub(crate) fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { - &[ - "ArrayIterator", - "ArrayObject", - "BadFunctionCallException", - "BadMethodCallException", - "CallbackFilterIterator", - "DomainException", - "Error", - "Exception", - "Fiber", - "FiberError", - "InvalidArgumentException", - "IteratorIterator", - "JsonException", - "LengthException", - "LogicException", - "OutOfBoundsException", - "OutOfRangeException", - "OverflowException", - "RangeException", - "RecursiveCallbackFilterIterator", - "ReflectionClass", - "ReflectionMethod", - "ReflectionProperty", - "RuntimeException", - "SplDoublyLinkedList", - "SplFixedArray", - "SplQueue", - "SplStack", - "TypeError", - "UnderflowException", - "UnexpectedValueException", - "ValueError", - "stdClass", - ] -} - -/// Returns builtin class names that should not be mistaken for user classes. -/// -/// These synthetic builtin classes are emitted on demand (only when used), so their method symbols -/// are not guaranteed to exist in every program. Besides gating dynamic `new $x()`, this list is -/// also used to keep their static methods out of the dynamic-callable descriptor (see -/// `crate::codegen::callable_dispatch`), which would otherwise reference an unemitted symbol. -pub(crate) fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { - &[ - "AppendIterator", - "ArrayIterator", - "ArrayObject", - "BadFunctionCallException", - "BadMethodCallException", - "CachingIterator", - "CallbackFilterIterator", - "DateInterval", - "DatePeriod", - "DateTime", - "DateTimeImmutable", - "DateTimeInterface", - "DateTimeZone", - "DirectoryIterator", - "DomainException", - "EmptyIterator", - "Error", - "Exception", - "Fiber", - "FiberError", - "FilesystemIterator", - "FilterIterator", - "Generator", - "GlobIterator", - "InfiniteIterator", - "InternalIterator", - "InvalidArgumentException", - "IteratorIterator", - "JsonException", - "LengthException", - "LimitIterator", - "LogicException", - "MultipleIterator", - "NoRewindIterator", - "OutOfBoundsException", - "OutOfRangeException", - "OverflowException", - "ParentIterator", - "Phar", - "PharData", - "RangeException", - "RecursiveArrayIterator", - "RecursiveCachingIterator", - "RecursiveCallbackFilterIterator", - "RecursiveDirectoryIterator", - "RecursiveFilterIterator", - "RecursiveIteratorIterator", - "RecursiveRegexIterator", - "ReflectionAttribute", - "ReflectionClass", - "ReflectionMethod", - "ReflectionProperty", - "RegexIterator", - "RuntimeException", - "SplDoublyLinkedList", - "SplFileInfo", - "SplFileObject", - "SplFixedArray", - "SplHeap", - "SplMaxHeap", - "SplMinHeap", - "SplObjectStorage", - "SplPriorityQueue", - "SplQueue", - "SplStack", - "SplTempFileObject", - "TypeError", - "UnderflowException", - "UnexpectedValueException", - "ValueError", - "stdClass", - ] -} - -/// Emits a branch to `matched_label` when the saved dynamic class-string matches `class_name`. -fn emit_branch_if_dynamic_new_class_name_matches( - class_name: &str, - matched_label: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (candidate_label, candidate_len) = data.add_string(class_name.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x1", 0); - abi::emit_load_temporary_stack_slot(emitter, "x2", 8); - abi::emit_symbol_address(emitter, "x3", &candidate_label); - abi::emit_load_int_immediate(emitter, "x4", candidate_len as i64); - abi::emit_call_label(emitter, "__rt_strcasecmp"); - emitter.instruction("cmp x0, #0"); // did the dynamic class-string match this AOT class name case-insensitively? - emitter.instruction(&format!("b.eq {}", matched_label)); // select this class allocation path when the class-string matches - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rdi", 0); - abi::emit_load_temporary_stack_slot(emitter, "rsi", 8); - abi::emit_symbol_address(emitter, "rdx", &candidate_label); - abi::emit_load_int_immediate(emitter, "rcx", candidate_len as i64); - abi::emit_call_label(emitter, "__rt_strcasecmp"); - emitter.instruction("test rax, rax"); // did the dynamic class-string match this AOT class name case-insensitively? - emitter.instruction(&format!("je {}", matched_label)); // select this class allocation path when the class-string matches - } - } -} - -/// Boxes the current object result register into a `Mixed` object cell. -fn emit_box_current_object_result(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x1, x0"); // payload_lo = object pointer - emitter.instruction("mov x2, #0"); // object Mixed cells have no high payload - emitter.instruction("mov x0, #6"); // runtime tag 6 = object - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // payload_lo = object pointer - emitter.instruction("xor esi, esi"); // object Mixed cells have no high payload - emitter.instruction("mov eax, 6"); // runtime tag 6 = object - } - } - abi::emit_call_label(emitter, "__rt_mixed_from_value"); -} - -/// Invokes the legacy runtime dynamic-new registry and boxes object/null results. -fn emit_new_dynamic_fallback( - emitter: &mut Emitter, - ctx: &mut Context, -) { - let null_label = ctx.next_label("new_dynamic_null"); - let done_label = ctx.next_label("new_dynamic_fallback_done"); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg_pair(emitter, "x1", "x2"); // restore the saved dynamic class-string for the legacy registry lookup - abi::emit_call_label(emitter, "__rt_new_by_name"); - emitter.instruction(&format!("cbz x0, {}", null_label)); // null pointer -> box PHP null on a registry miss - emit_box_current_object_result(emitter); - emitter.instruction(&format!("b {}", done_label)); // skip null boxing after a successful registry allocation - emitter.label(&null_label); - emitter.instruction("mov x1, #0"); // null payload_lo - emitter.instruction("mov x2, #0"); // null payload_hi - emitter.instruction("mov x0, #8"); // runtime tag 8 = null - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - Arch::X86_64 => { - abi::emit_pop_reg_pair(emitter, "rax", "rdx"); // restore the saved dynamic class-string for the legacy registry lookup - abi::emit_call_label(emitter, "__rt_new_by_name"); - emitter.instruction("test rax, rax"); // did the registry miss this dynamic class name? - emitter.instruction(&format!("jz {}", null_label)); // box PHP null on a registry miss - emit_box_current_object_result(emitter); - emitter.instruction(&format!("jmp {}", done_label)); // skip null boxing after a successful registry allocation - emitter.label(&null_label); - emitter.instruction("xor edi, edi"); // null payload_lo - emitter.instruction("xor esi, esi"); // null payload_hi - emitter.instruction("mov eax, 8"); // runtime tag 8 = null - abi::emit_call_label(emitter, "__rt_mixed_from_value"); - emitter.label(&done_label); - } - } -} - -/// Emits a `new $class(...)`-style internal factory constrained to a parent class. -pub(crate) fn emit_new_dynamic_object( - class_name: &Expr, - fallback_class: &str, - required_parent: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!( - "new dynamic {} subclass from class-string", - required_parent - )); - let class_ty = super::emit_expr(class_name, emitter, ctx, data).codegen_repr(); - if !emit_prepare_dynamic_new_class_string(&class_ty, required_parent, emitter, ctx, data) { - return PhpType::Object(fallback_class.to_string()); - } - - abi::emit_call_label(emitter, "__rt_instanceof_lookup"); // resolve the requested dynamic factory class-string to class metadata - let invalid_label = ctx.next_label("dynamic_new_invalid"); - let unmatched_label = ctx.next_label("dynamic_new_unmatched"); - let done_label = ctx.next_label("dynamic_new_done"); - emit_branch_if_dynamic_new_lookup_invalid(&invalid_label, emitter); - emit_push_dynamic_new_class_id(emitter); - - let classes = sorted_dynamic_new_classes_by_id(required_parent, ctx); - let mut cases = Vec::new(); - for (_, class_id) in &classes { - let label = ctx.next_label("dynamic_new_case"); - emit_compare_dynamic_new_class_id(*class_id, &label, emitter); - cases.push(label); - } - abi::emit_jump(emitter, &unmatched_label); // report invalid factory classes that are outside the required parent hierarchy - - emitter.label(&unmatched_label); - abi::emit_release_temporary_stack(emitter, 16); // discard the unmatched resolved class id before aborting - emit_dynamic_new_fatal(required_parent, emitter, data); - - emitter.label(&invalid_label); - emit_dynamic_new_fatal(required_parent, emitter, data); - - for ((class_name, _), label) in classes.into_iter().zip(cases) { - emitter.label(&label); - abi::emit_release_temporary_stack(emitter, 16); // discard the resolved class id before constructing the selected class - allocation::emit_new_object(&class_name, args, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); // skip the remaining dynamic factory cases after construction - } - - emitter.label(&done_label); - PhpType::Object(fallback_class.to_string()) -} - -/// Normalizes a direct or boxed class-string into the ABI string-result registers. -fn emit_prepare_dynamic_new_class_string( - class_ty: &PhpType, - required_parent: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - match class_ty { - PhpType::Str => true, - PhpType::Mixed | PhpType::Union(_) => { - let ok_label = ctx.next_label("dynamic_new_class_string"); - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // unwrap nullable/mixed factory class names before class metadata lookup - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #1"); // runtime tag 1 means the factory argument is a string - emitter.instruction(&format!("b.eq {}", ok_label)); // continue only when the boxed factory argument is a class-string - emit_dynamic_new_fatal(required_parent, emitter, data); - emitter.label(&ok_label); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 1"); // runtime tag 1 means the factory argument is a string - emitter.instruction(&format!("je {}", ok_label)); // continue only when the boxed factory argument is a class-string - emit_dynamic_new_fatal(required_parent, emitter, data); - emitter.label(&ok_label); - emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the lookup input register - } - } - true - } - _ => { - emit_dynamic_new_fatal(required_parent, emitter, data); - false - } - } -} - -/// Emits a dynamic property access where the property name is a runtime expression. -pub(crate) fn emit_dynamic_property_access( - object: &Expr, - property: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - access::emit_dynamic_property_access(object, property, false, emitter, ctx, data) -} - -/// Emits a nullsafe dynamic property access (`?->`). -pub(crate) fn emit_nullsafe_dynamic_property_access( - object: &Expr, - property: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - access::emit_dynamic_property_access(object, property, true, emitter, ctx, data) -} - -/// Emits a property access on a `Mixed`-typed receiver by name. -pub(crate) fn emit_mixed_property_access( - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - access::emit_mixed_property_access(property, emitter, ctx, data) -} - -/// Resolves a `StaticReceiver` (`self`/`parent`/`Named`) to a class name string. -/// Returns `None` for `Static` (late-bound) which must be handled at runtime. -fn resolve_scoped_receiver_to_class(receiver: &StaticReceiver, ctx: &Context) -> Option { - match receiver { - StaticReceiver::Self_ => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|c| ctx.classes.get(c)) - .and_then(|info| info.parent.clone()), - StaticReceiver::Named(name) => Some(name.as_canonical()), - StaticReceiver::Static => None, - } -} - -/// Emits a class constant access for `self`/`parent`/`Named` receivers. -/// For `Static` receivers, dispatches to `emit_late_bound_class_constant` at runtime. -pub(super) fn emit_class_constant( - receiver: &StaticReceiver, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if matches!(receiver, StaticReceiver::Static) { - return emit_late_bound_class_constant(emitter, ctx, data); - } - - let name = resolve_scoped_receiver_to_class(receiver, ctx).unwrap_or_default(); - scalars::emit_string_literal(&name, emitter, data) -} - -/// Emits a scoped constant access (self/parent/named receiver with constant name). -pub(super) fn emit_scoped_constant_access( - receiver: &StaticReceiver, - name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let class_name = resolve_scoped_receiver_to_class(receiver, ctx) - .expect("ScopedConstantAccess on `static` not supported yet"); - // Enum case: dispatch to the existing enum codegen. - if ctx.enums.contains_key(&class_name) { - return emit_enum_case(&class_name, name, emitter, ctx); - } - // Class constant: walk parent chain. - let mut current: Option = Some(class_name.clone()); - let mut value: Option = None; - while let Some(cn) = current.as_deref() { - if let Some(info) = ctx.classes.get(cn) { - if let Some(v) = info.constants.get(name).cloned() { - value = Some(v); - break; - } - current = info.parent.clone(); - } else { - break; - } - } - if value.is_none() { - // Search interfaces (and parent interfaces) the class implements. - let mut visited: std::collections::HashSet = Default::default(); - let mut queue: Vec = ctx - .classes - .get(&class_name) - .map(|info| info.interfaces.clone()) - .unwrap_or_default(); - // Direct interface receiver: include the receiver itself. - queue.push(class_name.clone()); - while let Some(iface_name) = queue.pop() { - if !visited.insert(iface_name.clone()) { - continue; - } - if let Some(info) = ctx.interfaces.get(&iface_name) { - if let Some(v) = info.constants.get(name).cloned() { - value = Some(v); - break; - } - queue.extend(info.parents.iter().cloned()); - } - } - } - let value = value.expect("type checker rejected unresolved class constant"); - super::emit_expr(&value, emitter, ctx, data) -} - -/// Emits `new self/parent/Static(...)` with a late-bound class. -pub(super) fn emit_new_scoped_object( - receiver: &StaticReceiver, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if matches!(receiver, StaticReceiver::Static) { - return emit_late_bound_new_static(args, emitter, ctx, data); - } - - let class_name = resolve_scoped_receiver_to_class(receiver, ctx) - .expect("new self/parent/static used outside class context — should be a type error"); - allocation::emit_new_object(&class_name, args, emitter, ctx, data) -} - -/// Collects all classes in the current inheritance hierarchy (same class or descendants) -/// sorted by class ID, used for late-static-binding dispatch tables. -fn sorted_late_bound_classes_by_id(ctx: &Context) -> Vec<(String, u64)> { - let Some(base_class) = ctx.current_class.as_deref() else { - return Vec::new(); - }; - let mut classes: Vec<(String, u64)> = ctx - .classes - .iter() - .filter(|(name, _)| class_is_same_or_descends_from(name, base_class, ctx)) - .map(|(name, info)| (name.clone(), info.class_id)) - .collect(); - classes.sort_by_key(|(_, class_id)| *class_id); - classes -} - -/// Returns true if `class_name` is the same as `base_class` or descends from it. -fn class_is_same_or_descends_from(class_name: &str, base_class: &str, ctx: &Context) -> bool { - let mut current = Some(class_name); - while let Some(name) = current { - if class_names_match(name, base_class) { - return true; - } - current = ctx.classes.get(name).and_then(|info| info.parent.as_deref()); - } - false -} - -/// Compares PHP class names using the same case-insensitive key used by symbol tables. -fn class_names_match(left: &str, right: &str) -> bool { - php_symbol_key(left.trim_start_matches('\\')) == php_symbol_key(right.trim_start_matches('\\')) -} - -/// Collects all concrete dynamic factory targets that satisfy the required parent. -fn sorted_dynamic_new_classes_by_id( - required_parent: &str, - ctx: &Context, -) -> Vec<(String, u64)> { - let mut classes: Vec<(String, u64)> = ctx - .classes - .iter() - .filter(|(name, _)| class_is_same_or_descends_from(name, required_parent, ctx)) - .map(|(name, info)| (name.clone(), info.class_id)) - .collect(); - classes.sort_by_key(|(_, class_id)| *class_id); - classes -} - -/// Branches when the dynamic factory class-string lookup failed or resolved to an interface. -fn emit_branch_if_dynamic_new_lookup_invalid(invalid_label: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the dynamic factory class-string resolve to metadata? - emitter.instruction(&format!("b.eq {}", invalid_label)); // abort unresolved factory classes before constructor arguments are evaluated - emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface - emitter.instruction(&format!("b.ne {}", invalid_label)); // abort interface targets because factories must instantiate objects - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the dynamic factory class-string resolve to metadata? - emitter.instruction(&format!("je {}", invalid_label)); // abort unresolved factory classes before constructor arguments are evaluated - emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface - emitter.instruction(&format!("jne {}", invalid_label)); // abort interface targets because factories must instantiate objects - } - } -} - -/// Preserves the resolved dynamic factory class id on the temporary stack. -fn emit_push_dynamic_new_class_id(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => abi::emit_push_reg(emitter, "x1"), - Arch::X86_64 => abi::emit_push_reg(emitter, "rdi"), - } -} - -/// Compares the saved dynamic factory class id with a concrete candidate class. -fn emit_compare_dynamic_new_class_id( - class_id: u64, - matched_label: &str, - emitter: &mut Emitter, -) { - let scratch = abi::temp_int_reg(emitter.target); - abi::emit_load_temporary_stack_slot(emitter, scratch, 0); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, #{}", scratch, class_id)); // compare the requested factory class with this concrete class id - emitter.instruction(&format!("b.eq {}", matched_label)); // branch when the runtime class-string selected this constructor - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", scratch, class_id)); // compare the requested factory class with this concrete class id - emitter.instruction(&format!("je {}", matched_label)); // branch when the runtime class-string selected this constructor - } - } -} - -/// Emits a fatal diagnostic for invalid dynamic SPL factory class names. -fn emit_dynamic_new_fatal(required_parent: &str, emitter: &mut Emitter, data: &mut DataSection) { - let message = format!( - "Fatal error: Dynamic factory class must extend {}\n", - required_parent - ); - let (message_label, message_len) = data.add_string(message.as_bytes()); - emit_fatal_message(emitter, &message_label, message_len); -} - -/// Unboxes a Mixed value and emits a fatal if it is null instead of an object. -pub(crate) fn emit_unbox_mixed_object_or_fatal( - message: &[u8], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let (message_label, message_len) = data.add_string(message); - let ok_label = ctx.next_label("mixed_object_not_null"); - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect the boxed nullable object before member access - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #8"); // runtime tag 8 means the nullable receiver is null - emitter.instruction(&format!("b.ne {}", ok_label)); // continue only for a real object payload - emit_fatal_message(emitter, &message_label, message_len); - emitter.label(&ok_label); - emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the AArch64 result register - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 8"); // runtime tag 8 means the nullable receiver is null - emitter.instruction(&format!("jne {}", ok_label)); // continue only for a real object payload - emit_fatal_message(emitter, &message_label, message_len); - emitter.label(&ok_label); - emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the SysV result register - } - } -} - -/// Unboxes a boxed Mixed receiver to a raw object pointer for dynamic dispatch. -/// -/// Calls `__rt_mixed_unbox` (runtime tag in the int result register, payload in -/// the secondary register) and fatals with `message` unless the tag is 6 -/// (object). On success the object pointer is promoted into the int result -/// register. Used when a method is called on a `Mixed` / union receiver whose -/// static type does not name a single class, so the value must be confirmed to -/// be an object before its class id is read for dispatch. -pub(crate) fn emit_unbox_mixed_object_strict_or_fatal( - message: &[u8], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let (message_label, message_len) = data.add_string(message); - let ok_label = ctx.next_label("mixed_object_strict_ok"); - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect the boxed receiver before reading its class id - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #6"); // runtime tag 6 means the receiver is an object - emitter.instruction(&format!("b.eq {}", ok_label)); // dispatch only for a real object payload - emit_fatal_message(emitter, &message_label, message_len); - emitter.label(&ok_label); - emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the AArch64 result register - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 6"); // runtime tag 6 means the receiver is an object - emitter.instruction(&format!("je {}", ok_label)); // dispatch only for a real object payload - emit_fatal_message(emitter, &message_label, message_len); - emitter.label(&ok_label); - emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the SysV result register - } - } -} - -/// Emits a fatal-error diagnostic with `message` and terminates the process. -/// -/// Convenience wrapper that interns the message in the data section and delegates -/// to `emit_fatal_message`. Used by callers outside this module (e.g. dynamic -/// method dispatch) that need an unconditional fatal with a runtime message. -pub(crate) fn emit_fatal_str(message: &str, emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string(message.as_bytes()); - emit_fatal_message(emitter, &message_label, message_len); -} - -/// Emits a null-check branch on a Mixed-object unbox result for nullsafe flows. -pub(super) fn emit_unbox_mixed_object_or_null_branch(null_label: &str, emitter: &mut Emitter) { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect the boxed nullable object before member access - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #8"); // runtime tag 8 means the nullable receiver is null - emitter.instruction(&format!("b.eq {}", null_label)); // branch to the PHP null receiver path instead of dereferencing it - emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the AArch64 result register - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 8"); // runtime tag 8 means the nullable receiver is null - emitter.instruction(&format!("je {}", null_label)); // branch to the PHP null receiver path instead of dereferencing it - emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the SysV result register - } - } -} - -/// Emits a runtime warning diagnostic with the given message. -pub(super) fn emit_runtime_warning( - message: &[u8], - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (message_label, message_len) = data.add_string(message); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", &message_label); // load the page containing the runtime warning text - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the runtime warning byte length to the diagnostic helper - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &message_label); // pass the runtime warning text pointer to the diagnostic helper - emitter.instruction(&format!("mov esi, {}", message_len)); // pass the runtime warning byte length to the diagnostic helper - } - } - abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the runtime warning under the current @ scope -} - -/// Emits a boxed null value (tagged nullable pointer) into expression result registers. -pub(super) fn emit_boxed_null(emitter: &mut Emitter) { - abi::emit_load_int_immediate( - emitter, - abi::int_result_reg(emitter), - NULL_SENTINEL, - ); - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Void); -} - -/// Boxes the current expression result as Mixed if the result type is not already Mixed. -pub(super) fn box_nullable_result(result_ty: &PhpType, emitter: &mut Emitter) { - if !matches!(result_ty.codegen_repr(), PhpType::Mixed) { - crate::codegen::emit_box_current_value_as_mixed(emitter, result_ty); - } -} - -/// Emits the fatal-message sequence (write to stderr + exit) for null object derefs. -fn emit_fatal_message(emitter: &mut Emitter, message_label: &str, message_len: usize) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // fd = stderr for the nullable-object fatal diagnostic - abi::emit_symbol_address(emitter, "x1", message_label); // load the page containing the nullable-object fatal diagnostic - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the nullable-object fatal diagnostic length to write() - emitter.syscall(4); - emitter.instruction("mov x0, #1"); // exit status 1 indicates abnormal termination - emitter.syscall(1); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", message_label); // point the Linux write buffer at the nullable-object fatal diagnostic - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the nullable-object fatal diagnostic length to write() - emitter.instruction("mov edi, 2"); // fd = stderr for the nullable-object fatal diagnostic - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the nullable-object fatal diagnostic - emitter.instruction("mov edi, 1"); // exit status 1 indicates abnormal termination - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate after reporting the nullable-object fatal diagnostic - } - } -} - -/// Emits the forwarded called-class ID or falls back to the lexical current-class ID. -fn emit_late_bound_class_id_or_lexical_fallback(emitter: &mut Emitter, ctx: &Context) { - if !dispatch::emit_forwarded_called_class_id(emitter, ctx) { - let class_id = ctx - .current_class - .as_ref() - .and_then(|name| ctx.classes.get(name)) - .map(|info| info.class_id) - .unwrap_or(0); - dispatch::emit_immediate_class_id(emitter, class_id); - } -} - -/// Emits a comparison of the forwarded called-class ID against a concrete class ID, -/// branching to `matched_label` if they match. -fn emit_compare_current_class_id(emitter: &mut Emitter, class_id: u64, matched_label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp x0, #{}", class_id)); // compare the forwarded called-class id against this concrete class id - emitter.instruction(&format!("b.eq {}", matched_label)); // branch to the matching late-static-binding case - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp rax, {}", class_id)); // compare the forwarded called-class id against this concrete class id - emitter.instruction(&format!("je {}", matched_label)); // branch to the matching late-static-binding case - } - } -} - -/// Emits a late-bound class constant using the forwarded called-class ID, -/// branching to the matching class's constant, with a lexical fallback. -fn emit_late_bound_class_constant( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let classes = sorted_late_bound_classes_by_id(ctx); - let done_label = ctx.next_label("static_class_done"); - let fallback_name = ctx.current_class.clone().unwrap_or_default(); - - emit_late_bound_class_id_or_lexical_fallback(emitter, ctx); - let mut cases = Vec::new(); - for (_, class_id) in &classes { - let label = ctx.next_label("static_class_case"); - emit_compare_current_class_id(emitter, *class_id, &label); - cases.push(label); - } - - scalars::emit_string_literal(&fallback_name, emitter, data); - abi::emit_jump(emitter, &done_label); // skip late-static-binding class-name cases after using the lexical fallback - - for ((class_name, _), label) in classes.into_iter().zip(cases) { - emitter.label(&label); - scalars::emit_string_literal(&class_name, emitter, data); - abi::emit_jump(emitter, &done_label); // finish after materializing the matched late-bound class name - } - - emitter.label(&done_label); - PhpType::Str -} - -/// Emits a `new static(...)` call using the forwarded called-class ID, -/// branching to the matching class's constructor, with a lexical fallback. -fn emit_late_bound_new_static( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let classes = sorted_late_bound_classes_by_id(ctx); - let done_label = ctx.next_label("new_static_done"); - let fallback_class = ctx.current_class.clone().unwrap_or_default(); - - emit_late_bound_class_id_or_lexical_fallback(emitter, ctx); - let mut cases = Vec::new(); - for (_, class_id) in &classes { - let label = ctx.next_label("new_static_case"); - emit_compare_current_class_id(emitter, *class_id, &label); - cases.push(label); - } - - if !fallback_class.is_empty() { - allocation::emit_new_object(&fallback_class, args, emitter, ctx, data); - } - abi::emit_jump(emitter, &done_label); // skip concrete new-static cases after the lexical fallback - - for ((class_name, _), label) in classes.into_iter().zip(cases) { - emitter.label(&label); - allocation::emit_new_object(&class_name, args, emitter, ctx, data); - abi::emit_jump(emitter, &done_label); // finish after constructing the matched late-bound class - } - - emitter.label(&done_label); - PhpType::Object(fallback_class) -} - -/// Emits a direct property access on a known class with a literal property name. -pub(super) fn emit_property_access( - object: &Expr, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - access::emit_property_access(object, property, emitter, ctx, data) -} - -/// Emits a property access on a nullable class where the class is known at codegen time. -pub(super) fn emit_nullable_object_property_access( - class_name: &str, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - access::emit_nullable_object_property_access(class_name, property, emitter, ctx, data) -} - -/// Emits a property access where the class is known but property is dynamically loaded. -pub(super) fn emit_loaded_object_property_access( - class_name: &str, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - access::emit_loaded_object_property_access(class_name, property, emitter, ctx, data) -} - -/// Emits a nullsafe property access (`?.property`). -pub(super) fn emit_nullsafe_property_access( - object: &Expr, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - nullsafe::emit_nullsafe_property_access(object, property, emitter, ctx, data) -} - -/// Emits a static property access (`StaticClass::$property`). -pub(super) fn emit_static_property_access( - receiver: &StaticReceiver, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - static_properties::emit_static_property_access(receiver, property, emitter, ctx, data) -} - -/// Emits a `ClassName::Case` enum case singleton load. -pub(super) fn emit_enum_case( - enum_name: &str, - case_name: &str, - emitter: &mut Emitter, - _ctx: &mut Context, -) -> PhpType { - let label = crate::names::enum_case_symbol(enum_name, case_name); - emitter.comment(&format!("load enum case {}::{}", enum_name, case_name)); - crate::codegen::abi::emit_load_symbol_to_reg( - emitter, - crate::codegen::abi::int_result_reg(emitter), - &label, - 0, - ); // load the enum singleton pointer from its global slot through the target-aware symbol helper - PhpType::Object(enum_name.to_string()) -} - -/// Pushes a magic `__property` name as a string argument pair for `__get`/`__set` calls. -pub(crate) fn push_magic_property_name_arg( - property: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (label, len) = data.add_string(property.as_bytes()); - let (ptr_reg, len_reg) = crate::codegen::abi::string_result_regs(emitter); - crate::codegen::abi::emit_symbol_address(emitter, ptr_reg, &label); // materialize the magic-property name string address for the active target ABI - crate::codegen::abi::emit_load_int_immediate(emitter, len_reg, len as i64); // materialize the magic-property name length for the active target ABI - crate::codegen::abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // push the magic-property name argument pair onto the temporary call stack -} - -/// Returns `[method_name_string, args_array]` for `__call`/`__callStatic` magic dispatch. -pub(super) fn magic_method_args(method: &str, args: &[Expr], span: crate::span::Span) -> Vec { - vec![ - Expr::new(ExprKind::StringLiteral(method.to_string()), span), - Expr::new(ExprKind::ArrayLiteral(args.to_vec()), span), - ] -} - -/// Emits an instance method call (`$object->method(...)`). -pub(crate) fn emit_method_call( - object: &Expr, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - dispatch::emit_method_call(object, method, args, emitter, ctx, data) -} - -/// Emits a nullsafe method call (`?->method(...)`). -pub(super) fn emit_nullsafe_method_call( - object: &Expr, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - nullsafe::emit_nullsafe_method_call(object, method, args, emitter, ctx, data) -} - -/// Emits a method call on a known class with args already pushed to the stack. -pub(crate) fn emit_method_call_with_pushed_args( - class_name: &str, - method: &str, - arg_types: &[PhpType], - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - dispatch::emit_method_call_with_pushed_args(class_name, method, arg_types, 0, emitter, ctx) -} - -/// Emits a method call with the receiver saved below the pushed args on the stack. -pub(super) fn emit_method_call_with_saved_receiver_below_args( - class_name: &str, - method: &str, - arg_types: &[PhpType], - source_temp_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - dispatch::emit_method_call_with_saved_receiver_below_args( - class_name, - method, - arg_types, - source_temp_bytes, - emitter, - ctx, - ) -} - -/// Emits the args portion of a method call when args have already been pushed. -pub(super) fn emit_pushed_method_args( - args: &[Expr], - sig: Option<&crate::types::FunctionSig>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> super::calls::args::EmittedCallArgs { - dispatch::emit_pushed_method_args(args, sig, emitter, ctx, data) -} - -/// Emits a static method call (`ClassName::method(...)` or `self/parent/static`). -pub(crate) fn emit_static_method_call( - receiver: &StaticReceiver, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - dispatch::emit_static_method_call(receiver, method, args, emitter, ctx, data) -} - -/// Emits an instanceof type check expression. -pub(super) fn emit_instanceof( - value: &Expr, - target: &InstanceOfTarget, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - instanceof::emit_instanceof(value, target, emitter, ctx, data) -} diff --git a/src/codegen/expr/objects/access.rs b/src/codegen/expr/objects/access.rs deleted file mode 100644 index 35aa10c593..0000000000 --- a/src/codegen/expr/objects/access.rs +++ /dev/null @@ -1,910 +0,0 @@ -//! Purpose: -//! Lowers property reads, magic access paths, and nullable object field loads. -//! Produces object-related expression results while respecting runtime metadata and ownership rules. -//! -//! Called from: -//! - `crate::codegen::expr::objects` -//! -//! Key details: -//! - Object handles, property storage, and class ids must stay consistent with emitted class tables. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::functions; -use crate::codegen::platform::Arch; -use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::super::{coerce_result_to_type, emit_expr}; - -/// Lowers `$obj->property` where the receiver type is known at compile time. -pub(super) fn emit_property_access( - object: &Expr, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - // Resolve the receiver's static class up-front so a nullable object - // union (`?Foo`) routes through the same path as a direct object type. - // Direct object receivers produce a raw object pointer, while nullable - // unions produce a boxed mixed cell that must be checked and unboxed - // before the normal property load. - let static_obj_ty = functions::infer_contextual_type(object, ctx); - let static_class = functions::singular_object_class(&static_obj_ty) - .map(|name| name.to_string()); - let obj_ty = emit_expr(object, emitter, ctx, data); - if let Some(class_name) = static_class.as_ref() { - if matches!(obj_ty, PhpType::Mixed | PhpType::Union(_)) { - return emit_nullable_object_property_access(class_name, property, emitter, ctx, data); - } - if matches!(obj_ty, PhpType::Object(_)) { - return emit_loaded_object_property_access(class_name, property, emitter, ctx, data); - } - } - let (class_name, prop_ty, offset, needs_deref, is_reference) = match &obj_ty { - PhpType::Object(class_name) => { - return emit_loaded_object_property_access(class_name, property, emitter, ctx, data); - } - PhpType::Mixed => { - return emit_mixed_property_access(property, emitter, ctx, data); - } - PhpType::Pointer(Some(class_name)) if ctx.extern_classes.contains_key(class_name) => { - let class_info = match ctx.extern_classes.get(class_name).cloned() { - Some(c) => c, - None => { - emitter.comment(&format!("WARNING: undefined extern class {}", class_name)); - return PhpType::Int; - } - }; - - let field = match class_info - .fields - .iter() - .find(|field| field.name == property) - { - Some(field) => field.clone(), - None => { - emitter.comment(&format!("WARNING: undefined extern field {}", property)); - return PhpType::Int; - } - }; - - (class_name.clone(), field.php_type, field.offset, true, false) - } - PhpType::Pointer(Some(class_name)) if ctx.packed_classes.contains_key(class_name) => { - let class_info = match ctx.packed_classes.get(class_name).cloned() { - Some(c) => c, - None => { - emitter.comment(&format!("WARNING: undefined packed class {}", class_name)); - return PhpType::Int; - } - }; - - let field = match class_info - .fields - .iter() - .find(|field| field.name == property) - { - Some(field) => field.clone(), - None => { - emitter.comment(&format!("WARNING: undefined packed field {}", property)); - return PhpType::Int; - } - }; - - (class_name.clone(), field.php_type, field.offset, true, false) - } - _ => { - emitter.comment("WARNING: property access on non-object"); - return PhpType::Int; - } - }; - - if needs_deref { - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with fatal error on null pointer dereference - emitter.comment(&format!( - "->{} via ptr<{}> (offset {})", - property, class_name, offset - )); - } else { - emitter.comment(&format!("->{} (offset {})", property, offset)); - } - - let object_reg = abi::int_result_reg(emitter); - - if is_reference { - let pointer_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_from_address(emitter, pointer_reg, object_reg, offset); - match &prop_ty { - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_from_address(emitter, ptr_reg, pointer_reg, 0); - abi::emit_load_from_address(emitter, len_reg, pointer_reg, 8); - } - PhpType::Float => { - abi::emit_load_from_address(emitter, abi::float_result_reg(emitter), pointer_reg, 0); - } - PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never | PhpType::Resource(_) => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - } - PhpType::TaggedScalar => { - unreachable!("nullable scalar properties use the boxed Mixed representation") - } - PhpType::Iterable - | PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - } - } - return prop_ty; - } - - match &prop_ty { - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - let base_reg = abi::symbol_scratch_reg(emitter); - emitter.instruction(&format!("mov {}, {}", base_reg, object_reg)); // preserve the object base pointer while loading the two-word string property payload - abi::emit_load_from_address(emitter, ptr_reg, base_reg, offset); - abi::emit_load_from_address(emitter, len_reg, base_reg, offset + 8); - } - PhpType::Float => { - abi::emit_load_from_address(emitter, abi::float_result_reg(emitter), object_reg, offset); - } - PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never | PhpType::Resource(_) => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - } - PhpType::TaggedScalar => { - unreachable!("nullable scalar properties use the boxed Mixed representation") - } - PhpType::Iterable - | PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - } - } - - prop_ty -} - -/// Lower a `$obj->name` read where `$obj` has type `Object("stdClass")`. -/// -/// stdClass has no static property layout, so route the access through the -/// runtime helper `__rt_stdclass_get`. The receiver is already in -/// int_result_reg (x0/rax) at this point thanks to `emit_property_access`. -fn emit_stdclass_property_access( - property: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) -> PhpType { - emit_named_dynamic_property_access( - property, - emitter, - data, - "stdClass", - "__rt_stdclass_get", - ) -} - -/// Lower a `$obj->name` read where `$obj` has type `Mixed`. -/// -/// The runtime helper unboxes the Mixed cell, validates that it carries a -/// stdClass instance, and routes to `__rt_stdclass_get`. Other payloads -/// return Mixed(null), matching PHP's "property access on non-object" -/// warning behaviour for the most common idiom (`json_decode($json)->name`). -pub(super) fn emit_mixed_property_access( - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let candidates = declared_property_candidates(property, ctx); - if candidates.is_empty() { - return emit_named_dynamic_property_access( - property, - emitter, - data, - "mixed", - "__rt_mixed_property_get", - ); - } - - emitter.comment(&format!("mixed->{} (class-id dispatch)", property)); - let null_label = ctx.next_label("mixed_prop_null"); - let done_label = ctx.next_label("mixed_prop_done"); - let stdclass_label = ctx.next_label("mixed_prop_stdclass"); - let match_labels: Vec = candidates - .iter() - .map(|(class_name, _, _)| { - ctx.next_label(&format!("mixed_prop_{}", label_fragment(class_name))) - }) - .collect(); - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect the boxed receiver before reading a declared property - emit_object_payload_or_null_branch(&null_label, emitter); - emit_branch_to_declared_property_candidates(&candidates, &match_labels, emitter); - emit_branch_to_stdclass_fallback(&stdclass_label, emitter); - abi::emit_jump(emitter, &null_label); // unknown object class falls through to the null result path - - for ((class_name, prop_ty, _), label) in candidates.into_iter().zip(match_labels) { - emitter.label(&label); - let loaded_ty = emit_loaded_object_property_access(&class_name, property, emitter, ctx, data); - box_dynamic_property_result(&loaded_ty, emitter); - abi::emit_jump(emitter, &done_label); // finish the mixed property read after boxing the declared slot value - let _ = prop_ty; - } - - emitter.label(&stdclass_label); - emit_static_stdclass_get_from_loaded_object(property, emitter, data); - abi::emit_jump(emitter, &done_label); // finish after stdClass hash lookup - - emitter.label(&null_label); - super::emit_boxed_null(emitter); - - emitter.label(&done_label); - PhpType::Mixed -} - -/// Emits named dynamic property access for this module. -fn emit_named_dynamic_property_access( - property: &str, - emitter: &mut Emitter, - data: &mut DataSection, - receiver_label: &str, - runtime_symbol: &str, -) -> PhpType { - emitter.comment(&format!("{}->{} (dynamic)", receiver_label, property)); - let (label, len) = data.add_string(property.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", &label); - abi::emit_load_int_immediate(emitter, "x2", len as i64); - emitter.instruction(&format!("bl {}", runtime_symbol)); // call the dynamic-property reader; result Mixed* lands in x0 - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // shift the receiver into the SysV first-arg register - abi::emit_symbol_address(emitter, "rsi", &label); - abi::emit_load_int_immediate(emitter, "rdx", len as i64); - emitter.instruction(&format!("call {}", runtime_symbol)); // call the dynamic-property reader; result Mixed* lands in rax - } - } - PhpType::Mixed -} - -/// Lowers `$obj->{$expr}` where property name is a dynamic expression. -pub(super) fn emit_dynamic_property_access( - object: &Expr, - property: &Expr, - nullsafe: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let static_obj_ty = functions::infer_contextual_type(object, ctx); - let static_class = functions::singular_object_class(&static_obj_ty) - .map(|name| name.to_string()); - let obj_ty = emit_expr(object, emitter, ctx, data); - - if nullsafe && matches!(obj_ty.codegen_repr(), PhpType::Void) { - super::emit_boxed_null(emitter); - return PhpType::Mixed; - } - - let null_label = nullsafe.then(|| ctx.next_label("dynamic_prop_null")); - let done_label = nullsafe.then(|| ctx.next_label("dynamic_prop_done")); - if nullsafe && matches!(obj_ty.codegen_repr(), PhpType::Mixed) { - super::emit_unbox_mixed_object_or_null_branch( - null_label - .as_deref() - .expect("nullsafe dynamic access must have a null label"), - emitter, - ); - } - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the receiver while the dynamic property-name expression is evaluated - let property_ty = emit_expr(property, emitter, ctx, data); - if property_ty != PhpType::Str { - coerce_result_to_type(emitter, ctx, data, &property_ty, &PhpType::Str); - } - let (name_ptr_reg, name_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, name_ptr_reg, name_len_reg); // preserve the evaluated property name for each runtime-name comparison - - if let Some(class_name) = static_class - .or_else(|| match &obj_ty { - PhpType::Object(class_name) => Some(class_name.clone()), - _ => None, - }) - { - emit_dynamic_declared_property_lookup(&class_name, emitter, ctx, data); - } else { - emit_runtime_dynamic_property_get_from_saved_receiver( - "__rt_mixed_property_get", - emitter, - ); - } - - if let (Some(null_label), Some(done_label)) = (null_label, done_label) { - abi::emit_jump(emitter, &done_label); // skip the nullsafe null branch after a real dynamic-property lookup - emitter.label(&null_label); - super::emit_boxed_null(emitter); - emitter.label(&done_label); - } - - PhpType::Mixed -} - -/// Emits a runtime dispatch over all classes that declare `property`. -/// -/// Scans `ctx.classes` for every class that has `property` as a declared -/// property, builds a match table keyed by class id, and falls through to -/// `emit_dynamic_property_miss` when no declared name matches the evaluated -/// dynamic property name. -fn emit_dynamic_declared_property_lookup( - class_name: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - if crate::types::checker::builtin_stdclass::is_stdclass(class_name) { - emit_runtime_dynamic_property_get_from_saved_receiver("__rt_stdclass_get", emitter); - return; - } - - let Some(class_info) = ctx.classes.get(class_name).cloned() else { - emit_dynamic_property_miss(emitter); - return; - }; - let done_label = ctx.next_label("dyn_prop_done"); - let miss_label = ctx.next_label("dyn_prop_miss"); - let candidates: Vec<(String, PhpType)> = class_info - .properties - .iter() - .map(|(name, ty)| (name.clone(), ty.clone())) - .collect(); - let match_labels: Vec = candidates - .iter() - .map(|(name, _)| ctx.next_label(&format!("dyn_prop_{}", label_fragment(name)))) - .collect(); - - for ((property_name, _), label) in candidates.iter().zip(match_labels.iter()) { - emit_branch_if_dynamic_name_matches(property_name, label, emitter, data); - } - abi::emit_jump(emitter, &miss_label); // no declared property name matched the evaluated dynamic name - - for ((property_name, _), label) in candidates.into_iter().zip(match_labels) { - emitter.label(&label); - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - let loaded_ty = - emit_loaded_object_property_access(class_name, &property_name, emitter, ctx, data); - box_dynamic_property_result(&loaded_ty, emitter); - abi::emit_release_temporary_stack(emitter, 32); - abi::emit_jump(emitter, &done_label); // finish after loading the matching declared property - } - - emitter.label(&miss_label); - emit_dynamic_property_miss(emitter); - emitter.label(&done_label); -} - -/// Emits cleanup for a failed dynamic property lookup and returns boxed null. -/// -/// Releases the temporary stack slot (32 bytes) and emits a boxed null as the -/// result of a dynamic property access that matched no declared property name. -fn emit_dynamic_property_miss(emitter: &mut Emitter) { - abi::emit_release_temporary_stack(emitter, 32); - super::emit_boxed_null(emitter); -} - -/// Emits a runtime dynamic property read from a receiver saved on the temporary stack. -/// -/// Loads the object pointer (offset 16), property name pointer (offset 0), and -/// name length (offset 8) from the temporary stack and calls `runtime_symbol` -/// (`__rt_mixed_property_get` or `__rt_stdclass_get`). Releases 32 bytes of -/// temporary stack after the call. Result lands in int_result_reg. -fn emit_runtime_dynamic_property_get_from_saved_receiver( - runtime_symbol: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x0", 16); - abi::emit_load_temporary_stack_slot(emitter, "x1", 0); - abi::emit_load_temporary_stack_slot(emitter, "x2", 8); - emitter.instruction(&format!("bl {}", runtime_symbol)); // read the runtime-named property through the object helper - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rdi", 16); - abi::emit_load_temporary_stack_slot(emitter, "rsi", 0); - abi::emit_load_temporary_stack_slot(emitter, "rdx", 8); - emitter.instruction(&format!("call {}", runtime_symbol)); // read the runtime-named property through the object helper - } - } - abi::emit_release_temporary_stack(emitter, 32); -} - -/// Emits a runtime string comparison and conditional branch for a declared property name. -/// -/// Compares the evaluated dynamic property name (loaded from temporary stack at -/// offsets 0 and 8) against `property` using `__rt_str_eq`. On match, branches -/// to `target_label`. Uses target-specific calling convention for the comparison -/// helper (x0/x1/x2 on ARM64, rdi/rsi/rdx on x86_64). -fn emit_branch_if_dynamic_name_matches( - property: &str, - target_label: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (label, len) = data.add_string(property.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_temporary_stack_slot(emitter, "x1", 0); - abi::emit_load_temporary_stack_slot(emitter, "x2", 8); - abi::emit_symbol_address(emitter, "x3", &label); - abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_str_eq"); // compare the evaluated property name against a declared property name - emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the declared property load when the names match - } - Arch::X86_64 => { - abi::emit_load_temporary_stack_slot(emitter, "rdi", 0); - abi::emit_load_temporary_stack_slot(emitter, "rsi", 8); - abi::emit_symbol_address(emitter, "rdx", &label); - abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_str_eq"); // compare the evaluated property name against a declared property name - emitter.instruction("test rax, rax"); // check whether the runtime string comparison matched - emitter.instruction(&format!("jne {}", target_label)); // dispatch to the declared property load when the names match - } - } -} - -/// Boxes the loaded property value as `PhpType::Mixed` when the result type is not already `Mixed`. -/// -/// Consults `result_ty.codegen_repr()` to determine whether boxing is needed; -/// when it is, calls `emit_box_current_value_as_mixed`. Used after loading a -/// declared property through a dynamic name to ensure the result type is consistent -/// with the broader dynamic property access path. -fn box_dynamic_property_result(result_ty: &PhpType, emitter: &mut Emitter) { - if !matches!(result_ty.codegen_repr(), PhpType::Mixed) { - crate::codegen::emit_box_current_value_as_mixed(emitter, result_ty); - } -} - -/// Collects all classes that declare `property` as a declared property, sorted by class id. -/// -/// Scans `ctx.classes` for every class that has `property` in its `properties` map, -/// returning a vector of `(class_name, property_type, class_id)` sorted by class id. -/// Used by `emit_mixed_property_access` to build a runtime dispatch table for -/// declared property access on `Mixed` receivers. -fn declared_property_candidates( - property: &str, - ctx: &Context, -) -> Vec<(String, PhpType, u64)> { - let mut candidates: Vec<(String, PhpType, u64)> = ctx - .classes - .iter() - .filter_map(|(class_name, class_info)| { - class_info - .properties - .iter() - .find(|(name, _)| name == property) - .map(|(_, ty)| (class_name.clone(), ty.clone(), class_info.class_id)) - }) - .collect(); - candidates.sort_by_key(|(_, _, class_id)| *class_id); - candidates -} - -/// Emits a branch to `null_label` when the mixed payload is not an object. -/// -/// Unboxes the mixed value in `int_result_reg` (x0/rax) and checks whether the -/// runtime tag equals 6 (object). On non-object, branches to `null_label`; on -/// object, promotes the unboxed pointer to the result register (x0 = x1 on ARM64, -/// rax = rdi on x86_64). -fn emit_object_payload_or_null_branch(null_label: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #6"); // runtime tag 6 means the mixed payload is an object - emitter.instruction(&format!("b.ne {}", null_label)); // non-object mixed receivers read as null for property dispatch - emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the normal result register - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 6"); // runtime tag 6 means the mixed payload is an object - emitter.instruction(&format!("jne {}", null_label)); // non-object mixed receivers read as null for property dispatch - emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the normal result register - } - } -} - -/// Emits a runtime class-id dispatch over declared property candidates. -/// -/// Loads the receiver's class id from `int_result_reg` and compares it against -/// each candidate's `class_id` (x9/x10 on ARM64, r11/r10 on x86_64). Jumps to -/// the corresponding `match_label` on equality, falling through when no candidate -/// matches. The caller is responsible for emitting the fallthrough path (typically -/// a miss label or stdclass fallback). -fn emit_branch_to_declared_property_candidates( - candidates: &[(String, PhpType, u64)], - match_labels: &[String], - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [x0]"); // load the receiver class id for declared-property dispatch - for ((_, _, class_id), label) in candidates.iter().zip(match_labels) { - abi::emit_load_int_immediate(emitter, "x10", *class_id as i64); - emitter.instruction("cmp x9, x10"); // compare the receiver class id against a class that declares the property - emitter.instruction(&format!("b.eq {}", label)); // jump to the matching declared-property load - } - } - Arch::X86_64 => { - emitter.instruction("mov r11, QWORD PTR [rax]"); // load the receiver class id for declared-property dispatch - for ((_, _, class_id), label) in candidates.iter().zip(match_labels) { - abi::emit_load_int_immediate(emitter, "r10", *class_id as i64); - emitter.instruction("cmp r11, r10"); // compare the receiver class id against a class that declares the property - emitter.instruction(&format!("je {}", label)); // jump to the matching declared-property load - } - } - } -} - -/// Emits a branch to `label` when the receiver's class id matches stdClass's sentinel. -/// -/// Reloads the class id from the object in `int_result_reg` and compares it -/// against the compile-time `_stdclass_class_id` sentinel via `emit_symbol_address`. -/// On match, branches to `label` to route the read through `__rt_stdclass_get`. -/// Used by `emit_mixed_property_access` to distinguish stdClass dynamic storage -/// from other object types before falling through to the null result path. -fn emit_branch_to_stdclass_fallback(label: &str, emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x10, [x0]"); // reload the receiver class id before the stdClass fallback check - abi::emit_symbol_address(emitter, "x11", "_stdclass_class_id"); - emitter.instruction("ldr x11, [x11]"); // load the compile-time stdClass class id sentinel - emitter.instruction("cmp x10, x11"); // check whether the object uses stdClass dynamic storage - emitter.instruction(&format!("b.eq {}", label)); // route stdClass property reads through the hash-backed helper - } - Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rax]"); // reload the receiver class id before the stdClass fallback check - abi::emit_load_symbol_to_reg(emitter, "r11", "_stdclass_class_id", 0); // load the compile-time stdClass class id sentinel - emitter.instruction("cmp r10, r11"); // check whether the object uses stdClass dynamic storage - emitter.instruction(&format!("je {}", label)); // route stdClass property reads through the hash-backed helper - } - } -} - -/// Emits a static stdClass property read from an already-unboxed object. -/// -/// The object pointer is expected in `int_result_reg` (x0/rax). Emits the property -/// name as a runtime string and calls `__rt_stdclass_get`. On ARM64 passes arguments -/// in x0/x1/x2; on x86_64 passes in rdi/rsi/rdx. Result is `PhpType::Mixed`. -fn emit_static_stdclass_get_from_loaded_object( - property: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (label, len) = data.add_string(property.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", &label); - abi::emit_load_int_immediate(emitter, "x2", len as i64); - emitter.instruction("bl __rt_stdclass_get"); // read the static property name from stdClass dynamic storage - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // pass the unboxed stdClass object pointer as the first helper argument - abi::emit_symbol_address(emitter, "rsi", &label); - abi::emit_load_int_immediate(emitter, "rdx", len as i64); - emitter.instruction("call __rt_stdclass_get"); // read the static property name from stdClass dynamic storage - } - } -} - -/// Converts a property or class name into a label-safe fragment. -/// -/// Replaces every non-alphanumeric character with `_` so the result can be used -/// in asm label names without冲突. Used to construct readable dispatch label -/// names like `mixed_prop_stdclass` or `dyn_prop_myProp`. -fn label_fragment(name: &str) -> String { - name.chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} - -/// Lowers `?Class->property` with a nullable receiver that may be null at runtime. -pub(super) fn emit_nullable_object_property_access( - class_name: &str, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let null_label = ctx.next_label("nullable_prop_null"); - let done_label = ctx.next_label("nullable_prop_done"); - let message = format!("Warning: Attempt to read property \"{}\" on null\n", property); - - super::emit_unbox_mixed_object_or_null_branch(&null_label, emitter); - let property_ty = emit_loaded_object_property_access(class_name, property, emitter, ctx, data); - super::box_nullable_result(&property_ty, emitter); - abi::emit_jump(emitter, &done_label); // skip the nullable property null path after a real property read - - emitter.label(&null_label); - super::emit_runtime_warning(message.as_bytes(), emitter, data); - super::emit_boxed_null(emitter); - - emitter.label(&done_label); - PhpType::Mixed -} - -/// Lowers `$obj->property` where the class is loaded and property is declared. -pub(super) fn emit_loaded_object_property_access( - class_name: &str, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if crate::types::checker::builtin_stdclass::is_stdclass(class_name) { - return emit_stdclass_property_access(property, emitter, data); - } - let class_info = match ctx.classes.get(class_name).cloned() { - Some(c) => c, - None => { - emitter.comment(&format!("WARNING: undefined class {}", class_name)); - return PhpType::Int; - } - }; - - let prop_ty = match class_info - .properties - .iter() - .find(|(n, _)| n == property) - .map(|(_, t)| t.clone()) - { - Some(v) => v, - None => { - if class_info.methods.contains_key("__get") { - emitter.comment(&format!("magic __get('{}')", property)); - let object_reg = abi::symbol_scratch_reg(emitter); - emitter.instruction(&format!("mov {}, {}", object_reg, abi::int_result_reg(emitter))); // preserve $this while the magic-property name setup clobbers normal result registers - super::push_magic_property_name_arg(property, emitter, data); - abi::emit_push_reg(emitter, object_reg); // push $this pointer for __get dispatch using the preserved object register - return super::emit_method_call_with_pushed_args( - class_name, - "__get", - &[PhpType::Str], - emitter, - ctx, - ); - } - if class_info.allow_dynamic_properties { - let dyn_slot_offset = 8 + class_info.properties.len() * 16; - return crate::codegen::stmt::emit_dynamic_property_get( - property, - dyn_slot_offset, - emitter, - ctx, - data, - ); - } - emitter.comment(&format!("WARNING: undefined property {}", property)); - return PhpType::Int; - } - }; - let offset = match class_info.property_offsets.get(property) { - Some(offset) => *offset, - None => { - emitter.comment(&format!("WARNING: missing property offset {}", property)); - return PhpType::Int; - } - }; - - emit_loaded_object_property_value( - class_name, - property, - prop_ty, - offset, - class_info.declared_properties.contains(property), - false, - class_info.reference_properties.contains(property), - ctx, - data, - emitter, - ) -} - -/// Lowers a declared property load for a known class and property offset. -/// -/// Called by `emit_loaded_object_property_access` once the class info, property -/// type, and memory offset have all been resolved. Emits a comment describing -/// the access path, optionally guards uninitialized typed properties, handles -/// reference vs value semantics, and loads the property payload into the -/// appropriate result register(s) based on `prop_ty`. Returns the loaded `PhpType`. -fn emit_loaded_object_property_value( - class_name: &str, - property: &str, - prop_ty: PhpType, - offset: usize, - is_declared: bool, - needs_deref: bool, - is_reference: bool, - ctx: &mut Context, - data: &mut DataSection, - emitter: &mut Emitter, -) -> PhpType { - if needs_deref { - abi::emit_call_label(emitter, "__rt_ptr_check_nonnull"); // abort with fatal error on null pointer dereference - emitter.comment(&format!( - "->{} via ptr<{}> (offset {})", - property, class_name, offset - )); - } else { - emitter.comment(&format!("->{} (offset {})", property, offset)); - } - - let object_reg = abi::int_result_reg(emitter); - - if is_declared { - emit_uninitialized_typed_property_guard( - class_name, property, offset, object_reg, emitter, ctx, data, - ); - } - - if is_reference { - let pointer_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_from_address(emitter, pointer_reg, object_reg, offset); - match &prop_ty { - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_from_address(emitter, ptr_reg, pointer_reg, 0); - abi::emit_load_from_address(emitter, len_reg, pointer_reg, 8); - } - PhpType::Float => { - abi::emit_load_from_address(emitter, abi::float_result_reg(emitter), pointer_reg, 0); - } - PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never | PhpType::Resource(_) => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - } - PhpType::TaggedScalar => { - unreachable!("nullable scalar properties use the boxed Mixed representation") - } - PhpType::Iterable - | PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - } - } - return prop_ty; - } - - match &prop_ty { - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - let base_reg = abi::symbol_scratch_reg(emitter); - emitter.instruction(&format!("mov {}, {}", base_reg, object_reg)); // preserve the object base pointer while loading the two-word string property payload - abi::emit_load_from_address(emitter, ptr_reg, base_reg, offset); - abi::emit_load_from_address(emitter, len_reg, base_reg, offset + 8); - } - PhpType::Float => { - abi::emit_load_from_address(emitter, abi::float_result_reg(emitter), object_reg, offset); - } - PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never | PhpType::Resource(_) => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - } - PhpType::TaggedScalar => { - unreachable!("nullable scalar properties use the boxed Mixed representation") - } - PhpType::Iterable - | PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - } - } - - prop_ty -} - -/// Emits a guard that aborts when a typed property has not been initialized. -/// -/// Loads the marker word at `offset + 8` from `object_reg` and compares it against -/// `UNINITIALIZED_TYPED_PROPERTY_SENTINEL`. When the sentinel is detected, falls -/// through to `emit_uninitialized_typed_property_fatal`; otherwise jumps to -/// `initialized_label` to continue the property read. Used for typed properties -/// that must not be accessed before initialization, matching PHP's own runtime -/// behavior. -fn emit_uninitialized_typed_property_guard( - class_name: &str, - property: &str, - offset: usize, - object_reg: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let initialized_label = ctx.next_label("typed_prop_initialized"); - let marker_reg = abi::secondary_scratch_reg(emitter); - let sentinel_reg = abi::tertiary_scratch_reg(emitter); - abi::emit_load_from_address(emitter, marker_reg, object_reg, offset + 8); - abi::emit_load_int_immediate(emitter, sentinel_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // check whether the typed property still carries the uninitialized marker - emitter.instruction(&format!("b.ne {}", initialized_label)); // continue the property read once the slot has been initialized - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // check whether the typed property still carries the uninitialized marker - emitter.instruction(&format!("jne {}", initialized_label)); // continue the property read once the slot has been initialized - } - } - emit_uninitialized_typed_property_fatal(class_name, property, emitter, data); - emitter.label(&initialized_label); -} - -/// Emits a fatal runtime error and terminates the program. -/// -/// Formats the message "Fatal error: Typed property {class_name}::{property} must -/// not be accessed before initialization" and emits it to stderr via the `write` -/// syscall, then calls `exit(1)`. Emits platform-specific syscalls directly (ARM64 -/// uses x0=fd, x1=buf, x2=len with syscall 4/1; x86_64 uses rdi=fd, rsi=buf, -/// rdx=len with syscall 1/60). Called by `emit_uninitialized_typed_property_guard` -/// when the sentinel marker is detected. -fn emit_uninitialized_typed_property_fatal( - class_name: &str, - property: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let message = format!( - "Fatal error: Typed property {}::${} must not be accessed before initialization\n", - class_name, property - ); - let (label, len) = data.add_string(message.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // fd = stderr for the typed-property initialization fatal - abi::emit_symbol_address(emitter, "x1", &label); // point write() at the typed-property initialization diagnostic - emitter.instruction(&format!("mov x2, #{}", len)); // pass the diagnostic byte length to write() - emitter.syscall(4); - emitter.instruction("mov x0, #1"); // exit status 1 indicates abnormal termination - emitter.syscall(1); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", &label); // point write() at the typed-property initialization diagnostic - emitter.instruction(&format!("mov edx, {}", len)); // pass the diagnostic byte length to write() - emitter.instruction("mov edi, 2"); // fd = stderr for the typed-property initialization fatal - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal diagnostic before terminating - emitter.instruction("mov edi, 1"); // exit status 1 indicates abnormal termination - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate after the typed-property initialization fatal - } - } -} diff --git a/src/codegen/expr/objects/allocation.rs b/src/codegen/expr/objects/allocation.rs deleted file mode 100644 index 027c1081c7..0000000000 --- a/src/codegen/expr/objects/allocation.rs +++ /dev/null @@ -1,1526 +0,0 @@ -//! Purpose: -//! Lowers object allocation and constructor-ready initialization. -//! Produces object-related expression results while respecting runtime metadata and ownership rules. -//! -//! Called from: -//! - `crate::codegen::expr::objects` -//! -//! Key details: -//! - Object handles, property storage, and class ids must stay consistent with emitted class tables. - -use crate::codegen::builtins::arrays::{callback_env, runtime_callable_array_callback}; -use crate::codegen::callable_dispatch::RuntimeCallableCase; -use crate::codegen::{abi, runtime_value_tag}; -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::calls::args as call_args; -use crate::codegen::platform::Arch; -use crate::codegen::{NULL_SENTINEL, UNINITIALIZED_TYPED_PROPERTY_SENTINEL}; -use crate::names::method_symbol; -use crate::parser::ast::{CallableTarget, Expr, ExprKind}; -use crate::types::{FunctionSig, PhpType}; - -use super::super::{ - coerce_result_to_type, emit_expr, expr_result_heap_ownership, - restore_concat_offset_after_nested_call, - save_concat_offset_before_nested_call, -}; -use super::dispatch::emit_dispatch_interface_method; - -const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; -const ITERATOR_ITERATOR_DOWNCAST_MESSAGE: &str = - "Class to downcast to not found or not base class or does not implement Traversable"; - -/// Emits assembly for new object. -pub(super) fn emit_new_object( - class_name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - if class_name == "Fiber" { - return emit_new_fiber(args, emitter, ctx, data); - } - if is_spl_doubly_linked_list_family(class_name) { - return emit_new_spl_doubly_linked_list(class_name, args, emitter, ctx); - } - if class_name == "SplFixedArray" { - return emit_new_spl_fixed_array(args, emitter, ctx, data); - } - if matches!(class_name, "ArrayIterator" | "ArrayObject") { - return emit_new_spl_array_storage_object(class_name, args, emitter, ctx, data); - } - if class_name == "IteratorIterator" { - return emit_new_iterator_iterator(args, emitter, ctx, data); - } - if matches!(class_name, "CallbackFilterIterator" | "RecursiveCallbackFilterIterator") { - return emit_new_callback_filter_iterator(class_name, args, emitter, ctx, data); - } - if super::reflection::is_reflection_owner_class(class_name) { - return super::reflection::emit_new_reflection_owner( - class_name, args, emitter, ctx, data, - ); - } - emit_new_object_core(class_name, args, true, emitter, ctx, data) -} - -/// Emits assembly for new object core. -pub(super) fn emit_new_object_core( - class_name: &str, - args: &[Expr], - run_constructor: bool, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let class_info = match ctx.classes.get(class_name).cloned() { - Some(c) => c, - None => { - emitter.comment(&format!("WARNING: undefined class {}", class_name)); - return PhpType::Int; - } - }; - if crate::types::checker::builtin_stdclass::is_stdclass(class_name) { - emitter.comment("new stdClass()"); - // stdClass instances do not have static property slots; the - // dedicated runtime helper allocates the 16-byte payload, stamps - // the class_id, and seeds the dynamic-property hash. User-supplied - // arguments (none allowed by PHP for stdClass) are ignored here. - let _ = args; - abi::emit_call_label(emitter, "__rt_stdclass_new"); // allocate a fresh stdClass instance with an empty property hash - return PhpType::Object(class_name.to_string()); - } - let num_props = class_info.properties.len(); - // PHP 8.2 #[\AllowDynamicProperties] adds a single 8-byte slot after the - // declared properties to hold a lazily-allocated hashtable pointer for - // undeclared property storage. The slot is initialised to 0 (null) and - // only allocated on the first dynamic property write. - let dyn_props_slot = if class_info.allow_dynamic_properties { - 8 - } else { - 0 - }; - let obj_size = 8 + num_props * 16 + dyn_props_slot; // 8 for class_id + 16 per property + optional dyn_props ptr - - emitter.comment(&format!("new {}()", class_name)); - - // -- allocate object on heap -- - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, #{}", obj_size)); // object size in bytes - emitter.instruction("bl __rt_heap_alloc"); // allocate object -> x0 = pointer - emitter.instruction("mov x9, #4"); // heap kind 4 = object instance - emitter.instruction("str x9, [x0, #-8]"); // store object kind in the uniform heap header - emitter.instruction(&format!("mov x10, #{}", class_info.class_id)); // load compile-time class id - emitter.instruction("str x10, [x0]"); // store class id at object header - abi::emit_push_reg(emitter, "x0"); // save the allocated object pointer while property slots are initialized - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", obj_size)); // object size in bytes - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate object -> rax = pointer - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word with the uniform heap marker - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as an object instance in the x86_64 uniform heap header - emitter.instruction(&format!("mov r10, {}", class_info.class_id)); // load the compile-time class id for the allocated object instance - emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id in the first field of the object payload - abi::emit_push_reg(emitter, "rax"); // save the allocated object pointer while property slots are initialized - } - } - - // -- zero-initialize all property slots -- - for i in 0..num_props { - let offset = 8 + i * 16; - let property_name = &class_info.properties[i].0; - let starts_uninitialized = class_info.declared_properties.contains(property_name) - && class_info.defaults.get(i).is_some_and(|default| default.is_none()); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // peek object pointer - emitter.instruction(&format!("str xzr, [x9, #{}]", offset)); // zero-init property lo - emitter.instruction(&format!("str xzr, [x9, #{}]", offset + 8)); // zero-init property hi - } - Arch::X86_64 => { - emitter.instruction("mov r11, QWORD PTR [rsp]"); // peek the allocated object pointer from the temporary stack slot - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", offset)); // zero-initialize the low word of the property storage slot - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", offset + 8)); // zero-initialize the high word / runtime metadata slot - } - } - if starts_uninitialized { - let marker_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, marker_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // peek object pointer before marking this typed property uninitialized - } - Arch::X86_64 => { - emitter.instruction("mov r11, QWORD PTR [rsp]"); // peek object pointer before marking this typed property uninitialized - } - } - let object_reg = match emitter.target.arch { - Arch::AArch64 => "x9", - Arch::X86_64 => "r11", - }; - abi::emit_store_to_address(emitter, marker_reg, object_reg, offset + 8); - } - } - - // -- allocate the dyn_props hashtable if the class declares - // #[\AllowDynamicProperties], and store the pointer in the slot -- - if dyn_props_slot != 0 { - let offset = 8 + num_props * 16; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #4"); // initial hashtable capacity for dyn_props - emitter.instruction("mov x1, #7"); // value type tag = mixed (heterogeneous) - emitter.instruction("bl __rt_hash_new"); // allocate empty hashtable -> x0 = hashtable pointer - emitter.instruction("ldr x9, [sp]"); // peek object pointer for dyn_props slot store - emitter.instruction(&format!("str x0, [x9, #{}]", offset)); // store the hashtable pointer in the dyn_props slot - } - Arch::X86_64 => { - emitter.instruction("mov rdi, 4"); // initial hashtable capacity for dyn_props - emitter.instruction("mov rsi, 7"); // value type tag = mixed - emitter.instruction("call __rt_hash_new"); // allocate empty hashtable -> rax = hashtable pointer - emitter.instruction("mov r11, QWORD PTR [rsp]"); // peek object pointer for dyn_props slot store - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", offset)); // store the hashtable pointer in the dyn_props slot - } - } - } - - // -- set default property values -- - for i in 0..num_props { - if let Some(default_expr) = &class_info.defaults[i] { - let default_expr = default_expr.clone(); - let offset = 8 + i * 16; - let prop_name = &class_info.properties[i].0; - let expected_ty = class_info.properties[i].1.clone(); - // An array-literal default whose refined property type is associative - // must be stored as hash storage (tag 5). `emit_expr` lowers `[]` (and - // positional literals) to an indexed-list array; later string-keyed - // writes then desync from that storage, so reads after the array is - // copied out or returned from a method miss the keys (they decode to the - // null sentinel). This mirrors the property-assignment path, where the - // same rewrite already runs. - let prop_ty = if let Some(assoc_ty) = - crate::codegen::expr::arrays::emit_array_literal_as_assoc_target( - &default_expr, - &expected_ty, - emitter, - ctx, - data, - ) { - assoc_ty - } else { - let actual_ty = emit_expr(&default_expr, emitter, ctx, data); - if class_info.declared_properties.contains(prop_name) { - coerce_result_to_type(emitter, ctx, data, &actual_ty, &expected_ty); - expected_ty - } else { - actual_ty - } - }; - let object_reg = abi::symbol_scratch_reg(emitter); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr {}, [sp]", object_reg)); // peek object pointer from the temporary stack slot on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, QWORD PTR [rsp]", object_reg)); // peek object pointer from the temporary stack slot on x86_64 - } - } - match &prop_ty { - PhpType::Int - | PhpType::Bool - | PhpType::Callable - | PhpType::Pointer(_) - | PhpType::Buffer(_) - | PhpType::Packed(_) => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - abi::emit_store_zero_to_address(emitter, object_reg, offset + 8); - } - PhpType::TaggedScalar => { - unreachable!("nullable scalar properties use the boxed Mixed representation") - } - PhpType::Resource(_) => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - let tag_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, tag_reg, 9); - abi::emit_store_to_address(emitter, tag_reg, object_reg, offset + 8); - } - PhpType::Mixed | PhpType::Iterable => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - let tag_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, tag_reg, 7); - abi::emit_store_to_address(emitter, tag_reg, object_reg, offset + 8); - } - PhpType::Union(_) => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - let tag_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, tag_reg, 7); - abi::emit_store_to_address(emitter, tag_reg, object_reg, offset + 8); - } - PhpType::Array(_) => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - let tag_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, tag_reg, 4); - abi::emit_store_to_address(emitter, tag_reg, object_reg, offset + 8); - } - PhpType::AssocArray { .. } => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - let tag_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, tag_reg, 5); - abi::emit_store_to_address(emitter, tag_reg, object_reg, offset + 8); - } - PhpType::Object(_) => { - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, offset); - let tag_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, tag_reg, 6); - abi::emit_store_to_address(emitter, tag_reg, object_reg, offset + 8); - } - PhpType::Float => { - abi::emit_store_to_address(emitter, abi::float_result_reg(emitter), object_reg, offset); - abi::emit_store_zero_to_address(emitter, object_reg, offset + 8); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_store_to_address(emitter, ptr_reg, object_reg, offset); - abi::emit_store_to_address(emitter, len_reg, object_reg, offset + 8); - } - PhpType::Void => { - let null_reg = abi::temp_int_reg(emitter.target); - abi::emit_load_int_immediate(emitter, null_reg, NULL_SENTINEL); - abi::emit_store_to_address(emitter, null_reg, object_reg, offset); - abi::emit_store_zero_to_address(emitter, object_reg, offset + 8); - } - PhpType::Never => {} - } - } - } - - // -- call __construct if it exists -- - if run_constructor && class_info.methods.contains_key("__construct") { - let sig = class_info.methods.get("__construct").cloned(); - let regular_param_count = call_args::regular_param_count(sig.as_ref(), args.len()); - let emitted_args = call_args::emit_pushed_call_args( - args, - sig.as_ref(), - regular_param_count, - "constructor ref arg", - false, - true, - emitter, - ctx, - data, - ); - let arg_types = emitted_args.arg_types; - - let assignments = crate::codegen::abi::build_outgoing_arg_assignments_for_target( - emitter.target, - &arg_types, - 1, - ); - let overflow_bytes = - crate::codegen::abi::materialize_outgoing_args(emitter, &assignments); - - let receiver_offset = overflow_bytes + emitted_args.source_temp_bytes; - if receiver_offset == 0 { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp]"); // load $this directly from the top of the stack when all args stayed in registers on AArch64 - } - Arch::X86_64 => { - emitter.instruction("mov rdi, QWORD PTR [rsp]"); // load $this directly into the first SysV integer argument register when all args stayed in registers on x86_64 - } - } - } else { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x0, [sp, #{}]", receiver_offset)); // skip argument temporaries to reload the saved object pointer as $this on AArch64 - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, QWORD PTR [rsp + {}]", receiver_offset)); // skip argument temporaries to reload the saved object pointer as $this in the first SysV integer argument register on x86_64 - } - } - } - save_concat_offset_before_nested_call(emitter, ctx); - let constructor_impl = class_info - .method_impl_classes - .get("__construct") - .map(String::as_str) - .unwrap_or(class_name); - abi::emit_call_label(emitter, &method_symbol(constructor_impl, "__construct")); // call the resolved constructor implementation for the active target ABI - restore_concat_offset_after_nested_call(emitter, ctx, &PhpType::Void); - abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop spilled constructor arguments after the nested call returns - abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); // drop source-order named-argument temporaries after constructor dispatch - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the allocated object pointer as the expression result for the active target ABI - PhpType::Object(class_name.to_string()) -} - -/// Returns true when SPL doubly linked list family. -fn is_spl_doubly_linked_list_family(class_name: &str) -> bool { - matches!(class_name, "SplDoublyLinkedList" | "SplStack" | "SplQueue") -} - -/// Emits assembly for new SPL doubly linked list. -fn emit_new_spl_doubly_linked_list( - class_name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &Context, -) -> PhpType { - if !args.is_empty() { - emitter.comment(&format!( - "WARNING: {} constructor arguments ignored by runtime-managed SPL list", - class_name - )); - } - let class_id = ctx - .classes - .get(class_name) - .map(|info| info.class_id) - .unwrap_or(0); - emitter.comment(&format!("new {}() — SPL runtime construction", class_name)); - abi::emit_load_int_immediate( - emitter, - abi::int_arg_reg_name(emitter.target, 0), - class_id as i64, - ); // pass the concrete SPL class id to the runtime allocator - abi::emit_call_label(emitter, "__rt_spl_dll_new"); // allocate a runtime-managed SPL doubly-linked-list payload - PhpType::Object(class_name.to_string()) -} - -/// Emits assembly for new SPL fixed array. -fn emit_new_spl_fixed_array( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let class_id = ctx - .classes - .get("SplFixedArray") - .map(|info| info.class_id) - .unwrap_or(0); - emitter.comment("new SplFixedArray() — SPL runtime construction"); - if let Some(size_expr) = args.first() { - let actual_ty = emit_expr(size_expr, emitter, ctx, data); - coerce_result_to_type(emitter, ctx, data, &actual_ty, &PhpType::Int); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve constructor size while loading class id - abi::emit_load_int_immediate( - emitter, - abi::int_arg_reg_name(emitter.target, 0), - class_id as i64, - ); // pass the concrete SplFixedArray class id to the runtime allocator - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 1)); // pass requested fixed-array size as the second runtime argument - abi::emit_call_label(emitter, "__rt_spl_fixed_new"); // allocate a runtime-managed SplFixedArray payload - PhpType::Object("SplFixedArray".to_string()) -} - -/// Emits assembly for new SPL array storage object. -fn emit_new_spl_array_storage_object( - class_name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("new {}() — SPL array storage construction", class_name)); - let Some(class_info) = ctx.classes.get(class_name).cloned() else { - emitter.comment(&format!("WARNING: missing {} metadata", class_name)); - return PhpType::Object(class_name.to_string()); - }; - let keys_offset = *class_info.property_offsets.get("keys").unwrap_or(&8); - let values_offset = *class_info.property_offsets.get("values").unwrap_or(&24); - let flags_offset = class_info - .property_offsets - .get("flags") - .copied() - .unwrap_or(if class_name == "ArrayIterator" { 56 } else { 40 }); - let position_offset = class_info.property_offsets.get("position").copied(); - - emit_new_object_core(class_name, &[], false, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the allocated SPL storage object while constructor arguments are normalized - - let source_ty = if let Some(source_expr) = args.first() { - let ty = emit_expr(source_expr, emitter, ctx, data); - if matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { - ty - } else { - emitter.comment("WARNING: ArrayIterator/ArrayObject source was not statically typed as array"); - emit_empty_mixed_array(emitter); - PhpType::Array(Box::new(PhpType::Mixed)) - } - } else { - emit_empty_mixed_array(emitter); - PhpType::Array(Box::new(PhpType::Mixed)) - }; - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the source array for both keys and values extraction - - if let Some(flags_expr) = args.get(1) { - let flags_ty = emit_expr(flags_expr, emitter, ctx, data); - coerce_result_to_type(emitter, ctx, data, &flags_ty, &PhpType::Int); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve ArrayIterator/ArrayObject flags until property storage is ready - - load_storage_source_from_stack(emitter); - let keys_ty = crate::codegen::builtins::arrays::array_keys::emit_loaded_keys( - &source_ty, - emitter, - ctx, - ) - .unwrap_or_else(|| PhpType::Array(Box::new(PhpType::Mixed))); - emit_convert_loaded_indexed_array_to_mixed(&keys_ty, emitter); - store_storage_array_property_from_result(emitter, keys_offset, 32); - - load_storage_source_from_stack(emitter); - let values_ty = crate::codegen::builtins::arrays::array_values::emit_loaded_values( - &source_ty, - emitter, - ctx, - data, - ) - .unwrap_or_else(|| PhpType::Array(Box::new(PhpType::Mixed))); - emit_convert_loaded_indexed_array_to_mixed(&values_ty, emitter); - store_storage_array_property_from_result(emitter, values_offset, 32); - - store_storage_int_property_from_stack(emitter, flags_offset, 0, 32); - if let Some(position_offset) = position_offset { - store_storage_zero_property(emitter, position_offset, 32); - } - - abi::emit_release_temporary_stack(emitter, 32); // discard preserved flags and source array after storage initialization - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the initialized SPL storage object as the expression result - PhpType::Object(class_name.to_string()) -} - -/// Emits assembly for new iterator iterator. -fn emit_new_iterator_iterator( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("new IteratorIterator() — Traversable normalization"); - let Some(class_info) = ctx.classes.get("IteratorIterator").cloned() else { - emitter.comment("WARNING: missing IteratorIterator metadata"); - return PhpType::Object("IteratorIterator".to_string()); - }; - let inner_offset = class_info.property_offsets.get("inner").copied().unwrap_or(8); - let normalized_args = - normalize_iterator_iterator_constructor_args(&class_info, args, emitter, ctx, data); - - emit_new_object_core("IteratorIterator", &[], false, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the allocated IteratorIterator while normalizing the constructor source - - if let Some(iterator_expr) = normalized_args.first() { - let source_ty = emit_expr(iterator_expr, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the Traversable candidate while evaluating the optional downcast class - emit_iterator_iterator_downcast_arg_status(normalized_args.get(1), emitter, ctx, data); - emit_normalize_saved_traversable_to_iterator(iterator_expr, &source_ty, emitter, ctx); - } else { - emitter.comment("WARNING: IteratorIterator constructor missing Traversable source"); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - - store_iterator_inner_property_from_result(emitter, inner_offset); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the initialized IteratorIterator as the expression result - PhpType::Object("IteratorIterator".to_string()) -} - -/// Emits assembly for new callback filter iterator. -fn emit_new_callback_filter_iterator( - class_name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("new {}() — callback filter construction", class_name)); - let Some(class_info) = ctx.classes.get(class_name).cloned() else { - emitter.comment(&format!("WARNING: missing {} metadata", class_name)); - return PhpType::Object(class_name.to_string()); - }; - let inner_offset = class_info.property_offsets.get("inner").copied().unwrap_or(8); - let callback_offset = class_info - .property_offsets - .get("callback") - .copied() - .unwrap_or(24); - let callback_env_offset = class_info - .property_offsets - .get("callbackEnv") - .copied() - .unwrap_or(40); - let normalized_args = normalize_constructor_args(&class_info, args, emitter, ctx, data); - - emit_new_object_core(class_name, &[], false, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the allocated callback-filter object while constructor arguments are stored - - if let Some(iterator_expr) = normalized_args.first() { - let actual_ty = emit_expr(iterator_expr, emitter, ctx, data); - coerce_result_to_type( - emitter, - ctx, - data, - &actual_ty, - &PhpType::Object("Iterator".to_string()), - ); - } else { - emitter.comment(&format!("WARNING: {} constructor missing Iterator source", class_name)); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - store_iterator_inner_property_from_result(emitter, inner_offset); - - if let Some(callback_expr) = normalized_args.get(1) { - let handled_callable_array = emit_runtime_callable_array_callback_filter( - callback_expr, - callback_offset, - callback_env_offset, - emitter, - ctx, - data, - ) || emit_runtime_callable_array_literal_callback_filter( - callback_expr, - callback_offset, - callback_env_offset, - emitter, - ctx, - data, - ) || emit_static_callable_array_callback_filter( - callback_expr, - callback_offset, - callback_env_offset, - emitter, - ctx, - data, - ); - if !handled_callable_array { - let (_callback_ty, captures, target_visible_arg_types) = - emit_callback_filter_callable_arg(callback_expr, emitter, ctx, data); - if callback_env::expr_call_needs_descriptor_callback_env(callback_expr, ctx) { - let wrapper_label = - callback_env::emit_persistent_descriptor_callback_env_from_result( - callback_expr, - callback_filter_visible_arg_types(), - PhpType::Bool, - emitter, - ctx, - ) - .expect("type checker must reject unsupported callback-filter descriptor env ownership"); - store_pointer_property_from_result(emitter, callback_env_offset); - emit_store_callback_filter_adapter_descriptor( - &wrapper_label, - callback_offset, - &[], - emitter, - data, - ); - } else if captures.is_empty() { - store_callable_property_from_result(emitter, callback_offset); - store_pointer_property_zero(emitter, callback_env_offset); - } else { - let wrapper_label = callback_env::emit_persistent_callback_env_from_result( - &captures, - callback_filter_visible_arg_types(), - target_visible_arg_types, - emitter, - ctx, - ); - store_pointer_property_from_result(emitter, callback_env_offset); - emit_store_callback_filter_adapter_descriptor( - &wrapper_label, - callback_offset, - &captures, - emitter, - data, - ); - } - } - } else { - emitter.comment(&format!("WARNING: {} constructor missing callback", class_name)); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - store_pointer_property_zero(emitter, callback_env_offset); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - store_callable_property_from_result(emitter, callback_offset); - } - - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the initialized callback-filter object as the expression result - PhpType::Object(class_name.to_string()) -} - -/// Emits persistent callback state for a runtime-selected callable-array callback. -fn emit_runtime_callable_array_callback_filter( - callback_expr: &Expr, - callback_offset: usize, - callback_env_offset: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - runtime_callable_array_callback::emit_without_saved_array( - callback_expr, - emitter, - ctx, - data, - |case, receiver_ty, emitter, ctx, data| { - emit_runtime_callable_array_callback_filter_case( - case, - receiver_ty, - callback_offset, - callback_env_offset, - 0, - emitter, - ctx, - data, - ); - }, - ) -} - -/// Emits persistent callback state for a runtime-selected callable-array literal callback. -fn emit_runtime_callable_array_literal_callback_filter( - callback_expr: &Expr, - callback_offset: usize, - callback_env_offset: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - runtime_callable_array_callback::emit_literal_without_saved_array( - callback_expr, - emitter, - ctx, - data, - |case, receiver_ty, emitter, ctx, data| { - emit_runtime_callable_array_callback_filter_case( - case, - receiver_ty, - callback_offset, - callback_env_offset, - 16, - emitter, - ctx, - data, - ); - }, - ) -} - -/// Stores one selected runtime callable-array descriptor on the callback-filter object. -fn emit_runtime_callable_array_callback_filter_case( - case: &RuntimeCallableCase, - receiver_ty: Option<&PhpType>, - callback_offset: usize, - callback_env_offset: usize, - object_stack_offset: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let descriptor_prefix_types = receiver_ty.iter().map(|ty| (*ty).clone()).collect(); - let wrapper_label = callback_env::emit_persistent_descriptor_callback_env_from_static_descriptor( - &case.descriptor_label, - callback_filter_visible_arg_types(), - descriptor_prefix_types, - PhpType::Bool, - emitter, - ctx, - ); - store_pointer_property_from_result_at_stack_offset( - emitter, - callback_env_offset, - object_stack_offset, - ); - emit_store_callback_filter_adapter_descriptor_at_stack_offset( - &wrapper_label, - callback_offset, - &[], - emitter, - data, - object_stack_offset, - ); -} - -/// Emits persistent callback state for a statically known callable-array callback. -fn emit_static_callable_array_callback_filter( - callback_expr: &Expr, - callback_offset: usize, - callback_env_offset: usize, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - let Some(array_callback) = - callback_env::resolve_callable_array_descriptor_callback(callback_expr, ctx, data) - else { - return false; - }; - let descriptor_prefix_types: Vec = array_callback - .receiver_prefix - .iter() - .map(|(_, ty)| ty.clone()) - .collect(); - if let Some((receiver, receiver_ty)) = &array_callback.receiver_prefix { - emit_expr(receiver, emitter, ctx, data); - abi::emit_push_result_value(emitter, receiver_ty); - } - let wrapper_label = callback_env::emit_persistent_descriptor_callback_env_from_static_descriptor( - &array_callback.descriptor_label, - callback_filter_visible_arg_types(), - descriptor_prefix_types, - PhpType::Bool, - emitter, - ctx, - ); - store_pointer_property_from_result(emitter, callback_env_offset); - emit_store_callback_filter_adapter_descriptor( - &wrapper_label, - callback_offset, - &[], - emitter, - data, - ); - true -} - -/// Emits and stores the descriptor for a callback-filter adapter wrapper. -fn emit_store_callback_filter_adapter_descriptor( - wrapper_label: &str, - callback_offset: usize, - captures: &[(String, PhpType, bool)], - emitter: &mut Emitter, - data: &mut DataSection, -) { - emit_store_callback_filter_adapter_descriptor_at_stack_offset( - wrapper_label, - callback_offset, - captures, - emitter, - data, - 0, - ); -} - -/// Emits and stores a callback-filter adapter descriptor on an object below temporary slots. -fn emit_store_callback_filter_adapter_descriptor_at_stack_offset( - wrapper_label: &str, - callback_offset: usize, - captures: &[(String, PhpType, bool)], - emitter: &mut Emitter, - data: &mut DataSection, - object_stack_offset: usize, -) { - let callback_sig = callback_filter_callable_sig(); - crate::codegen::callable_descriptor::emit_load_descriptor_address_with_meta( - emitter, - data, - abi::int_result_reg(emitter), - wrapper_label, - None, - crate::codegen::callable_descriptor::CALLABLE_DESC_KIND_CALLBACK_ADAPTER, - Some(&callback_sig), - captures, - &[], - crate::codegen::callable_descriptor::CallableDescriptorInvocation::new( - crate::codegen::callable_descriptor::CallableDescriptorShape::CallbackAdapter, - ), - ); - store_callable_property_from_result_at_stack_offset( - emitter, - callback_offset, - object_stack_offset, - ); -} - -/// Normalizes iterator iterator constructor args into the representation expected by later lowering. -fn normalize_iterator_iterator_constructor_args( - class_info: &crate::types::ClassInfo, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Vec { - normalize_constructor_args(class_info, args, emitter, ctx, data) -} - -/// Normalizes constructor args into the representation expected by later lowering. -fn normalize_constructor_args( - class_info: &crate::types::ClassInfo, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Vec { - let Some(sig) = class_info.methods.get("__construct") else { - return args.to_vec(); - }; - let call_span = args - .first() - .map(|arg| arg.span) - .unwrap_or_else(crate::span::Span::dummy); - let regular_param_count = call_args::regular_param_count(Some(sig), args.len()); - call_args::preevaluate_named_call_args_to_temps( - sig, - args, - call_span, - regular_param_count, - false, - emitter, - ctx, - data, - ) - .args -} - -/// Computes the callable signature metadata for callback filter callable. -fn callback_filter_callable_sig() -> FunctionSig { - FunctionSig { - params: vec![ - ("current".to_string(), PhpType::Mixed), - ("key".to_string(), PhpType::Mixed), - ("iterator".to_string(), PhpType::Object("Iterator".to_string())), - ], - defaults: vec![None, None, None], - return_type: PhpType::Bool, - declared_return: false, - by_ref_return: false, - ref_params: vec![false, false, false], - declared_params: vec![false, false, false], - variadic: None, - deprecation: None, - } -} - -/// Provides the Callback filter visible arg types helper used by the allocation module. -fn callback_filter_visible_arg_types() -> Vec { - vec![ - PhpType::Mixed, - PhpType::Mixed, - PhpType::Object("Iterator".to_string()), - ] -} - -/// Emits assembly for callback filter callable arg. -fn emit_callback_filter_callable_arg( - callback_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> (PhpType, Vec<(String, PhpType, bool)>, Vec) { - let previous_sig = ctx - .expected_first_class_callable_sig - .replace(callback_filter_callable_sig()); - let (callback_ty, capture_source) = if let ExprKind::Variable(name) = &callback_expr.kind { - if let Some(CallableTarget::Function(function_name)) = - ctx.first_class_callable_targets.get(name).cloned() - { - let synthetic = Expr::new( - ExprKind::FirstClassCallable(CallableTarget::Function(function_name)), - callback_expr.span, - ); - (emit_expr(&synthetic, emitter, ctx, data), synthetic) - } else { - (emit_expr(callback_expr, emitter, ctx, data), callback_expr.clone()) - } - } else { - (emit_expr(callback_expr, emitter, ctx, data), callback_expr.clone()) - }; - let captures = crate::codegen::callables::callable_captures(&capture_source, ctx); - let target_visible_arg_types = callback_filter_target_arg_types(&capture_source, ctx); - ctx.expected_first_class_callable_sig = previous_sig; - (callback_ty, captures, target_visible_arg_types) -} - -/// Provides the Callback filter target arg types helper used by the allocation module. -fn callback_filter_target_arg_types(callback_expr: &Expr, ctx: &Context) -> Vec { - let sig = match &callback_expr.kind { - ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) => { - ctx.deferred_closures.last().map(|closure| closure.sig.clone()) - } - _ => crate::codegen::callables::callable_sig(callback_expr, ctx), - }; - sig.map(|sig| { - sig.params - .into_iter() - .take(3) - .map(|(_, ty)| ty) - .collect::>() - }) - .filter(|types| types.len() == 3) - .unwrap_or_else(callback_filter_visible_arg_types) -} - -/// Emits assembly for iterator iterator downcast arg status. -fn emit_iterator_iterator_downcast_arg_status( - class_expr: Option<&Expr>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let Some(class_expr) = class_expr else { - emit_push_iterator_iterator_downcast_status(emitter, 0, 0); - return; - }; - - let class_ty = emit_expr(class_expr, emitter, ctx, data).codegen_repr(); - match class_ty { - PhpType::Str => emit_push_iterator_iterator_downcast_status_from_string(emitter, ctx), - PhpType::Void | PhpType::Never => { - emit_push_iterator_iterator_downcast_status(emitter, 0, 0); - } - PhpType::Mixed | PhpType::Union(_) => { - emit_push_iterator_iterator_downcast_status_from_mixed(emitter, ctx); - } - _ => emit_push_iterator_iterator_downcast_status(emitter, 2, 0), - } -} - -/// Emits assembly for push iterator iterator downcast status from string. -fn emit_push_iterator_iterator_downcast_status_from_string( - emitter: &mut Emitter, - ctx: &mut Context, -) { - abi::emit_call_label(emitter, "__rt_instanceof_lookup"); // resolve the optional downcast class-string argument - emit_push_iterator_iterator_downcast_status_from_lookup(emitter, ctx); -} - -/// Emits assembly for push iterator iterator downcast status from mixed. -fn emit_push_iterator_iterator_downcast_status_from_mixed( - emitter: &mut Emitter, - ctx: &mut Context, -) { - let string_case = ctx.next_label("iterator_iterator_downcast_string"); - let null_case = ctx.next_label("iterator_iterator_downcast_null"); - let invalid_case = ctx.next_label("iterator_iterator_downcast_invalid"); - let done = ctx.next_label("iterator_iterator_downcast_done"); - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect nullable mixed downcast values at runtime - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #1"); // runtime tag 1 means the downcast argument is a string - emitter.instruction(&format!("b.eq {}", string_case)); // resolve string downcast targets through class metadata - emitter.instruction("cmp x0, #8"); // runtime tag 8 means the downcast argument is null - emitter.instruction(&format!("b.eq {}", null_case)); // null behaves like the omitted second constructor argument - emitter.instruction(&format!("b {}", invalid_case)); // non-string, non-null mixed payloads are invalid downcast targets - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 1"); // runtime tag 1 means the downcast argument is a string - emitter.instruction(&format!("je {}", string_case)); // resolve string downcast targets through class metadata - emitter.instruction("cmp rax, 8"); // runtime tag 8 means the downcast argument is null - emitter.instruction(&format!("je {}", null_case)); // null behaves like the omitted second constructor argument - emitter.instruction(&format!("jmp {}", invalid_case)); // non-string, non-null mixed payloads are invalid downcast targets - } - } - - emitter.label(&string_case); - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the lookup input register - } - emit_push_iterator_iterator_downcast_status_from_string(emitter, ctx); - abi::emit_jump(emitter, &done); // converge after pushing the resolved downcast metadata - - emitter.label(&null_case); - emit_push_iterator_iterator_downcast_status(emitter, 0, 0); - abi::emit_jump(emitter, &done); // converge after pushing the omitted/null downcast marker - - emitter.label(&invalid_case); - emit_push_iterator_iterator_downcast_status(emitter, 2, 0); - - emitter.label(&done); -} - -/// Emits assembly for push iterator iterator downcast status from lookup. -fn emit_push_iterator_iterator_downcast_status_from_lookup( - emitter: &mut Emitter, - ctx: &mut Context, -) { - let invalid_case = ctx.next_label("iterator_iterator_downcast_lookup_invalid"); - let done = ctx.next_label("iterator_iterator_downcast_lookup_done"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the class-string lookup resolve to a declared target? - emitter.instruction(&format!("b.eq {}", invalid_case)); // unknown downcast class names fail when the source is an aggregate - emitter.instruction("cmp x2, #0"); // only concrete class targets are valid downcast classes - emitter.instruction(&format!("b.ne {}", invalid_case)); // interface names are not valid IteratorIterator downcast classes - emitter.instruction("mov x0, #1"); // status 1 means a concrete downcast class id follows - emitter.instruction(&format!("b {}", done)); // keep the resolved class id in x1 - - emitter.label(&invalid_case); - emitter.instruction("mov x0, #2"); // status 2 means the class argument must throw for aggregates - emitter.instruction("mov x1, #0"); // invalid targets have no usable class id - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the class-string lookup resolve to a declared target? - emitter.instruction(&format!("je {}", invalid_case)); // unknown downcast class names fail when the source is an aggregate - emitter.instruction("test rdx, rdx"); // only concrete class targets are valid downcast classes - emitter.instruction(&format!("jne {}", invalid_case)); // interface names are not valid IteratorIterator downcast classes - emitter.instruction("mov rax, 1"); // status 1 means a concrete downcast class id follows - emitter.instruction(&format!("jmp {}", done)); // keep the resolved class id in rdi - - emitter.label(&invalid_case); - emitter.instruction("mov rax, 2"); // status 2 means the class argument must throw for aggregates - emitter.instruction("xor edi, edi"); // invalid targets have no usable class id - } - } - emitter.label(&done); - match emitter.target.arch { - Arch::AArch64 => abi::emit_push_reg_pair(emitter, "x0", "x1"), - Arch::X86_64 => abi::emit_push_reg_pair(emitter, "rax", "rdi"), - } -} - -/// Emits assembly for push iterator iterator downcast status. -fn emit_push_iterator_iterator_downcast_status( - emitter: &mut Emitter, - status: i64, - class_id: i64, -) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_int_immediate(emitter, "x0", status); - abi::emit_load_int_immediate(emitter, "x1", class_id); - abi::emit_push_reg_pair(emitter, "x0", "x1"); - } - Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "rax", status); - abi::emit_load_int_immediate(emitter, "rdi", class_id); - abi::emit_push_reg_pair(emitter, "rax", "rdi"); - } - } -} - -/// Emits assembly for normalize saved traversable to iterator. -fn emit_normalize_saved_traversable_to_iterator( - source_expr: &Expr, - source_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let iterator_id = ctx - .interfaces - .get("Iterator") - .expect("codegen bug: missing builtin Iterator interface") - .interface_id; - let aggregate_id = ctx - .interfaces - .get("IteratorAggregate") - .expect("codegen bug: missing builtin IteratorAggregate interface") - .interface_id; - let direct_case = ctx.next_label("iterator_iterator_source_iterator"); - let aggregate_case = ctx.next_label("iterator_iterator_source_aggregate"); - let done = ctx.next_label("iterator_iterator_source_done"); - let source_is_borrowed = expr_result_heap_ownership(source_expr) != HeapOwnership::Owned; - - emit_branch_if_saved_traversable_implements(iterator_id, 16, &direct_case, emitter); - emit_branch_if_saved_traversable_implements(aggregate_id, 16, &aggregate_case, emitter); - abi::emit_release_temporary_stack(emitter, 32); // discard downcast metadata and unsupported Traversable candidate - abi::emit_call_label(emitter, "__rt_iterable_unsupported_kind"); // invalid Traversable metadata aborts defensively - - emitter.label(&direct_case); - abi::emit_release_temporary_stack(emitter, 16); // discard ignored downcast metadata for direct Iterator inputs - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the direct Iterator object pointer - if source_is_borrowed { - abi::emit_incref_if_refcounted(emitter, &source_ty.codegen_repr()); - } - abi::emit_jump(emitter, &done); // direct Iterator inputs are already normalized - - emitter.label(&aggregate_case); - emit_validate_iterator_iterator_aggregate_downcast(aggregate_id, emitter, ctx); - abi::emit_release_temporary_stack(emitter, 16); // discard validated downcast metadata before dispatching getIterator() - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the IteratorAggregate object pointer before getIterator() - move_loaded_result_to_receiver_arg(emitter); - emit_dispatch_interface_method("IteratorAggregate", "getiterator", emitter, ctx); - - emitter.label(&done); -} - -/// Emits assembly for branch if saved traversable implements. -fn emit_branch_if_saved_traversable_implements( - interface_id: u64, - candidate_stack_offset: usize, - target_label: &str, - emitter: &mut Emitter, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x0, [sp, #{}]", candidate_stack_offset)); // load the saved Traversable candidate 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"); // test whether the candidate implements the requested Traversable interface - emitter.instruction("cmp x0, #0"); // did the runtime interface matcher succeed? - emitter.instruction(&format!("b.ne {}", target_label)); // branch to the matching normalization path - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rdi, QWORD PTR [rsp + {}]", candidate_stack_offset)); // load the saved Traversable candidate 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"); // test whether the candidate implements the requested Traversable interface - emitter.instruction("test rax, rax"); // did the runtime interface matcher succeed? - emitter.instruction(&format!("jne {}", target_label)); // branch to the matching normalization path - } - } -} - -/// Emits assembly for validate iterator iterator aggregate downcast. -fn emit_validate_iterator_iterator_aggregate_downcast( - aggregate_interface_id: u64, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let skip = ctx.next_label("iterator_iterator_downcast_skip"); - let throw = ctx.next_label("iterator_iterator_downcast_throw"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid - emitter.instruction(&format!("cbz x9, {}", skip)); // omitted/null class arguments do not constrain IteratorAggregate inputs - emitter.instruction("cmp x9, #1"); // only status 1 carries a valid concrete class id - emitter.instruction(&format!("b.ne {}", throw)); // invalid class names and interfaces throw LogicException for aggregates - emitter.instruction("ldr x0, [sp, #16]"); // pass the saved IteratorAggregate object to the class matcher - emitter.instruction("ldr x1, [sp, #8]"); // pass the requested downcast class id to the class matcher - abi::emit_load_int_immediate(emitter, "x2", 0); - abi::emit_call_label(emitter, "__rt_exception_matches"); // require the aggregate object to be an instance of the requested class - emitter.instruction("cmp x0, #0"); // did the aggregate object match the requested class? - emitter.instruction(&format!("b.eq {}", throw)); // non-base downcast classes are rejected like PHP - emitter.instruction("ldr x0, [sp, #8]"); // pass the requested class id to the metadata-only interface checker - abi::emit_load_int_immediate(emitter, "x1", aggregate_interface_id as i64); - abi::emit_call_label(emitter, "__rt_class_implements_interface"); // require the downcast class itself to implement IteratorAggregate - emitter.instruction("cmp x0, #0"); // did the downcast class implement IteratorAggregate? - emitter.instruction(&format!("b.eq {}", throw)); // non-Traversable base classes are rejected like PHP - emitter.instruction(&format!("b {}", skip)); // the aggregate downcast class is valid - } - Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rsp]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid - emitter.instruction("test r10, r10"); // is there an explicit downcast class to validate? - emitter.instruction(&format!("je {}", skip)); // omitted/null class arguments do not constrain IteratorAggregate inputs - emitter.instruction("cmp r10, 1"); // only status 1 carries a valid concrete class id - emitter.instruction(&format!("jne {}", throw)); // invalid class names and interfaces throw LogicException for aggregates - emitter.instruction("mov rdi, QWORD PTR [rsp + 16]"); // pass the saved IteratorAggregate object to the class matcher - emitter.instruction("mov rsi, QWORD PTR [rsp + 8]"); // pass the requested downcast class id to the class matcher - abi::emit_load_int_immediate(emitter, "rdx", 0); - abi::emit_call_label(emitter, "__rt_exception_matches"); // require the aggregate object to be an instance of the requested class - emitter.instruction("test rax, rax"); // did the aggregate object match the requested class? - emitter.instruction(&format!("je {}", throw)); // non-base downcast classes are rejected like PHP - emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // pass the requested class id to the metadata-only interface checker - abi::emit_load_int_immediate(emitter, "rsi", aggregate_interface_id as i64); - abi::emit_call_label(emitter, "__rt_class_implements_interface"); // require the downcast class itself to implement IteratorAggregate - emitter.instruction("test rax, rax"); // did the downcast class implement IteratorAggregate? - emitter.instruction(&format!("je {}", throw)); // non-Traversable base classes are rejected like PHP - emitter.instruction(&format!("jmp {}", skip)); // the aggregate downcast class is valid - } - } - - emitter.label(&throw); - emit_throw_iterator_iterator_downcast_logic_exception(emitter); - emitter.label(&skip); -} - -/// Emits assembly for throw iterator iterator downcast logic exception. -fn emit_throw_iterator_iterator_downcast_logic_exception(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #32"); // request Throwable payload storage - emitter.instruction("bl __rt_heap_alloc"); // allocate the LogicException object payload - emitter.instruction("mov x9, #6"); // heap kind 6 = object instance - emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object - abi::emit_symbol_address(emitter, "x9", "_spl_logic_exception_class_id"); - emitter.instruction("ldr x9, [x9]"); // load LogicException's runtime class id for this program - emitter.instruction("str x9, [x0]"); // store class id at object header - abi::emit_symbol_address(emitter, "x9", "_iterator_iterator_downcast_msg"); - emitter.instruction("str x9, [x0, #8]"); // store static exception message pointer - emitter.instruction(&format!("mov x9, #{}", ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len())); // load static exception message length - emitter.instruction("str x9, [x0, #16]"); // store exception message length - emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - abi::emit_symbol_address(emitter, "x9", "_exc_value"); - emitter.instruction("str x0, [x9]"); // publish the active exception object - emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder - } - Arch::X86_64 => { - emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation - emitter.instruction("mov rbp, rsp"); // establish aligned helper frame - emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned - emitter.instruction("mov rax, 32"); // request Throwable payload storage - emitter.instruction("call __rt_heap_alloc"); // allocate the LogicException object payload - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object - abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_logic_exception_class_id", 0); // load LogicException's runtime class id for this program - emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at object header - abi::emit_symbol_address(emitter, "r10", "_iterator_iterator_downcast_msg"); // materialize static exception message pointer - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static exception message pointer - emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len())); // store static exception message length - emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); // publish the active exception object - emitter.instruction("mov rsp, rbp"); // release helper frame before throwing - emitter.instruction("pop rbp"); // restore caller frame pointer before throwing - emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder - } - } -} - -/// Moves loaded result to receiver arg into the register or storage slot expected by the next operation. -fn move_loaded_result_to_receiver_arg(emitter: &mut Emitter) { - if emitter.target.arch == Arch::X86_64 { - emitter.instruction("mov rdi, rax"); // move the object result into the SysV receiver argument register - } -} - -/// Stores iterator inner property from result into runtime storage or stack state. -fn store_iterator_inner_property_from_result(emitter: &mut Emitter, inner_offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // reload the IteratorIterator object pointer - emitter.instruction(&format!("str x0, [x9, #{}]", inner_offset)); // store the normalized inner Iterator object - emitter.instruction("mov x10, #6"); // runtime property tag 6 = object - emitter.instruction(&format!("str x10, [x9, #{}]", inner_offset + 8)); // stamp the inner property as an object - } - Arch::X86_64 => { - emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the IteratorIterator object pointer - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", inner_offset)); // store the normalized inner Iterator object - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 6", inner_offset + 8)); // stamp the inner property as an object - } - } -} - -/// Stores callable property from result into runtime storage or stack state. -fn store_callable_property_from_result(emitter: &mut Emitter, property_offset: usize) { - store_callable_property_from_result_at_stack_offset(emitter, property_offset, 0); -} - -/// Stores callable property from result on an object below temporary stack slots. -fn store_callable_property_from_result_at_stack_offset( - emitter: &mut Emitter, - property_offset: usize, - object_stack_offset: usize, -) { - if object_stack_offset != 0 { - let object_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, object_reg, object_stack_offset); - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, property_offset); - abi::emit_store_zero_to_address(emitter, object_reg, property_offset + 8); - return; - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // reload the object pointer that owns the callable property - emitter.instruction(&format!("str x0, [x9, #{}]", property_offset)); // store the callable descriptor pointer - emitter.instruction(&format!("str xzr, [x9, #{}]", property_offset + 8)); // clear the unused inline property metadata slot for callable descriptors - } - Arch::X86_64 => { - emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the object pointer that owns the callable property - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", property_offset)); // store the callable descriptor pointer - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", property_offset + 8)); // clear the unused inline property metadata slot for callable descriptors - } - } -} - -/// Stores pointer property from result into runtime storage or stack state. -fn store_pointer_property_from_result(emitter: &mut Emitter, property_offset: usize) { - store_pointer_property_from_result_at_stack_offset(emitter, property_offset, 0); -} - -/// Stores pointer property from result on an object below temporary stack slots. -fn store_pointer_property_from_result_at_stack_offset( - emitter: &mut Emitter, - property_offset: usize, - object_stack_offset: usize, -) { - if object_stack_offset != 0 { - let object_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, object_reg, object_stack_offset); - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), object_reg, property_offset); - abi::emit_store_zero_to_address(emitter, object_reg, property_offset + 8); - return; - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // reload the object pointer that owns the raw pointer property - emitter.instruction(&format!("str x0, [x9, #{}]", property_offset)); // store the raw pointer payload - emitter.instruction(&format!("str xzr, [x9, #{}]", property_offset + 8)); // clear pointer property metadata because it is not PHP-owned - } - Arch::X86_64 => { - emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the object pointer that owns the raw pointer property - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", property_offset)); // store the raw pointer payload - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", property_offset + 8)); // clear pointer property metadata because it is not PHP-owned - } - } -} - -/// Stores pointer property zero into runtime storage or stack state. -fn store_pointer_property_zero(emitter: &mut Emitter, property_offset: usize) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // reload the object pointer that owns the raw pointer property - emitter.instruction(&format!("str xzr, [x9, #{}]", property_offset)); // initialize the raw pointer payload as null - emitter.instruction(&format!("str xzr, [x9, #{}]", property_offset + 8)); // clear pointer property metadata because it is not PHP-owned - } - Arch::X86_64 => { - emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the object pointer that owns the raw pointer property - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", property_offset)); // initialize the raw pointer payload as null - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", property_offset + 8)); // clear pointer property metadata because it is not PHP-owned - } - } -} - -/// Emits assembly for empty mixed array. -fn emit_empty_mixed_array(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #4"); // allocate a small empty storage array for SPL keys/values - emitter.instruction("mov x1, #8"); // Mixed storage uses pointer-sized slots - } - Arch::X86_64 => { - emitter.instruction("mov rdi, 4"); // allocate a small empty storage array for SPL keys/values - emitter.instruction("mov rsi, 8"); // Mixed storage uses pointer-sized slots - } - } - abi::emit_call_label(emitter, "__rt_array_new"); // allocate empty indexed storage - emit_convert_loaded_indexed_array_to_mixed(&PhpType::Array(Box::new(PhpType::Int)), emitter); -} - -/// Loads storage source from stack from runtime storage or stack state. -fn load_storage_source_from_stack(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x0, [sp, #16]"); // reload the preserved constructor source array - } - Arch::X86_64 => { - emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the preserved constructor source array - } - } -} - -/// Emits assembly for convert loaded indexed array to mixed. -fn emit_convert_loaded_indexed_array_to_mixed(array_ty: &PhpType, emitter: &mut Emitter) { - let elem_ty = match array_ty { - PhpType::Array(elem_ty) => elem_ty.as_ref(), - _ => &PhpType::Mixed, - }; - let tag = runtime_value_tag(&elem_ty.codegen_repr()) as i64; - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x1, #{}", tag)); // pass the current indexed-array value_type tag to the Mixed converter - } - Arch::X86_64 => { - emitter.instruction("mov rdi, rax"); // pass the loaded indexed-array pointer to the Mixed converter - emitter.instruction(&format!("mov rsi, {}", tag)); // pass the current indexed-array value_type tag to the Mixed converter - } - } - abi::emit_call_label(emitter, "__rt_array_to_mixed"); // normalize SPL storage arrays to boxed Mixed slots -} - -/// Stores storage array property from result into runtime storage or stack state. -fn store_storage_array_property_from_result( - emitter: &mut Emitter, - property_offset: usize, - object_stack_offset: usize, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x9, [sp, #{}]", object_stack_offset)); // reload the SPL storage object pointer - emitter.instruction(&format!("str x0, [x9, #{}]", property_offset)); // store the initialized storage array pointer - emitter.instruction("mov x10, #4"); // runtime property tag 4 = indexed array - emitter.instruction(&format!("str x10, [x9, #{}]", property_offset + 8)); // stamp the property as an indexed array - } - Arch::X86_64 => { - emitter.instruction(&format!("mov r11, QWORD PTR [rsp + {}]", object_stack_offset)); // reload the SPL storage object pointer - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", property_offset)); // store the initialized storage array pointer - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 4", property_offset + 8)); // stamp the property as an indexed array - } - } -} - -/// Stores storage integer property from stack into runtime storage or stack state. -fn store_storage_int_property_from_stack( - emitter: &mut Emitter, - property_offset: usize, - value_stack_offset: usize, - object_stack_offset: usize, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x9, [sp, #{}]", object_stack_offset)); // reload the SPL storage object pointer - emitter.instruction(&format!("ldr x10, [sp, #{}]", value_stack_offset)); // reload the preserved integer property value - emitter.instruction(&format!("str x10, [x9, #{}]", property_offset)); // store the integer property value - emitter.instruction(&format!("str xzr, [x9, #{}]", property_offset + 8)); // clear scalar property metadata - } - Arch::X86_64 => { - emitter.instruction(&format!("mov r11, QWORD PTR [rsp + {}]", object_stack_offset)); // reload the SPL storage object pointer - emitter.instruction(&format!("mov r10, QWORD PTR [rsp + {}]", value_stack_offset)); // reload the preserved integer property value - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], r10", property_offset)); // store the integer property value - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", property_offset + 8)); // clear scalar property metadata - } - } -} - -/// Stores storage zero property into runtime storage or stack state. -fn store_storage_zero_property( - emitter: &mut Emitter, - property_offset: usize, - object_stack_offset: usize, -) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x9, [sp, #{}]", object_stack_offset)); // reload the SPL storage object pointer - emitter.instruction(&format!("str xzr, [x9, #{}]", property_offset)); // initialize the integer property to zero - emitter.instruction(&format!("str xzr, [x9, #{}]", property_offset + 8)); // clear scalar property metadata - } - Arch::X86_64 => { - emitter.instruction(&format!("mov r11, QWORD PTR [rsp + {}]", object_stack_offset)); // reload the SPL storage object pointer - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", property_offset)); // initialize the integer property to zero - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", property_offset + 8)); // clear scalar property metadata - } - } -} - -/// Codegen interception for `new Fiber($callable)`. -/// -/// The standard `emit_new_object` path would size the object as `8 + num_props * 16`, -/// which for Fiber (zero declared properties) yields only the object header and -/// not enough room for the runtime-managed Fiber payload. We instead delegate the -/// entire allocation, stack setup, and field initialisation to `__rt_fiber_construct`, -/// passing the captured closure plus the runtime class id so `instanceof Fiber` keeps -/// working. -fn emit_new_fiber( - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let class_id = ctx - .classes - .get("Fiber") - .map(|info| info.class_id) - .unwrap_or(0); - - emitter.comment("new Fiber() — runtime construction"); - - let wrapper_label = if let Some(callable_expr) = args.first() { - super::fiber_callable::emit_fiber_callable_descriptor(callable_expr, emitter, ctx, data) - } else { - emitter.comment("WARNING: Fiber constructor missing $callback argument"); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - None - }; - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the callable descriptor across constructor-argument setup - abi::emit_load_int_immediate( - emitter, - abi::int_arg_reg_name(emitter.target, 1), - class_id as i64, - ); // load the runtime class id of Fiber into the second integer argument register - if let Some(label) = wrapper_label { - abi::emit_symbol_address(emitter, abi::int_arg_reg_name(emitter.target, 2), &label); - } else { - abi::emit_load_int_immediate(emitter, abi::int_arg_reg_name(emitter.target, 2), 0); - } - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 0)); // pop the closure pointer into the first integer argument register for the active target ABI - abi::emit_call_label(emitter, "__rt_fiber_construct"); // delegate allocation, stack setup, and field initialisation to the runtime helper - - PhpType::Object("Fiber".to_string()) -} diff --git a/src/codegen/expr/objects/dispatch/enums.rs b/src/codegen/expr/objects/dispatch/enums.rs deleted file mode 100644 index 58d748ec93..0000000000 --- a/src/codegen/expr/objects/dispatch/enums.rs +++ /dev/null @@ -1,390 +0,0 @@ -//! Purpose: -//! Lowers enum case and enum method dispatch paths. -//! Shares receiver preparation and ABI call conventions with the object call dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::objects::dispatch` -//! -//! Key details: -//! - Receiver ownership, late/static binding, and vtable slot layout must match class metadata emission. - -use crate::codegen::abi; -use crate::codegen::NULL_SENTINEL; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::emit_expr; -use crate::codegen::platform::Arch; -use crate::names::enum_case_symbol; -use crate::parser::ast::Expr; -use crate::types::{EnumCaseValue, EnumInfo, PhpType}; - -const ENUM_FROM_INVALID_BACKING_SUFFIX: &str = " is not a valid backing value for enum "; -const ENUM_FROM_INVALID_STRING_BACKING_SUFFIX: &str = "\" is not a valid backing value for enum "; - -/// Lowers `EnumName::method(...)` by routing through builtin enum helpers. -pub(super) fn emit_enum_static_method_call( - enum_name: &str, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("{}::{}()", enum_name, method)); - let Some(enum_info) = ctx.enums.get(enum_name).cloned() else { - emitter.comment(&format!("WARNING: undefined enum {}", enum_name)); - return PhpType::Int; - }; - - match method { - "cases" => emit_enum_cases(enum_name, &enum_info, emitter, ctx), - "from" => emit_enum_from_like(enum_name, &enum_info, args, emitter, ctx, data, false), - "tryfrom" => emit_enum_from_like(enum_name, &enum_info, args, emitter, ctx, data, true), - _ => { - emitter.comment(&format!("WARNING: undefined enum method {}::{}", enum_name, method)); - PhpType::Int - } - } -} - -/// Emits the `EnumName::cases()` static method, which returns an array of all -/// enum case singleton objects in declaration order. -/// -/// Each case singleton is loaded from the data section via `enum_case_symbol` -/// and stored into the payload of a newly allocated array. The array capacity -/// is set to the exact case count (or 4 for enums with no cases). The returned -/// type is `Array`. -fn emit_enum_cases( - enum_name: &str, - enum_info: &EnumInfo, - emitter: &mut Emitter, - _ctx: &mut Context, -) -> PhpType { - let capacity = if enum_info.cases.is_empty() { - 4 - } else { - enum_info.cases.len() - }; - let result_reg = abi::int_result_reg(emitter); - let array_ptr_reg = abi::symbol_scratch_reg(emitter); - let len_reg = abi::temp_int_reg(emitter.target); - let cap_reg = abi::int_arg_reg_name(emitter.target, 0); - let elem_size_reg = abi::int_arg_reg_name(emitter.target, 1); - abi::emit_load_int_immediate(emitter, cap_reg, capacity as i64); // capacity = exact enum case count (or a small empty-array default) - abi::emit_load_int_immediate(emitter, elem_size_reg, 8); // enum case arrays store one pointer per element - abi::emit_call_label(emitter, "__rt_array_new"); // allocate the enum cases array - abi::emit_push_reg(emitter, result_reg); // save the array pointer while filling elements - - for (i, case) in enum_info.cases.iter().enumerate() { - let case_label = enum_case_symbol(enum_name, &case.name); - abi::emit_load_symbol_to_reg(emitter, result_reg, &case_label, 0); // load the enum singleton pointer from its slot through the target-aware symbol helper - abi::emit_incref_if_refcounted(emitter, &PhpType::Object(enum_name.to_string())); // array storage becomes a new owner of the singleton reference - abi::emit_load_temporary_stack_slot(emitter, array_ptr_reg, 0); // peek the enum cases array pointer from the temporary stack slot - if i == 0 { - super::super::super::arrays::emit_array_value_type_stamp( - emitter, - array_ptr_reg, - &PhpType::Object(enum_name.to_string()), - ); - } - abi::emit_store_to_address(emitter, result_reg, array_ptr_reg, 24 + i * 8); // store the enum singleton pointer in the array payload - abi::emit_load_int_immediate(emitter, len_reg, (i + 1) as i64); // materialize the updated array length after appending this enum case - abi::emit_store_to_address(emitter, len_reg, array_ptr_reg, 0); // persist the new enum cases array length - } - - abi::emit_pop_reg(emitter, result_reg); // pop the enum cases array pointer into the active integer result register - PhpType::Array(Box::new(PhpType::Object(enum_name.to_string()))) -} - -/// Emits the `EnumName::from(value)` or `EnumName::tryFrom(value)` static method. -/// -/// `from` throws a catchable `ValueError` if no case matches. `tryFrom` returns -/// `null` (boxed as `Void`) when no case matches and yields a `EnumName|Void` -/// union type. Backing type must be `Int` or `Str`. For `Str` backing, the input -/// string is preserved on the temporary stack across candidate comparisons and -/// cleaned up on both the success and "no match" paths. -fn emit_enum_from_like( - enum_name: &str, - enum_info: &EnumInfo, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - is_try: bool, -) -> PhpType { - let Some(backing_ty) = enum_info.backing_type.as_ref() else { - emitter.comment(&format!("WARNING: enum {} has no backing type", enum_name)); - return PhpType::Int; - }; - let Some(arg) = args.first() else { - emitter.comment(&format!( - "WARNING: missing enum backing argument for {}::{}", - enum_name, - if is_try { "tryFrom" } else { "from" } - )); - return PhpType::Int; - }; - - let input_ty = emit_expr(arg, emitter, ctx, data); - let success_label = ctx.next_label("enum_from_success"); - let done_label = ctx.next_label("enum_from_done"); - let result_reg = abi::int_result_reg(emitter); - let string_ptr_reg = abi::string_result_regs(emitter).0; - let string_len_reg = abi::string_result_regs(emitter).1; - let string_cleanup_label = if matches!(backing_ty, PhpType::Str) { - Some(ctx.next_label("enum_from_cleanup_input")) - } else { - None - }; - - match backing_ty { - PhpType::Int => { - let _ = input_ty; - for case in &enum_info.cases { - let Some(EnumCaseValue::Int(value)) = case.value.as_ref() else { - continue; - }; - let next_label = ctx.next_label("enum_from_next"); - let case_value_reg = abi::temp_int_reg(emitter.target); - load_immediate(emitter, case_value_reg, *value); // materialize the current enum backing integer for comparison - emitter.instruction(&format!("cmp {}, {}", result_reg, case_value_reg)); // compare the input integer with the current enum backing value - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("b.ne {}", next_label)); // continue scanning when the current enum backing value does not match - } - Arch::X86_64 => { - emitter.instruction(&format!("jne {}", next_label)); // continue scanning when the current enum backing value does not match - } - } - let case_label = enum_case_symbol(enum_name, &case.name); - abi::emit_load_symbol_to_reg(emitter, result_reg, &case_label, 0); // load the matching enum singleton pointer - abi::emit_jump(emitter, &success_label); // return the matching enum singleton immediately - emitter.label(&next_label); - } - } - PhpType::Str => { - abi::emit_push_reg_pair(emitter, string_ptr_reg, string_len_reg); // preserve the input string payload across candidate comparisons - for case in &enum_info.cases { - let Some(EnumCaseValue::Str(value)) = case.value.as_ref() else { - continue; - }; - let match_label = ctx.next_label("enum_from_case"); - let next_label = ctx.next_label("enum_from_next"); - let bytes = crate::string_bytes::literal_bytes(value); - let (label, len) = data.add_string(&bytes); - let (input_ptr_reg, input_len_reg, candidate_ptr_reg, candidate_len_reg) = - match emitter.target.arch { - Arch::AArch64 => ("x1", "x2", "x3", "x4"), - Arch::X86_64 => ("rdi", "rsi", "rdx", "rcx"), - }; - abi::emit_load_temporary_stack_slot(emitter, input_ptr_reg, 0); // reload the input string pointer into the first __rt_str_eq argument register for this candidate comparison - abi::emit_load_temporary_stack_slot(emitter, input_len_reg, 8); // reload the input string length into the paired __rt_str_eq argument register for this candidate comparison - abi::emit_symbol_address(emitter, candidate_ptr_reg, &label); // materialize the candidate enum backing string address - abi::emit_load_int_immediate(emitter, candidate_len_reg, len as i64); // materialize the candidate enum backing string length - abi::emit_call_label(emitter, "__rt_str_eq"); // compare the input string against the candidate backing string - abi::emit_branch_if_int_result_nonzero(emitter, &match_label); // branch when the current enum backing string matches - abi::emit_jump(emitter, &next_label); // continue scanning when the current enum backing string does not match - emitter.label(&match_label); - let case_label = enum_case_symbol(enum_name, &case.name); - abi::emit_load_symbol_to_reg(emitter, result_reg, &case_label, 0); // load the matching enum singleton pointer - if let Some(cleanup_label) = &string_cleanup_label { - abi::emit_jump(emitter, cleanup_label); // drop the preserved input string before returning the match - } - emitter.label(&next_label); - } - } - _ => { - emitter.comment("WARNING: unsupported enum backing type in codegen"); - return PhpType::Int; - } - } - - if is_try { - if matches!(backing_ty, PhpType::Str) { - abi::emit_release_temporary_stack(emitter, 16); // drop the preserved input string payload before returning null - } - emit_null_into_x0(emitter); - crate::codegen::emit_box_current_value_as_mixed(emitter, &PhpType::Void); - abi::emit_jump(emitter, &done_label); // return boxed null when tryFrom() does not match any case - } else { - emit_enum_from_value_error(enum_name, backing_ty, emitter, data); - } - - if let Some(cleanup_label) = &string_cleanup_label { - emitter.label(cleanup_label); - abi::emit_release_temporary_stack(emitter, 16); // drop the preserved input string payload before returning the matching singleton - abi::emit_jump(emitter, &success_label); // continue through the shared success path with a clean stack - } - - emitter.label(&success_label); - if is_try { - crate::codegen::emit_box_current_value_as_mixed( - emitter, - &PhpType::Object(enum_name.to_string()), - ); - } - emitter.label(&done_label); - if is_try { - PhpType::Union(vec![PhpType::Object(enum_name.to_string()), PhpType::Void]) - } else { - PhpType::Object(enum_name.to_string()) - } -} - -/// Builds and throws the PHP-compatible `ValueError` for `Enum::from()` when -/// no declared case has the requested backing value. Leaves no enum-input -/// temporary stack storage behind before entering the exception unwinder. -fn emit_enum_from_value_error( - enum_name: &str, - backing_ty: &PhpType, - emitter: &mut Emitter, - data: &mut DataSection, -) { - match backing_ty { - PhpType::Int => emit_enum_from_int_value_error_message(enum_name, emitter, data), - PhpType::Str => emit_enum_from_string_value_error_message(enum_name, emitter, data), - _ => return, - } - abi::emit_call_label(emitter, "__rt_str_persist"); // copy the dynamically composed ValueError message into stable heap storage - if matches!(backing_ty, PhpType::Str) { - abi::emit_release_temporary_stack(emitter, 16); // drop the preserved unmatched string after the message has been copied - } - emit_throw_value_error_from_string_result(emitter); -} - -/// Emits the dynamic error-message text for an unmatched integer-backed enum -/// value. The unmatched integer is still in the active integer result register -/// when this helper runs. -fn emit_enum_from_int_value_error_message( - enum_name: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - abi::emit_call_label(emitter, "__rt_itoa"); // convert the unmatched backing integer to decimal text - let suffix = format!("{}{}", ENUM_FROM_INVALID_BACKING_SUFFIX, enum_name); - emit_concat_current_string_with_static_suffix(&suffix, emitter, data); -} - -/// Emits the dynamic error-message text for an unmatched string-backed enum -/// value. The input string pointer and length are still preserved in the -/// temporary stack slot created before candidate scanning. -fn emit_enum_from_string_value_error_message( - enum_name: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - emit_concat_static_prefix_with_preserved_string("\"", emitter, data); - let suffix = format!( - "{}{}", - ENUM_FROM_INVALID_STRING_BACKING_SUFFIX, - enum_name - ); - emit_concat_current_string_with_static_suffix(&suffix, emitter, data); -} - -/// Concatenates a static prefix with the preserved enum input string and leaves -/// the resulting string in the target's string-result registers. -fn emit_concat_static_prefix_with_preserved_string( - prefix: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (prefix_label, prefix_len) = data.add_string(prefix.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x1", &prefix_label); - abi::emit_load_int_immediate(emitter, "x2", prefix_len as i64); - abi::emit_load_temporary_stack_slot(emitter, "x3", 0); - abi::emit_load_temporary_stack_slot(emitter, "x4", 8); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rax", &prefix_label); - abi::emit_load_int_immediate(emitter, "rdx", prefix_len as i64); - abi::emit_load_temporary_stack_slot(emitter, "rdi", 0); - abi::emit_load_temporary_stack_slot(emitter, "rsi", 8); - } - } - abi::emit_call_label(emitter, "__rt_concat"); // copy the static prefix and preserved input into the concat buffer -} - -/// Concatenates the current string result with a static suffix and leaves the -/// resulting string in the target's string-result registers. -fn emit_concat_current_string_with_static_suffix( - suffix: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let (suffix_label, suffix_len) = data.add_string(suffix.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(emitter, "x3", &suffix_label); - abi::emit_load_int_immediate(emitter, "x4", suffix_len as i64); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rdi", &suffix_label); - abi::emit_load_int_immediate(emitter, "rsi", suffix_len as i64); - } - } - abi::emit_call_label(emitter, "__rt_concat"); // append the static suffix to the current dynamic message prefix -} - -/// Allocates a `ValueError` object using the current string result as its -/// message, publishes it in `_exc_value`, and enters the standard exception -/// unwinder. The current string must already be heap-persisted by the caller. -fn emit_throw_value_error_from_string_result(emitter: &mut Emitter) { - let (message_ptr_reg, message_len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, message_ptr_reg, message_len_reg); // preserve the dynamic message while allocating the exception object - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_int_immediate(emitter, "x0", 32); // request Throwable payload storage - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction("mov x9, #6"); // heap kind 6 = throwable object instance - emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object - abi::emit_load_symbol_to_reg(emitter, "x9", "_spl_value_error_class_id", 0); - emitter.instruction("str x9, [x0]"); // store ValueError class id at object header - abi::emit_load_temporary_stack_slot(emitter, "x9", 0); - emitter.instruction("str x9, [x0, #8]"); // store dynamic exception message pointer - abi::emit_load_temporary_stack_slot(emitter, "x9", 8); - emitter.instruction("str x9, [x0, #16]"); // store dynamic exception message length - emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero - abi::emit_store_reg_to_symbol(emitter, "x0", "_exc_value", 0); - abi::emit_release_temporary_stack(emitter, 16); // release the preserved dynamic-message pair before unwinding - abi::emit_jump(emitter, "__rt_throw_current"); // enter the standard exception unwinder - } - Arch::X86_64 => { - abi::emit_load_int_immediate(emitter, "rax", 32); // request Throwable payload storage - abi::emit_call_label(emitter, "__rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object - abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_value_error_class_id", 0); - emitter.instruction("mov QWORD PTR [rax], r10"); // store ValueError class id at object header - abi::emit_load_temporary_stack_slot(emitter, "r10", 0); - emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store dynamic exception message pointer - abi::emit_load_temporary_stack_slot(emitter, "r10", 8); - emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store dynamic exception message length - emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - abi::emit_store_reg_to_symbol(emitter, "rax", "_exc_value", 0); - abi::emit_release_temporary_stack(emitter, 16); // release the preserved dynamic-message pair before unwinding - abi::emit_jump(emitter, "__rt_throw_current"); // enter the standard exception unwinder - } - } -} - -/// Materialises the shared null sentinel value (a known non-null pointer bit -/// pattern) into the active integer result register. Used by `tryFrom` to -/// represent a null return before boxing it as `Void`. -fn emit_null_into_x0(emitter: &mut Emitter) { - abi::emit_load_int_immediate( - emitter, - abi::int_result_reg(emitter), - NULL_SENTINEL, - ); // materialize the shared null sentinel in the active integer result register -} - -/// Materialises a 64-bit signed integer immediate into the specified register -/// via `abi::emit_load_int_immediate`. Used only for enum backing value -/// comparisons in `emit_enum_from_like`. -fn load_immediate(emitter: &mut Emitter, reg: &str, value: i64) { - abi::emit_load_int_immediate(emitter, reg, value); // materialize the immediate through the shared target-aware helper -} diff --git a/src/codegen/expr/objects/dispatch/interface.rs b/src/codegen/expr/objects/dispatch/interface.rs deleted file mode 100644 index 7136d76cee..0000000000 --- a/src/codegen/expr/objects/dispatch/interface.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Purpose: -//! Lowers interface method dispatch through vtable-compatible wrapper targets. -//! Shares receiver preparation and ABI call conventions with the object call dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::objects::dispatch` -//! -//! Key details: -//! - Receiver ownership, late/static binding, and vtable slot layout must match class metadata emission. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::emit::Emitter; -use crate::intrinsics::IntrinsicCall; -use crate::types::PhpType; - -use super::super::super::{ - restore_concat_offset_after_nested_call, restore_concat_offset_after_owned_string_call, - save_concat_offset_before_nested_call, -}; - -/// Emits interface method dispatch by scanning the receiver's implemented-interfaces -/// metadata for a matching interface ID, then branching to the resolved method slot. -/// -/// Shares receiver preparation and ABI call conventions with the object call dispatcher. -/// Uses the `_class_interface_ptrs` global symbol; vtable slot layout must match class -/// metadata emission in the runtime data segment. -/// -/// # Arguments -/// * `interface_name` - The target interface name for dispatch -/// * `method` - The method name to invoke on the interface -/// * `emitter` - Assembly emitter (consumed/reused for all emitted instructions) -/// * `ctx` - Codegen context providing interface metadata, labels, and platform info -/// -/// # Returns -/// The `PhpType` of the resolved interface method (fallback to `PhpType::Int` if -/// interface or slot metadata is absent; valid programs never trigger this fallback). -pub(crate) fn emit_dispatch_interface_method( - interface_name: &str, - method: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let Some(interface_info) = ctx.interfaces.get(interface_name).cloned() else { - emitter.comment(&format!( - "WARNING: missing interface metadata for {}::{}", - interface_name, method - )); - return PhpType::Int; - }; - let ret_ty = interface_info - .methods - .get(method) - .map(|sig| sig.return_type.clone()) - .unwrap_or(PhpType::Int); - let Some(slot) = interface_info.method_slots.get(method).copied() else { - emitter.comment(&format!( - "WARNING: missing interface slot for {}::{}", - interface_name, method - )); - return ret_ty; - }; - - let interface_id = interface_info.interface_id as i64; - let scan_loop = ctx.next_label("interface_dispatch_scan"); - let found = ctx.next_label("interface_dispatch_found"); - let done = ctx.next_label("interface_dispatch_done"); - let missing = ctx.next_label("interface_dispatch_missing"); - - save_concat_offset_before_nested_call(emitter, ctx); - if interface_name == "Iterator" { - if let Some(rt_label) = IntrinsicCall::instance_method("Generator", method) - .and_then(|intrinsic| intrinsic.runtime_helper()) - { - emit_generator_interface_fast_path(rt_label, &done, emitter, ctx); - } - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldr x10, [x0]"); // load the receiver object's runtime class id without consuming x0 - abi::emit_symbol_address(emitter, "x11", "_class_interface_ptrs"); - emitter.instruction("ldr x11, [x11, x10, lsl #3]"); // select the receiver class's emitted interface metadata block - emitter.instruction("ldr x10, [x11]"); // load the number of implemented interface entries to scan - emitter.instruction("add x11, x11, #8"); // advance to the first [interface_id, impl_table] pair - abi::emit_load_int_immediate(emitter, "x13", interface_id); - - emitter.label(&scan_loop); - emitter.instruction(&format!("cbz x10, {}", missing)); // stop scanning if no implemented interface matched the target id - emitter.instruction("ldr x12, [x11]"); // load the current implemented interface id - emitter.instruction("cmp x12, x13"); // compare the current interface id with the dispatch target - emitter.instruction(&format!("b.eq {}", found)); // use this implementation table when the interface id matches - emitter.instruction("add x11, x11, #16"); // advance to the next [interface_id, impl_table] pair - emitter.instruction("sub x10, x10, #1"); // consume one implemented interface metadata entry - emitter.instruction(&format!("b {}", scan_loop)); // continue scanning the receiver's implemented interfaces - - emitter.label(&found); - emitter.instruction("ldr x11, [x11, #8]"); // load the implementation table pointer for the matched interface - if slot == 0 { - emitter.instruction("ldr x11, [x11]"); // load the first method implementation pointer from the interface table - } else { - emitter.instruction(&format!("ldr x11, [x11, #{}]", slot * 8)); // load the selected method implementation pointer from the interface table - } - emitter.instruction("blr x11"); // call the resolved interface method implementation - emitter.instruction(&format!("b {}", done)); // skip the defensive missing-interface fallback - - emitter.label(&missing); - emitter.instruction("mov x0, #0"); // defensive fallback for invalid runtime metadata; valid programs never take this path - emitter.label(&done); - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the receiver object's runtime class id without consuming rdi - abi::emit_symbol_address(emitter, "r11", "_class_interface_ptrs"); - emitter.instruction("mov r11, QWORD PTR [r11 + r10 * 8]"); // select the receiver class's emitted interface metadata block - emitter.instruction("mov r10, QWORD PTR [r11]"); // load the number of implemented interface entries to scan - emitter.instruction("add r11, 8"); // advance to the first [interface_id, impl_table] pair - abi::emit_load_int_immediate(emitter, "r9", interface_id); - - emitter.label(&scan_loop); - emitter.instruction("test r10, r10"); // check whether any implemented interface entries remain - emitter.instruction(&format!("je {}", missing)); // stop scanning if no implemented interface matched the target id - emitter.instruction("mov r8, QWORD PTR [r11]"); // load the current implemented interface id - emitter.instruction("cmp r8, r9"); // compare the current interface id with the dispatch target - emitter.instruction(&format!("je {}", found)); // use this implementation table when the interface id matches - emitter.instruction("add r11, 16"); // advance to the next [interface_id, impl_table] pair - emitter.instruction("sub r10, 1"); // consume one implemented interface metadata entry - emitter.instruction(&format!("jmp {}", scan_loop)); // continue scanning the receiver's implemented interfaces - - emitter.label(&found); - emitter.instruction("mov r11, QWORD PTR [r11 + 8]"); // load the implementation table pointer for the matched interface - if slot == 0 { - emitter.instruction("mov r11, QWORD PTR [r11]"); // load the first method implementation pointer from the interface table - } else { - emitter.instruction(&format!("mov r11, QWORD PTR [r11 + {}]", slot * 8)); // load the selected method implementation pointer from the interface table - } - emitter.instruction("call r11"); // call the resolved interface method implementation - emitter.instruction(&format!("jmp {}", done)); // skip the defensive missing-interface fallback - - emitter.label(&missing); - emitter.instruction("xor eax, eax"); // defensive fallback for invalid runtime metadata; valid programs never take this path - emitter.label(&done); - } - } - if ret_ty == PhpType::Str { - restore_concat_offset_after_owned_string_call(emitter, ctx); - } else { - restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } - - ret_ty -} - -/// Emits a fast path for `Iterator` methods when the receiver is the built-in `Generator` class, -/// bypassing the generic interface-vtable scan. -/// -/// Checks the receiver's class ID against `_generator_class_id` and, on match, calls the -/// appropriate runtime helper directly before jumping to `done`. Non-Generator receivers fall -/// through to the `not_generator` label to continue with normal interface dispatch. -/// -/// # Arguments -/// * `rt_label` - Runtime helper label to call when the receiver is a Generator -/// * `done` - Label to jump to after the fast path completes, skipping the generic dispatch path -/// * `emitter` - Assembly emitter -/// * `ctx` - Codegen context providing labels and platform info -/// -/// # Notes -/// This fast path is only valid for the `Iterator` interface, which `Generator` implements natively. -fn emit_generator_interface_fast_path( - rt_label: &str, - done: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) { - let not_generator = ctx.next_label("interface_dispatch_not_generator"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction("ldr x10, [x0]"); // load the receiver class id before checking for the built-in Generator - abi::emit_load_symbol_to_reg(emitter, "x11", "_generator_class_id", 0); - emitter.instruction("cmp x10, x11"); // compare the receiver class id with the built-in Generator class id - emitter.instruction(&format!("b.ne {}", not_generator)); // fall back to interface dispatch for non-Generator iterators - abi::emit_call_label(emitter, rt_label); // call the Generator runtime helper instead of the synthetic stub - emitter.instruction(&format!("b {}", done)); // skip the generic interface-vtable dispatch path - emitter.label(¬_generator); - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the receiver class id before checking for the built-in Generator - abi::emit_load_symbol_to_reg(emitter, "r11", "_generator_class_id", 0); - emitter.instruction("cmp r10, r11"); // compare the receiver class id with the built-in Generator class id - emitter.instruction(&format!("jne {}", not_generator)); // fall back to interface dispatch for non-Generator iterators - abi::emit_call_label(emitter, rt_label); // call the Generator runtime helper instead of the synthetic stub - emitter.instruction(&format!("jmp {}", done)); // skip the generic interface-vtable dispatch path - emitter.label(¬_generator); - } - } -} diff --git a/src/codegen/expr/objects/dispatch/intrinsic.rs b/src/codegen/expr/objects/dispatch/intrinsic.rs deleted file mode 100644 index 82f5047280..0000000000 --- a/src/codegen/expr/objects/dispatch/intrinsic.rs +++ /dev/null @@ -1,487 +0,0 @@ -//! Purpose: -//! Lowers runtime-managed intrinsic method calls for core objects such as Fiber and Generator. -//! Keeps direct runtime-helper interception behind a shared `IntrinsicCall` registry. -//! -//! Called from: -//! - `crate::codegen::expr::objects::dispatch` -//! -//! Key details: -//! - Receivers and visible arguments are prepared by the normal method-call path before this file dispatches. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::intrinsics::{IntrinsicCall, IntrinsicCallForm, IntrinsicCallKind}; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::super::super::{ - coerce_result_to_type, emit_expr, restore_concat_offset_after_nested_call, - restore_concat_offset_after_owned_string_call, save_concat_offset_before_nested_call, -}; - -/// Maps an `IntrinsicCallKind` to its PHP return type. -/// -/// Used by both static and instance intrinsic lowering to determine the result type -/// when emitting calls or falling back after an unsupported intrinsic warning. -pub(super) fn return_type_for(intrinsic: IntrinsicCall) -> PhpType { - match intrinsic.kind() { - IntrinsicCallKind::FiberIsStarted - | IntrinsicCallKind::FiberIsRunning - | IntrinsicCallKind::FiberIsSuspended - | IntrinsicCallKind::FiberIsTerminated - | IntrinsicCallKind::GeneratorValid - | IntrinsicCallKind::CallbackFilterAccept - | IntrinsicCallKind::SplDllIsEmpty - | IntrinsicCallKind::SplDllOffsetExists - | IntrinsicCallKind::SplDllValid - | IntrinsicCallKind::SplFixedOffsetExists => PhpType::Bool, - IntrinsicCallKind::SplRecursiveAssumeIterator => { - PhpType::Object("RecursiveIterator".to_string()) - } - IntrinsicCallKind::SplDllCount - | IntrinsicCallKind::SplDllGetIteratorMode - | IntrinsicCallKind::SplFixedCount - | IntrinsicCallKind::SplFixedGetSize => PhpType::Int, - IntrinsicCallKind::SplDllSerialize => PhpType::Str, - IntrinsicCallKind::SplFixedToArray | IntrinsicCallKind::SplFixedJsonSerialize => { - PhpType::Array(Box::new(PhpType::Mixed)) - } - IntrinsicCallKind::SplDllSerializeArray => PhpType::Array(Box::new(PhpType::Mixed)), - IntrinsicCallKind::SplFixedFromArray => { - PhpType::Object("SplFixedArray".to_string()) - } - IntrinsicCallKind::GeneratorNext - | IntrinsicCallKind::GeneratorRewind - | IntrinsicCallKind::SplDllAdd - | IntrinsicCallKind::SplDllPush - | IntrinsicCallKind::SplDllUnshift - | IntrinsicCallKind::SplDllSetIteratorMode - | IntrinsicCallKind::SplDllUnserialize - | IntrinsicCallKind::SplDllOffsetSet - | IntrinsicCallKind::SplDllOffsetUnset - | IntrinsicCallKind::SplDllRewind - | IntrinsicCallKind::SplDllPrev - | IntrinsicCallKind::SplDllNext - | IntrinsicCallKind::SplQueueEnqueue - | IntrinsicCallKind::SplFixedConstruct - | IntrinsicCallKind::SplFixedSetSize - | IntrinsicCallKind::SplFixedUnserialize - | IntrinsicCallKind::SplFixedOffsetSet - | IntrinsicCallKind::SplFixedOffsetUnset => PhpType::Void, - _ => PhpType::Mixed, - } -} - -/// Lowers a static intrinsic call such as `Fiber::suspend(...)` or `SplFixedArray::fromArray(...)`. -/// -/// Arguments are emitted and coerced before the runtime helper is called. For `Fiber::suspend`, -/// the argument (or `null`) is shuttled through the temporary stack before being passed as the -/// first integer argument to the helper. For `SplFixedArray::fromArray`, the source array and -/// optional `preserveKeys` boolean are passed as arguments 2 and 3 after the class ID in arg 0. -pub(super) fn emit_static_intrinsic_call( - intrinsic: IntrinsicCall, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - debug_assert_eq!(intrinsic.form(), IntrinsicCallForm::Static); - emitter.comment(&format!( - "{}::{}() intrinsic runtime dispatch", - intrinsic.class_name(), - intrinsic.method_key() - )); - - match intrinsic.kind() { - IntrinsicCallKind::FiberSuspend => { - if let Some(value_expr) = args.first() { - let actual_ty = emit_expr(value_expr, emitter, ctx, data); - coerce_result_to_type(emitter, ctx, data, &actual_ty, &PhpType::Mixed); - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - coerce_result_to_type(emitter, ctx, data, &PhpType::Void, &PhpType::Mixed); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // shuttle the boxed Mixed pointer through the temporary stack - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 0)); // pass the suspend value as runtime helper argument 1 - abi::emit_call_label( - emitter, - intrinsic - .runtime_helper() - .expect("Fiber::suspend intrinsic must have a runtime helper"), - ); // suspend the current Fiber and return the resumed Mixed payload - PhpType::Mixed - } - IntrinsicCallKind::FiberGetCurrent => { - abi::emit_call_label( - emitter, - intrinsic - .runtime_helper() - .expect("Fiber::getCurrent intrinsic must have a runtime helper"), - ); // read the currently running Fiber from runtime state - PhpType::Mixed - } - IntrinsicCallKind::SplFixedFromArray => { - let Some(array_expr) = args.first() else { - emitter.comment("WARNING: SplFixedArray::fromArray() intrinsic missing array argument"); - return return_type_for(intrinsic); - }; - let array_ty = emit_expr(array_expr, emitter, ctx, data); - coerce_result_to_type(emitter, ctx, data, &array_ty, &PhpType::Array(Box::new(PhpType::Mixed))); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the source array while optional arguments are evaluated - if let Some(preserve_expr) = args.get(1) { - let preserve_ty = emit_expr(preserve_expr, emitter, ctx, data); - coerce_result_to_type(emitter, ctx, data, &preserve_ty, &PhpType::Bool); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the runtime preserveKeys flag for the SPL helper - } else { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 1); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // default preserveKeys=true, matching PHP - } - let class_id = ctx - .classes - .get("SplFixedArray") - .map(|info| info.class_id) - .unwrap_or(u64::MAX); - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 2)); // pass preserveKeys as runtime helper argument 3 - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 1)); // pass the source PHP array as runtime helper argument 2 - abi::emit_load_int_immediate( - emitter, - abi::int_arg_reg_name(emitter.target, 0), - class_id as i64, - ); - abi::emit_call_label( - emitter, - intrinsic - .runtime_helper() - .expect("SplFixedArray::fromArray intrinsic must have a runtime helper"), - ); // build a SplFixedArray from the source PHP array - PhpType::Object("SplFixedArray".to_string()) - } - other => { - emitter.comment(&format!( - "WARNING: unsupported static intrinsic {:?} for {}::{}", - other, - intrinsic.class_name(), - intrinsic.method_key() - )); - return_type_for(intrinsic) - } - } -} - -/// Lowers an instance intrinsic call where arguments are already materialized in registers. -/// -/// The receiver is in `x0` (ARM64) or `rdi` (x86_64). Additional arguments are sourced from -/// `assignments` which describes where each argument currently lives (register or stack overflow). -/// `overflow_bytes` describes how many bytes of stack arguments were passed beyond the ABI limit. -/// Falls through to `emit_simple_runtime_intrinsic` for most kinds; handles `Fiber::start` specially. -pub(super) fn emit_instance_intrinsic_with_loaded_args( - intrinsic: IntrinsicCall, - assignments: &[abi::OutgoingArgAssignment], - _overflow_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - debug_assert_eq!(intrinsic.form(), IntrinsicCallForm::Instance); - match intrinsic.kind() { - IntrinsicCallKind::FiberStart => emit_fiber_start_intrinsic(intrinsic, assignments, emitter, ctx), - IntrinsicCallKind::FiberResume - | IntrinsicCallKind::FiberThrow - | IntrinsicCallKind::FiberGetReturn => emit_simple_runtime_intrinsic(intrinsic, emitter), - IntrinsicCallKind::FiberIsStarted - | IntrinsicCallKind::FiberIsRunning - | IntrinsicCallKind::FiberIsSuspended - | IntrinsicCallKind::FiberIsTerminated => emit_fiber_state_intrinsic(intrinsic, emitter), - IntrinsicCallKind::GeneratorCurrent - | IntrinsicCallKind::GeneratorKey - | IntrinsicCallKind::GeneratorNext - | IntrinsicCallKind::GeneratorValid - | IntrinsicCallKind::GeneratorRewind - | IntrinsicCallKind::GeneratorSend - | IntrinsicCallKind::GeneratorThrow - | IntrinsicCallKind::GeneratorGetReturn => emit_generator_intrinsic(intrinsic, emitter, ctx), - IntrinsicCallKind::CallbackFilterAccept => { - emit_callback_filter_accept_intrinsic(intrinsic, emitter, ctx) - } - IntrinsicCallKind::SplRecursiveAssumeIterator => { - emit_recursive_assume_iterator_intrinsic(emitter, ctx) - } - IntrinsicCallKind::SplDllAdd - | IntrinsicCallKind::SplDllPop - | IntrinsicCallKind::SplDllShift - | IntrinsicCallKind::SplDllPush - | IntrinsicCallKind::SplDllUnshift - | IntrinsicCallKind::SplDllTop - | IntrinsicCallKind::SplDllBottom - | IntrinsicCallKind::SplDllCount - | IntrinsicCallKind::SplDllIsEmpty - | IntrinsicCallKind::SplDllSetIteratorMode - | IntrinsicCallKind::SplDllGetIteratorMode - | IntrinsicCallKind::SplDllSerialize - | IntrinsicCallKind::SplDllUnserialize - | IntrinsicCallKind::SplDllSerializeArray - | IntrinsicCallKind::SplDllOffsetExists - | IntrinsicCallKind::SplDllOffsetGet - | IntrinsicCallKind::SplDllOffsetSet - | IntrinsicCallKind::SplDllOffsetUnset - | IntrinsicCallKind::SplDllRewind - | IntrinsicCallKind::SplDllCurrent - | IntrinsicCallKind::SplDllKey - | IntrinsicCallKind::SplDllPrev - | IntrinsicCallKind::SplDllNext - | IntrinsicCallKind::SplDllValid - | IntrinsicCallKind::SplQueueEnqueue - | IntrinsicCallKind::SplQueueDequeue - | IntrinsicCallKind::SplFixedConstruct - | IntrinsicCallKind::SplFixedCount - | IntrinsicCallKind::SplFixedToArray - | IntrinsicCallKind::SplFixedGetSize - | IntrinsicCallKind::SplFixedSetSize - | IntrinsicCallKind::SplFixedOffsetExists - | IntrinsicCallKind::SplFixedOffsetGet - | IntrinsicCallKind::SplFixedOffsetSet - | IntrinsicCallKind::SplFixedOffsetUnset - | IntrinsicCallKind::SplFixedJsonSerialize - | IntrinsicCallKind::SplFixedUnserialize => emit_simple_runtime_intrinsic(intrinsic, emitter), - IntrinsicCallKind::FiberSuspend - | IntrinsicCallKind::FiberGetCurrent - | IntrinsicCallKind::SplFixedFromArray => { - emitter.comment(&format!( - "WARNING: static intrinsic used as instance call {:?}", - intrinsic.kind() - )); - return_type_for(intrinsic) - } - } -} - -/// Lowers `Fiber::start` by copying user-supplied start arguments into the Fiber's start_args buffer -/// before invoking the runtime helper. -/// -/// Uses `FIBER_USER_ARG_MAX_OFFSET` to limit how many slots the callee may write, and -/// `FIBER_START_ARGS_OFFSET` as the base for the start_args array. Only arguments up to -/// `assignments.len()` are copied, capped by `FIBER_START_ARGS_MAX`. ARM64 spills registers -/// directly; x86_64 handles stack overflow slots by loading from the known overflow area. -fn emit_fiber_start_intrinsic( - intrinsic: IntrinsicCall, - assignments: &[abi::OutgoingArgAssignment], - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let max_arg_off = crate::codegen::runtime::FIBER_USER_ARG_MAX_OFFSET; - let skip_label = ctx.next_label("fiber_start_args_done"); - let supplied_arg_count = assignments - .len() - .min(crate::codegen::runtime::FIBER_START_ARGS_MAX as usize); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x9, [x0, #{}]", max_arg_off)); // x9 = how many start_args slots start() may write - for i in 0..supplied_arg_count { - let src = abi::int_arg_reg_name(emitter.target, assignments[i].start_reg); - let off = crate::codegen::runtime::FIBER_START_ARGS_OFFSET + (i as i32) * 8; - emitter.instruction(&format!("cmp x9, #{}", i + 1)); // is this supplied argument still within user_arg_max? - emitter.instruction(&format!("b.lt {}", skip_label)); // stop spilling once we hit the capture-reserved tail - emitter.instruction(&format!("str {}, [x0, #{}]", src, off)); // start_args[i] = caller-supplied Mixed value - } - } - Arch::X86_64 => { - emitter.instruction(&format!("mov r11, QWORD PTR [rdi + {}]", max_arg_off)); // r11 = how many start_args slots start() may write - let mut overflow_slot = 0usize; - for (i, assignment) in assignments.iter().take(supplied_arg_count).enumerate() { - let off = crate::codegen::runtime::FIBER_START_ARGS_OFFSET + (i as i32) * 8; - emitter.instruction(&format!("cmp r11, {}", i + 1)); // is this slot index still within user_arg_max? - emitter.instruction(&format!("jl {}", skip_label)); // stop spilling once we hit the capture-reserved tail - if assignment.in_register() { - let src = abi::int_arg_reg_name(emitter.target, assignment.start_reg); - emitter.instruction(&format!("mov QWORD PTR [rdi + {}], {}", off, src)); // start_args[i] = caller-supplied Mixed value - } else { - let stack_offset = overflow_slot * 16; - if stack_offset == 0 { - emitter.instruction("mov r10, QWORD PTR [rsp]"); // load stack-passed start() Mixed argument from the top overflow slot - } else { - emitter.instruction(&format!("mov r10, QWORD PTR [rsp + {}]", stack_offset)); // load stack-passed start() Mixed argument from its overflow slot - } - emitter.instruction(&format!("mov QWORD PTR [rdi + {}], r10", off)); // start_args[i] = caller-supplied stack-passed Mixed value - overflow_slot += 1; - } - } - } - } - emitter.label(&skip_label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x9, #{}", supplied_arg_count)); // materialize how many boxed start() values were supplied - emitter.instruction(&format!("str x9, [x0, #{}]", crate::codegen::runtime::FIBER_START_ARG_COUNT_OFFSET)); // record start() arity for descriptor-backed Fiber invokers - } - Arch::X86_64 => { - emitter.instruction(&format!("mov QWORD PTR [rdi + {}], {}", crate::codegen::runtime::FIBER_START_ARG_COUNT_OFFSET, supplied_arg_count)); // record start() arity for descriptor-backed Fiber invokers - } - } - abi::emit_call_label( - emitter, - intrinsic - .runtime_helper() - .expect("Fiber::start intrinsic must have a runtime helper"), - ); // switch into the Fiber runtime and return the yielded Mixed value - PhpType::Mixed -} - -/// Lowers simple instance intrinsics that only need a runtime helper call with no special setup. -/// -/// Receiver and arguments are already materialized in registers per the ABI. Calls the -/// runtime helper directly and returns the type for the intrinsic kind. -fn emit_simple_runtime_intrinsic(intrinsic: IntrinsicCall, emitter: &mut Emitter) -> PhpType { - abi::emit_call_label( - emitter, - intrinsic - .runtime_helper() - .expect("simple intrinsic must have a runtime helper"), - ); // call the runtime helper with the already materialized receiver and args - return_type_for(intrinsic) -} - -/// Lowers Fiber state-query intrinsics (`Fiber::isStarted`, `isRunning`, `isSuspended`, `isTerminated`). -/// -/// Sets argument 1 to the expected state enum value (0–3), calls the shared runtime predicate helper, -/// then inverts the result for `Fiber::isStarted` since the runtime encodes `NotStarted` as the -/// absence of the Started state (state == 0 means not started). -fn emit_fiber_state_intrinsic(intrinsic: IntrinsicCall, emitter: &mut Emitter) -> PhpType { - let arg1 = abi::int_arg_reg_name(emitter.target, 1); - let expected_state = match intrinsic.kind() { - IntrinsicCallKind::FiberIsStarted => 0, - IntrinsicCallKind::FiberIsRunning => 1, - IntrinsicCallKind::FiberIsSuspended => 2, - IntrinsicCallKind::FiberIsTerminated => 3, - _ => unreachable!("fiber state intrinsic called with non-state kind"), - }; - abi::emit_load_int_immediate(emitter, arg1, expected_state); // pass the Fiber state value to compare against - abi::emit_call_label( - emitter, - intrinsic - .runtime_helper() - .expect("Fiber state intrinsic must have a runtime helper"), - ); // test the Fiber state through the shared runtime predicate - if matches!(intrinsic.kind(), IntrinsicCallKind::FiberIsStarted) { - match emitter.target.arch { - Arch::AArch64 => emitter.instruction("eor x0, x0, #1"), // invert: isStarted means state is not NotStarted - Arch::X86_64 => emitter.instruction("xor rax, 1"), // invert the boolean predicate result - } - } - PhpType::Bool -} - -/// Lowers Generator intrinsics (`Generator::current`, `key`, `next`, `valid`, `rewind`, `send`, `throw`, `getReturn`). -/// -/// Saves concat offsets before the call to preserve nested string operations, calls the Generator -/// runtime helper directly, then restores offsets after based on whether the result type is `Str`. -/// Returns the PHP return type for the intrinsic kind. -fn emit_generator_intrinsic( - intrinsic: IntrinsicCall, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let ret_ty = return_type_for(intrinsic); - save_concat_offset_before_nested_call(emitter, ctx); - abi::emit_call_label( - emitter, - intrinsic - .runtime_helper() - .expect("Generator intrinsic must have a runtime helper"), - ); // call directly into the Generator runtime helper - if ret_ty == PhpType::Str { - restore_concat_offset_after_owned_string_call(emitter, ctx); - } else { - restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } - ret_ty -} - -/// Emits assembly for callback filter accept intrinsic. -fn emit_callback_filter_accept_intrinsic( - intrinsic: IntrinsicCall, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let Some(class_info) = ctx.classes.get(intrinsic.class_name()) else { - emitter.comment("WARNING: missing CallbackFilterIterator metadata for callback accept"); - return PhpType::Bool; - }; - let callback_offset = class_info.property_offsets.get("callback").copied().unwrap_or(24); - let callback_env_offset = class_info - .property_offsets - .get("callbackEnv") - .copied() - .unwrap_or(40); - let direct_call = ctx.next_label("callback_filter_direct_call"); - let done = ctx.next_label("callback_filter_call_done"); - - save_concat_offset_before_nested_call(emitter, ctx); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr x9, [x0, #{}]", callback_offset)); // load the stored callback descriptor pointer - emitter.instruction(&format!("ldr x10, [x0, #{}]", callback_env_offset)); // load the optional persistent callback environment - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - "x9", - "x9", - ); - emitter.instruction("mov x0, x1"); // shift current value into callback argument 1 - emitter.instruction("mov x1, x2"); // shift current key into callback argument 2 - emitter.instruction("mov x2, x3"); // shift inner iterator into callback argument 3 - emitter.instruction(&format!("cbz x10, {}", direct_call)); // call the original callback directly when no env is stored - emitter.instruction("mov x3, x10"); // pass persistent capture env as the wrapper's hidden argument - emitter.instruction("blr x9"); // invoke the stored callback wrapper with captures - emitter.instruction(&format!("b {}", done)); // skip the direct-call path after wrapper dispatch - emitter.label(&direct_call); - emitter.instruction("blr x9"); // invoke the stored callback without hidden captures - emitter.label(&done); - } - Arch::X86_64 => { - emitter.instruction(&format!("mov r10, QWORD PTR [rdi + {}]", callback_offset)); // load the stored callback descriptor pointer - emitter.instruction(&format!("mov r11, QWORD PTR [rdi + {}]", callback_env_offset)); // load the optional persistent callback environment - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - "r10", - "r10", - ); - emitter.instruction("mov rdi, rsi"); // shift current value into callback argument 1 - emitter.instruction("mov rsi, rdx"); // shift current key into callback argument 2 - emitter.instruction("mov rdx, rcx"); // shift inner iterator into callback argument 3 - emitter.instruction("test r11, r11"); // check whether a persistent callback environment exists - emitter.instruction(&format!("je {}", direct_call)); // call the original callback directly when no env is stored - emitter.instruction("mov rcx, r11"); // pass persistent capture env as the wrapper's hidden argument - emitter.instruction("call r10"); // invoke the stored callback wrapper with captures - emitter.instruction(&format!("jmp {}", done)); // skip the direct-call path after wrapper dispatch - emitter.label(&direct_call); - emitter.instruction("call r10"); // invoke the stored callback without hidden captures - emitter.label(&done); - } - } - restore_concat_offset_after_nested_call(emitter, ctx, &PhpType::Bool); - PhpType::Bool -} - -/// Emits assembly for recursive assume iterator intrinsic. -fn emit_recursive_assume_iterator_intrinsic( - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - save_concat_offset_before_nested_call(emitter, ctx); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, x1"); // move the boxed candidate iterator into the mixed-unbox helper input - emitter.instruction("bl __rt_mixed_unbox"); // unwrap the candidate so the raw object pointer can be returned - emitter.instruction("mov x0, x1"); // return the unboxed object payload as RecursiveIterator - } - Arch::X86_64 => { - emitter.instruction("mov rax, rsi"); // move the boxed candidate iterator into the mixed-unbox helper input - emitter.instruction("call __rt_mixed_unbox"); // unwrap the candidate so the raw object pointer can be returned - emitter.instruction("mov rax, rdi"); // return the unboxed object payload as RecursiveIterator - } - } - let ret_ty = PhpType::Object("RecursiveIterator".to_string()); - restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - ret_ty -} diff --git a/src/codegen/expr/objects/dispatch/method.rs b/src/codegen/expr/objects/dispatch/method.rs deleted file mode 100644 index 97195d3b4c..0000000000 --- a/src/codegen/expr/objects/dispatch/method.rs +++ /dev/null @@ -1,398 +0,0 @@ -//! Purpose: -//! Lowers instance method target selection and invocation. -//! Shares receiver preparation and ABI call conventions with the object call dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::objects::dispatch` -//! -//! Key details: -//! - Receiver ownership, late/static binding, and vtable slot layout must match class metadata emission. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::functions; -use crate::intrinsics::IntrinsicCall; -use crate::names::php_symbol_key; -use crate::parser::ast::Expr; -use crate::types::{FunctionSig, PhpType}; - -use super::intrinsic::emit_instance_intrinsic_with_loaded_args; -use super::interface::emit_dispatch_interface_method; -use super::prep::{compute_register_assignments, eval_and_push_args, pop_args_to_registers}; -use super::super::super::emit_expr; -use super::vtable::emit_dispatch_instance_method; - -/// Lowers a method call where arguments are already pushed to the temporary stack. -pub(in crate::codegen::expr::objects) fn emit_method_call_with_pushed_args( - class_name: &str, - method: &str, - arg_types: &[PhpType], - source_temp_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let assignments = compute_register_assignments(emitter, arg_types, 1); - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 0)); // pop $this into the first integer argument register for the target ABI - let overflow_bytes = pop_args_to_registers(emitter, &assignments); - let ret_ty = if let Some(intrinsic) = IntrinsicCall::instance_method(class_name, method) { - emit_instance_intrinsic_with_loaded_args( - intrinsic, - &assignments, - overflow_bytes, - emitter, - ctx, - ) - } else if ctx.interfaces.contains_key(class_name) { - emit_dispatch_interface_method(class_name, method, emitter, ctx) - } else { - emit_dispatch_instance_method(class_name, method, emitter, ctx) - }; - abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop spilled stack arguments after the method call returns - abi::emit_release_temporary_stack(emitter, source_temp_bytes); // drop source-order named-argument temporaries after dispatch - ret_ty -} - -/// Lowers a method call where the receiver was saved below the pushed argument temporaries. -pub(in crate::codegen::expr::objects) fn emit_method_call_with_saved_receiver_below_args( - class_name: &str, - method: &str, - arg_types: &[PhpType], - source_temp_bytes: usize, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let arg_temp_bytes = pushed_arg_temp_bytes(arg_types) + source_temp_bytes; - abi::emit_load_temporary_stack_slot( - emitter, - abi::int_result_reg(emitter), - arg_temp_bytes, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // duplicate the saved receiver above the evaluated arguments for normal method dispatch - let ret_ty = emit_method_call_with_pushed_args( - class_name, - method, - arg_types, - source_temp_bytes, - emitter, - ctx, - ); - abi::emit_release_temporary_stack(emitter, 16); // discard the original receiver slot saved below the argument temporaries - ret_ty -} - -/// Evaluates and pushes method arguments, returning metadata for subsequent dispatch. -pub(in crate::codegen::expr::objects) fn emit_pushed_method_args( - args: &[Expr], - sig: Option<&crate::types::FunctionSig>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> super::super::super::calls::args::EmittedCallArgs { - eval_and_push_args(args, sig, emitter, ctx, data) -} - -/// Computes the total size in bytes occupied by argument temporaries on the temporary stack. -/// -/// Each argument occupies 16 bytes except `Void`-typed arguments which occupy 0 bytes. -/// Used to locate the saved receiver slot when preparing late-binding dispatch. -fn pushed_arg_temp_bytes(arg_types: &[PhpType]) -> usize { - arg_types - .iter() - .map(|ty| if matches!(ty, PhpType::Void) { 0 } else { 16 }) - .sum() -} - -/// Lowers a method call expression with receiver, method name, and arguments. -/// -/// Handles receiver evaluation order (object before arguments per PHP semantics), -/// nullable/object-union unboxing with fatal-on-null, `__call` magic fallback, -/// and `Fiber::start` special-cased signature. Emits receiver below argument -/// temporaries then delegates to `emit_method_call_with_saved_receiver_below_args`. -pub(in crate::codegen::expr::objects) fn emit_method_call( - object: &Expr, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("->{}()", method)); - - // Resolve the receiver's static class. Accepts a direct object type or - // a nullable object union (`?Foo`, `Foo|null`) — for those, the - // singular Object member's class is used and the runtime unbox below - // turns null receivers into a controlled fatal before dispatch. - let obj_ty = functions::infer_contextual_type(object, ctx); - let class_name = match functions::singular_object_class(&obj_ty) { - Some(cn) => cn.to_string(), - None => { - // No single static class. When the receiver could be an object at - // runtime (a `Mixed` value, or a union of object classes), dispatch - // on the runtime class id instead of giving up. - let method_key = php_symbol_key(method); - let candidates = - dynamic_dispatch_candidates(&obj_ty, &method_key, args.len(), ctx); - if !candidates.is_empty() { - return emit_dynamic_method_call( - object, method, args, &candidates, emitter, ctx, data, - ); - } - emitter.comment("WARNING: method call on non-object"); - return PhpType::Int; - } - }; - // Evaluate the receiver before arguments, matching PHP's left-to-right - // call order. When the receiver's codegen-level type is Mixed (the - // runtime representation for nullable / union object parameters), the - // result register holds a pointer to a boxed mixed cell rather than the - // raw object — unbox it so the downstream method dispatch receives the - // underlying object pointer. - let runtime_obj_ty = emit_expr(object, emitter, ctx, data); - if matches!(runtime_obj_ty, PhpType::Mixed | PhpType::Union(_)) { - let message = format!( - "Fatal error: Call to a member function {}() on null\n", - method - ); - super::super::emit_unbox_mixed_object_or_fatal( - message.as_bytes(), - emitter, - ctx, - data, - ); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the receiver below later argument temporaries for PHP evaluation order - - let method_key = php_symbol_key(method); - let mut dispatch_method = method_key.as_str(); - let mut magic_args = None; - let sig = if let Some(class_info) = ctx.classes.get(&class_name) { - if let Some(sig) = class_info.methods.get(&method_key) { - Some(sig.clone()) - } else if let Some(sig) = class_info.methods.get("__call") { - dispatch_method = "__call"; - magic_args = Some(super::super::magic_method_args(method, args, object.span)); - Some(sig.clone()) - } else { - None - } - } else { - ctx.interfaces - .get(&class_name) - .and_then(|interface_info| interface_info.methods.get(&method_key)) - .cloned() - }; - let args_to_emit = magic_args.as_deref().unwrap_or(args); - let fiber_start_sig = if class_name == "Fiber" && dispatch_method == "start" { - crate::codegen::fiber_sigs::fiber_start_sig_for_expr(object, ctx) - .or_else(|| Some(fiber_start_call_sig(args_to_emit.len()))) - } else { - None - }; - let emitted_args = eval_and_push_args( - args_to_emit, - fiber_start_sig.as_ref().or(sig.as_ref()), - emitter, - ctx, - data, - ); - - emit_method_call_with_saved_receiver_below_args( - &class_name, - dispatch_method, - &emitted_args.arg_types, - emitted_args.source_temp_bytes, - emitter, - ctx, - ) -} - -/// Returns whether `sig` can accept `arg_count` positional arguments: at least the number of -/// required parameters (those without a default) and at most the declared parameter count, unless -/// the signature is variadic (in which case any count at or above the required minimum is accepted). -fn sig_accepts_arg_count(sig: &FunctionSig, arg_count: usize) -> bool { - let required = (0..sig.params.len()) - .filter(|i| sig.defaults.get(*i).map_or(true, Option::is_none)) - .count(); - if arg_count < required { - return false; - } - sig.variadic.is_some() || arg_count <= sig.params.len() -} - -/// Collects the candidate classes for a dynamic method call whose receiver type -/// does not name a single class. -/// -/// For a `Mixed` receiver every class that defines `method_key` is a candidate; -/// for a union, the object members that define it are. Returns `(class_name, -/// class_id)` pairs sorted by class id (and de-duplicated) so the emitted dispatch -/// chain is deterministic. -/// -/// Candidates are also filtered to those whose method can accept `arg_count` -/// positional arguments. The dispatch marshals arguments once using the first -/// candidate's signature, so candidates with an incompatible arity would corrupt -/// the call — e.g. a user `add(int, int)` and `DateTime::add(DateInterval)` share -/// the name `add` but not the shape. Filtering by arity keeps the shared argument -/// layout valid and makes the candidate set independent of class-id ordering. -fn dynamic_dispatch_candidates( - obj_ty: &PhpType, - method_key: &str, - arg_count: usize, - ctx: &Context, -) -> Vec<(String, u64)> { - // A class is a usable candidate only when it declares the method, the method - // is a normal vtable method (not an intrinsic — SPL containers carry their own - // argument shapes and special lowering, so a single shared argument layout - // cannot serve them), and the method accepts `arg_count` arguments. Excluded - // classes leave a `Mixed` value holding such an object to fault cleanly as - // "undefined method" rather than miscompiling. - let dispatchable = |name: &str| -> bool { - ctx.classes.get(name).is_some_and(|info| { - info.methods - .get(method_key) - .is_some_and(|sig| sig_accepts_arg_count(sig, arg_count)) - }) && IntrinsicCall::instance_method(name, method_key).is_none() - }; - let mut out: Vec<(String, u64)> = Vec::new(); - match obj_ty { - PhpType::Mixed => { - for (name, info) in &ctx.classes { - if dispatchable(name) { - out.push((name.clone(), info.class_id)); - } - } - } - PhpType::Union(members) => { - for member in members { - if let PhpType::Object(name) = member { - if dispatchable(name) { - if let Some(info) = ctx.classes.get(name) { - out.push((name.clone(), info.class_id)); - } - } - } - } - } - _ => {} - } - out.sort_by_key(|(_, id)| *id); - out.dedup_by_key(|(_, id)| *id); - out -} - -/// Lowers a method call on a receiver whose static type does not name a single -/// class by dispatching on the receiver's runtime class id. -/// -/// The receiver is evaluated once and unboxed to an object pointer (fataling if -/// the runtime value is not an object). Arguments are laid out once using the -/// first candidate's signature. For each candidate class the runtime class id is -/// compared and, on a match, the call is lowered through the normal static -/// dispatch path for that class (only one branch runs, so the saved receiver and -/// argument temporaries are consumed exactly once). An unmatched class id fatals. -/// Returns the first candidate's return type; same-named methods are expected to -/// share a return representation. -fn emit_dynamic_method_call( - object: &Expr, - method: &str, - args: &[Expr], - candidates: &[(String, u64)], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("dynamic ->{}() dispatch on runtime class id", method)); - let method_key = php_symbol_key(method); - - let _ = emit_expr(object, emitter, ctx, data); - let on_non_object = format!( - "Fatal error: Call to a member function {}() on a non-object\n", - method - ); - super::super::emit_unbox_mixed_object_strict_or_fatal( - on_non_object.as_bytes(), - emitter, - ctx, - data, - ); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the object receiver below later argument temporaries - - let sig = ctx - .classes - .get(&candidates[0].0) - .and_then(|class_info| class_info.methods.get(&method_key)) - .cloned(); - let emitted_args = eval_and_push_args(args, sig.as_ref(), emitter, ctx, data); - let arg_types = emitted_args.arg_types; - let source_temp_bytes = emitted_args.source_temp_bytes; - - let arg_temp_bytes = pushed_arg_temp_bytes(&arg_types) + source_temp_bytes; - let recv_reg = abi::symbol_scratch_reg(emitter); - let class_id_reg = abi::secondary_scratch_reg(emitter); - let imm_reg = abi::tertiary_scratch_reg(emitter); - abi::emit_load_temporary_stack_slot(emitter, recv_reg, arg_temp_bytes); // peek the saved object receiver beneath the argument temporaries - abi::emit_load_from_address(emitter, class_id_reg, recv_reg, 0); // load the runtime class id from the object header - - let done = ctx.next_label("dyn_dispatch_done"); - let mut ret_ty = PhpType::Mixed; - for (index, (class_name, class_id)) in candidates.iter().enumerate() { - let next = ctx.next_label("dyn_dispatch_next"); - abi::emit_load_int_immediate(emitter, imm_reg, *class_id as i64); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, {}", class_id_reg, imm_reg)); // compare the runtime class id with this candidate class - emitter.instruction(&format!("b.ne {}", next)); // try the next candidate when the class id differs - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", class_id_reg, imm_reg)); // compare the runtime class id with this candidate class - emitter.instruction(&format!("jne {}", next)); // try the next candidate when the class id differs - } - } - let branch_ret = emit_method_call_with_saved_receiver_below_args( - class_name, - &method_key, - &arg_types, - source_temp_bytes, - emitter, - ctx, - ); - if index == 0 { - ret_ty = branch_ret; - } - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("b {}", done)); // the matched candidate handled the call - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("jmp {}", done)); // the matched candidate handled the call - } - } - emitter.label(&next); - } - let undefined = format!("Fatal error: Call to undefined method {}()\n", method); - super::super::emit_fatal_str(&undefined, emitter, data); - emitter.label(&done); - ret_ty -} - -/// Constructs a synthetic `FunctionSig` for `Fiber::start` calls where argument -/// count is determined at runtime. -/// -/// The PHP `Fiber::start` method accepts an arbitrary number of `Mixed`-typed -/// arguments and returns `Mixed`. This is distinct from the type-checked catalog -/// signature because the compiler emits call sites with runtime-discovered arity. -fn fiber_start_call_sig(arg_count: usize) -> FunctionSig { - FunctionSig { - params: (0..arg_count) - .map(|idx| (format!("arg{}", idx), PhpType::Mixed)) - .collect(), - defaults: vec![None; arg_count], - return_type: PhpType::Mixed, - declared_return: false, - by_ref_return: false, - ref_params: vec![false; arg_count], - declared_params: vec![false; arg_count], - variadic: None, - deprecation: None, - } -} diff --git a/src/codegen/expr/objects/dispatch/mod.rs b/src/codegen/expr/objects/dispatch/mod.rs deleted file mode 100644 index 1797f4f5b5..0000000000 --- a/src/codegen/expr/objects/dispatch/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Purpose: -//! Groups object dispatch helpers for methods, static calls, interfaces, enums, fibers, and vtables. -//! Keeps receiver preparation and call target selection isolated from object expression dispatch. -//! -//! Called from: -//! - `crate::codegen::expr::objects` -//! -//! Key details: -//! - Dispatch paths must share receiver ownership and ABI argument conventions with normal call lowering. - -mod enums; -mod intrinsic; -mod interface; -mod method; -mod prep; -mod static_call; -mod vtable; - -pub(crate) use interface::emit_dispatch_interface_method; -pub(crate) use vtable::emit_dispatch_instance_method; -pub(super) use method::{ - emit_method_call, emit_method_call_with_pushed_args, - emit_method_call_with_saved_receiver_below_args, emit_pushed_method_args, -}; -pub(super) use static_call::{ - emit_forwarded_called_class_id, emit_immediate_class_id, emit_static_method_call, -}; diff --git a/src/codegen/expr/objects/dispatch/prep.rs b/src/codegen/expr/objects/dispatch/prep.rs deleted file mode 100644 index dc6fc69f6a..0000000000 --- a/src/codegen/expr/objects/dispatch/prep.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Purpose: -//! Lowers receiver and argument preparation before object dispatch. -//! Shares receiver preparation and ABI call conventions with the object call dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::objects::dispatch` -//! -//! Key details: -//! - Receiver ownership, late/static binding, and vtable slot layout must match class metadata emission. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::names::method_symbol; -use crate::parser::ast::{Expr, Visibility}; -use crate::types::{FunctionSig, PhpType}; - -/// Evaluates arguments left-to-right and pushes them to the temporary stack in source order. -/// Returns emitted call args describing how arguments were placed for the call dispatcher. -pub(super) fn eval_and_push_args( - args: &[Expr], - sig: Option<&FunctionSig>, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> super::super::super::calls::args::EmittedCallArgs { - super::super::super::calls::args::emit_pushed_call_args( - args, - sig, - super::super::super::calls::args::regular_param_count(sig, args.len()), - "method ref arg", - true, - true, - emitter, - ctx, - data, - ) -} - -/// Computes register assignments for outgoing call arguments on the target ABI. -/// Takes the target ABI, argument types, and the first integer register number to use. -pub(super) fn compute_register_assignments( - emitter: &Emitter, - arg_types: &[PhpType], - first_int_reg: usize, -) -> Vec { - abi::build_outgoing_arg_assignments_for_target(emitter.target, arg_types, first_int_reg) -} - -/// Pops arguments from the temporary stack into registers according to the ABI layout. -/// Returns the number of stack bytes consumed by the argument materialization. -pub(super) fn pop_args_to_registers( - emitter: &mut Emitter, - assignments: &[abi::OutgoingArgAssignment], -) -> usize { - abi::materialize_outgoing_args(emitter, assignments) -} - -/// Resolves return type, vtable slot, and private-method label for an instance method dispatch. -pub(super) fn resolve_instance_method_dispatch( - ctx: &Context, - class_name: &str, - method: &str, -) -> (PhpType, Option, Option) { - let class_info = ctx.classes.get(class_name).cloned(); - let ret_ty = class_info - .as_ref() - .and_then(|ci| { - let impl_class = ci - .method_impl_classes - .get(method) - .map(String::as_str) - .unwrap_or(class_name); - ctx.classes - .get(impl_class) - .and_then(|impl_info| impl_info.methods.get(method)) - .cloned() - }) - .map(|sig| sig.return_type) - .unwrap_or(PhpType::Int); - let slot = class_info - .as_ref() - .and_then(|ci| ci.vtable_slots.get(method).copied()); - let direct_private_label = class_info.as_ref().and_then(|ci| { - if ci.method_visibilities.get(method) == Some(&Visibility::Private) { - let impl_class = ci - .method_impl_classes - .get(method) - .map(String::as_str) - .unwrap_or(class_name); - Some(method_symbol(impl_class, method)) - } else { - None - } - }); - (ret_ty, slot, direct_private_label) -} diff --git a/src/codegen/expr/objects/dispatch/static_call.rs b/src/codegen/expr/objects/dispatch/static_call.rs deleted file mode 100644 index 7287f9740e..0000000000 --- a/src/codegen/expr/objects/dispatch/static_call.rs +++ /dev/null @@ -1,293 +0,0 @@ -//! Purpose: -//! Lowers static method call target selection and invocation. -//! Shares receiver preparation and ABI call conventions with the object call dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::objects::dispatch` -//! -//! Key details: -//! - Receiver ownership, late/static binding, and vtable slot layout must match class metadata emission. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::intrinsics::IntrinsicCall; -use crate::names::{method_symbol, static_method_symbol}; -use crate::parser::ast::{Expr, StaticReceiver}; -use crate::types::PhpType; - -use super::enums::emit_enum_static_method_call; -use super::intrinsic::emit_static_intrinsic_call; -use super::prep::{compute_register_assignments, eval_and_push_args, pop_args_to_registers}; -use super::super::super::{ - restore_concat_offset_after_nested_call, restore_concat_offset_after_owned_string_call, - save_concat_offset_before_nested_call, -}; - -/// Emits a compile-time class ID as an immediate integer into the ABI integer result register. -/// Used for direct static dispatch where the class is known at compile time. -/// Loads `class_id as i64` into `abi::int_result_reg(emitter)`. -pub(in crate::codegen::expr::objects) fn emit_immediate_class_id(emitter: &mut Emitter, class_id: u64) { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), class_id as i64); -} - -/// Loads the called-class ID for late static binding into the ABI integer result register. -/// -/// Checks, in order: `__elephc_fcc_called_class_id` (first-class callable capture), -/// `__elephc_called_class_id` (static method frame), `__elephc_fcc_this` (FCC receiver), -/// then `this` (implicit receiver). Dereferences pointers to get the class ID. -/// Returns `false` if no called-class context is available in the current frame. -pub(in crate::codegen::expr::objects) fn emit_forwarded_called_class_id(emitter: &mut Emitter, ctx: &Context) -> bool { - if let Some(var) = ctx.variables.get("__elephc_fcc_called_class_id") { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), var.stack_offset); // forward the first-class callable's captured called-class id - return true; - } - if let Some(var) = ctx.variables.get("__elephc_called_class_id") { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), var.stack_offset); // forward the hidden called-class id from the current static method frame - true - } else if let Some(var) = ctx.variables.get("__elephc_fcc_this") { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), var.stack_offset); // load the first-class callable's captured receiver for dynamic static dispatch - abi::emit_load_from_address( - emitter, - abi::int_result_reg(emitter), - abi::int_result_reg(emitter), - 0, - ); - true - } else if let Some(var) = ctx.variables.get("this") { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), var.stack_offset); // load the implicit $this pointer for dynamic static dispatch - abi::emit_load_from_address( - emitter, - abi::int_result_reg(emitter), - abi::int_result_reg(emitter), - 0, - ); - true - } else { - false - } -} - -/// Lowers `ClassName::method(...)`, `self::method(...)`, `parent::method(...)`, -/// and `static::method(...)` static calls. -/// -/// Dispatches through the static vtable when `static::` has a vtable slot (dynamic static dispatch), -/// falls back to direct private static helpers, or calls the resolved method label. -/// Pushes hidden `called_class` ID and implicit `$this` receiver as ABI registers when required, -/// evaluates and materializes arguments in source order, then restores concat offsets and releases -/// temporary stack space after the call returns. -pub(in crate::codegen::expr::objects) fn emit_static_method_call( - receiver: &StaticReceiver, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let parent_call = matches!(receiver, StaticReceiver::Parent); - let self_call = matches!(receiver, StaticReceiver::Self_); - let static_call = matches!(receiver, StaticReceiver::Static); - let forwarded_call = matches!( - receiver, - StaticReceiver::Parent | StaticReceiver::Self_ | StaticReceiver::Static - ); - let class_name = match receiver { - StaticReceiver::Named(class_name) => class_name.as_str().to_string(), - StaticReceiver::Self_ | StaticReceiver::Static => match &ctx.current_class { - Some(class_name) => class_name.clone(), - None => { - emitter.comment("WARNING: self::/static:: used outside class scope"); - return PhpType::Int; - } - }, - StaticReceiver::Parent => { - let current_class = match &ctx.current_class { - Some(class_name) => class_name.clone(), - None => { - emitter.comment("WARNING: parent:: used outside class scope"); - return PhpType::Int; - } - }; - match ctx.classes.get(¤t_class).and_then(|info| info.parent.clone()) { - Some(parent_name) => parent_name, - None => { - emitter.comment(&format!("WARNING: class {} has no parent", current_class)); - return PhpType::Int; - } - } - } - }; - if ctx.enums.contains_key(&class_name) { - return emit_enum_static_method_call(&class_name, method, args, emitter, ctx, data); - } - if let Some(intrinsic) = IntrinsicCall::static_method(&class_name, method) { - return emit_static_intrinsic_call(intrinsic, args, emitter, ctx, data); - } - emitter.comment(&format!("{}::{}()", class_name, method)); - - let class_info = match ctx.classes.get(&class_name).cloned() { - Some(class_info) => class_info, - None => { - emitter.comment(&format!("WARNING: undefined class {}", class_name)); - return PhpType::Int; - } - }; - let sig = if class_info.static_methods.contains_key(method) { - class_info.static_methods.get(method) - } else if parent_call || self_call { - class_info.methods.get(method) - } else { - None - } - .cloned(); - let emitted_args = eval_and_push_args(args, sig.as_ref(), emitter, ctx, data); - let static_slot = class_info.static_vtable_slots.get(method).copied(); - let direct_static_private_label = if static_call { - None - } else if class_info.static_methods.contains_key(method) && static_slot.is_none() { - let impl_class = class_info - .static_method_impl_classes - .get(method) - .map(String::as_str) - .unwrap_or(class_name.as_str()); - Some(static_method_symbol(impl_class, method)) - } else { - None - }; - - let (ret_ty, label, needs_this, needs_called_class_id, dynamic_static_dispatch) = - if class_info.static_methods.contains_key(method) { - let impl_class = class_info - .static_method_impl_classes - .get(method) - .map(String::as_str) - .unwrap_or(class_name.as_str()); - ( - ctx.classes - .get(impl_class) - .and_then(|impl_info| impl_info.static_methods.get(method)) - .map(|sig| sig.return_type.clone()) - .unwrap_or(PhpType::Int), - static_method_symbol(impl_class, method), - false, - true, - static_call && static_slot.is_some(), - ) - } else if static_call { - emitter.comment(&format!( - "WARNING: undefined static method {}::{}", - class_name, method - )); - return PhpType::Int; - } else if parent_call || self_call { - let _sig = match class_info.methods.get(method) { - Some(sig) => sig, - None => { - emitter.comment(&format!( - "WARNING: undefined direct instance method {}::{}", - class_name, method - )); - return PhpType::Int; - } - }; - let impl_class = class_info - .method_impl_classes - .get(method) - .map(String::as_str) - .unwrap_or(class_name.as_str()); - ( - ctx.classes - .get(impl_class) - .and_then(|impl_info| impl_info.methods.get(method)) - .map(|sig| sig.return_type.clone()) - .unwrap_or(PhpType::Int), - method_symbol(impl_class, method), - true, - false, - false, - ) - } else { - emitter.comment(&format!( - "WARNING: cannot call instance method statically {}::{}", - class_name, method - )); - return PhpType::Int; - }; - - let first_int_reg = - (if needs_called_class_id { 1 } else { 0 }) + (if needs_this { 1 } else { 0 }); - let assignments = compute_register_assignments(emitter, &emitted_args.arg_types, first_int_reg); - let hidden_called_class_reg = abi::int_arg_reg_name(emitter.target, 0); - let hidden_this_reg = - abi::int_arg_reg_name(emitter.target, if needs_called_class_id { 1 } else { 0 }); - let class_id_scratch = abi::temp_int_reg(emitter.target); - let dispatch_scratch = abi::symbol_scratch_reg(emitter); - - if needs_called_class_id { - if forwarded_call { - if !emit_forwarded_called_class_id(emitter, ctx) { - emitter.comment("WARNING: missing forwarded called class id"); - return PhpType::Int; - } - } else if let Some(target_info) = ctx.classes.get(&class_name) { - emit_immediate_class_id(emitter, target_info.class_id); - } else { - emitter.comment(&format!("WARNING: undefined class {}", class_name)); - return PhpType::Int; - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // push the hidden called-class id before loading the visible arguments - } - - if needs_this { - let this_var = match ctx.variables.get("this") { - Some(var) => var, - None => { - emitter.comment("WARNING: direct scoped instance call without $this"); - return PhpType::Int; - } - }; - abi::load_at_offset(emitter, abi::int_result_reg(emitter), this_var.stack_offset); // load the implicit scoped-call receiver into the integer result register - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // push the implicit receiver before visible argument materialization - } - - if needs_called_class_id { - abi::emit_pop_reg(emitter, hidden_called_class_reg); // pop the hidden called-class id into its outgoing ABI register - } - if needs_this { - abi::emit_pop_reg(emitter, hidden_this_reg); // pop the implicit receiver into its outgoing ABI register - } - let overflow_bytes = pop_args_to_registers(emitter, &assignments); - - save_concat_offset_before_nested_call(emitter, ctx); - if dynamic_static_dispatch { - let slot = static_slot.expect("codegen bug: dynamic static dispatch without slot"); - emitter.instruction(&format!("mov {}, {}", class_id_scratch, hidden_called_class_reg)); // preserve the forwarded called-class id across static-vtable address materialization - abi::emit_symbol_address(emitter, dispatch_scratch, "_class_static_vtable_ptrs"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("ldr {}, [{}, {}, lsl #3]", dispatch_scratch, dispatch_scratch, class_id_scratch)); // load the class-specific static-vtable pointer from the global table - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, QWORD PTR [{} + {} * 8]", dispatch_scratch, dispatch_scratch, class_id_scratch)); // load the class-specific static-vtable pointer from the global table - } - } - abi::emit_load_from_address(emitter, dispatch_scratch, dispatch_scratch, slot * 8); // load the selected static method entry from the class-specific vtable - abi::emit_call_reg(emitter, dispatch_scratch); // call the late-bound static method implementation - } else if let Some(label) = direct_static_private_label { - abi::emit_call_label(emitter, &label); // call the direct private static helper - } else { - abi::emit_call_label(emitter, &label); // call the resolved static or parent/self method target - } - if ret_ty == PhpType::Str { - restore_concat_offset_after_owned_string_call(emitter, ctx); - } else { - restore_concat_offset_after_nested_call(emitter, ctx, &ret_ty); - } - if overflow_bytes > 0 { - abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop spilled stack arguments after the static call returns - } - abi::emit_release_temporary_stack(emitter, emitted_args.source_temp_bytes); // drop source-order named-argument temporaries after the static call - - ret_ty -} diff --git a/src/codegen/expr/objects/dispatch/vtable.rs b/src/codegen/expr/objects/dispatch/vtable.rs deleted file mode 100644 index 76c406a584..0000000000 --- a/src/codegen/expr/objects/dispatch/vtable.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Purpose: -//! Lowers vtable lookup and class/interface slot calculations. -//! Shares receiver preparation and ABI call conventions with the object call dispatcher. -//! -//! Called from: -//! - `crate::codegen::expr::objects::dispatch` -//! -//! Key details: -//! - Receiver ownership, late/static binding, and vtable slot layout must match class metadata emission. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::emit::Emitter; -use crate::intrinsics::IntrinsicCall; -use crate::types::PhpType; - -use super::super::super::{ - restore_concat_offset_after_nested_call, restore_concat_offset_after_owned_string_call, - save_concat_offset_before_nested_call, -}; -use super::intrinsic::emit_instance_intrinsic_with_loaded_args; -use super::prep::resolve_instance_method_dispatch; - -/// Lowers vtable-based instance method dispatch for a known class and method. -/// -/// Resolves the method to a vtable slot or direct private-label call, then emits -/// code to load the class ID from the receiver object header, index into the -/// class's instance-vtable, and call the resolved implementation. Intrinsic -/// methods are handled directly without a vtable lookup. -/// -/// # Arguments -/// * `class_name` - The fully-qualified class name for vtable resolution -/// * `method` - The method name to dispatch -/// * `emitter` - The assembly emitter -/// * `ctx` - The codegen context (contains class metadata, vtable layout) -/// -/// # Returns -/// The `PhpType` of the method's return value, used to guide subsequent codegen -/// (e.g., concat offset restoration for string returns). -/// -/// # Side effects -/// - Saves concat offset before the call if the method may mutate string operands. -/// - Restores concat offset after the call based on the return type. -/// - Uses scratch registers for class ID and dispatch address; caller-saved -/// registers are clobbered by the indirect call. -pub(crate) fn emit_dispatch_instance_method( - class_name: &str, - method: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - if let Some(intrinsic) = IntrinsicCall::instance_method(class_name, method) { - return emit_instance_intrinsic_with_loaded_args(intrinsic, &[], 0, emitter, ctx); - } - - let (ret_ty, slot, direct_private_label) = - resolve_instance_method_dispatch(ctx, class_name, method); - - save_concat_offset_before_nested_call(emitter, ctx); - if let Some(slot) = slot { - let class_id_reg = abi::temp_int_reg(emitter.target); - let dispatch_reg = abi::symbol_scratch_reg(emitter); - abi::emit_load_from_address( - emitter, - class_id_reg, - abi::int_arg_reg_name(emitter.target, 0), - 0, - ); // load the dynamic class id from the receiver object header - abi::emit_symbol_address(emitter, dispatch_reg, "_class_vtable_ptrs"); - match emitter.target.arch { - crate::codegen::platform::Arch::AArch64 => { - emitter.instruction(&format!("ldr {}, [{}, {}, lsl #3]", dispatch_reg, dispatch_reg, class_id_reg)); // load the class-specific instance-vtable pointer from the global table - } - crate::codegen::platform::Arch::X86_64 => { - emitter.instruction(&format!("mov {}, QWORD PTR [{} + {} * 8]", dispatch_reg, dispatch_reg, class_id_reg)); // load the class-specific instance-vtable pointer from the global table - } - } - abi::emit_load_from_address(emitter, dispatch_reg, dispatch_reg, slot * 8); // load the selected method entry from the class-specific instance vtable - abi::emit_call_reg(emitter, dispatch_reg); // call the resolved virtual method implementation - } else if let Some(label) = direct_private_label { - abi::emit_call_label(emitter, &label); // call lexically-resolved private method directly - } else { - emitter.comment(&format!( - "WARNING: missing vtable slot for {}::{}", - class_name, method - )); - } - restore_concat_offset_after_user_call(emitter, ctx, &ret_ty); - - ret_ty -} - -/// Restores the concat offset after a user-method call, based on the return type. -/// -/// If the method returns a `Str`, the concat offset is restored for an owned string -/// call (which may use the same scratch slot as the return value). Otherwise, -/// restores via the generic nested-call path. -fn restore_concat_offset_after_user_call(emitter: &mut Emitter, ctx: &Context, ret_ty: &PhpType) { - if ret_ty == &PhpType::Str { - restore_concat_offset_after_owned_string_call(emitter, ctx); - } else { - restore_concat_offset_after_nested_call(emitter, ctx, ret_ty); - } -} diff --git a/src/codegen/expr/objects/fiber_callable.rs b/src/codegen/expr/objects/fiber_callable.rs deleted file mode 100644 index 3e471a8e79..0000000000 --- a/src/codegen/expr/objects/fiber_callable.rs +++ /dev/null @@ -1,719 +0,0 @@ -//! Purpose: -//! Materializes PHP callable shapes passed to `new Fiber(...)` as runtime callable descriptors. -//! Keeps Fiber constructor lowering focused on object allocation while callable selection stays here. -//! -//! Called from: -//! - `crate::codegen::expr::objects::allocation` -//! -//! Key details: -//! - The Fiber object stores one callable descriptor pointer and a generated wrapper pointer. -//! - Raw string callbacks use runtime descriptor-name dispatch; receiver-bound shapes are -//! converted to first-class-callable descriptors so receiver environments live in captures. - -use crate::codegen::callable_descriptor::{ - self, CallableDescriptorInvocation, CallableDescriptorShape, -}; -use crate::codegen::callable_dispatch::{RuntimeCallableCase, RuntimeCallableSelector}; -use crate::codegen::context::{Context, DeferredClosure, HeapOwnership}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::codegen::{abi, callable_dispatch}; -use crate::names::{php_symbol_key, Name}; -use crate::parser::ast::{CallableTarget, Expr, ExprKind, StaticReceiver, Stmt, StmtKind}; -use crate::span::Span; -use crate::types::{callable_wrapper_sig, FunctionSig, PhpType}; - -const FIBER_RECEIVER_CAPTURE_PARAM: &str = "__elephc_fiber_callable_receiver"; - -/// Emits a Fiber callback descriptor and returns the wrapper label that can invoke it. -/// -/// Existing descriptor-valued expressions delegate to the ordinary Fiber wrapper planner. -/// Raw string callbacks and callable arrays are materialized into descriptor pointers first, -/// then use the generic descriptor-invoker Fiber wrapper. -pub(super) fn emit_fiber_callable_descriptor( - callable_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Option { - if emit_callable_array_descriptor(callable_expr, emitter, ctx, data) - || emit_invokable_object_descriptor(callable_expr, emitter, ctx, data) - || emit_string_callable_descriptor(callable_expr, emitter, ctx, data) - { - return Some(super::fiber_wrapper::prepare_descriptor_invoker_wrapper(ctx)); - } - - crate::codegen::expr::emit_expr(callable_expr, emitter, ctx, data); - super::fiber_wrapper::prepare_fiber_wrapper(callable_expr, ctx) -} - -/// Emits a first-class-callable descriptor for a callable-array Fiber callback. -fn emit_callable_array_descriptor( - callable_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - if let Some(target) = callable_array_literal_target(callable_expr, ctx) { - return emit_callable_array_target_descriptor( - target, - callable_expr, - emitter, - ctx, - data, - ); - } - - if let ExprKind::Variable(var_name) = &callable_expr.kind { - if let Some(target) = ctx.callable_array_targets.get(var_name).cloned() { - return match target { - CallableTarget::StaticMethod { .. } => { - emit_synthetic_first_class_callable(target, callable_expr, emitter, ctx, data) - } - CallableTarget::Method { object, method } => { - emit_stored_instance_callable_array_descriptor( - var_name, - &object, - &method, - emitter, - ctx, - data, - ) - } - CallableTarget::Function(_) => false, - }; - } - } - - emit_runtime_callable_array_descriptor(callable_expr, emitter, ctx, data) -} - -/// Emits a first-class-callable descriptor for an object with public `__invoke()`. -fn emit_invokable_object_descriptor( - callable_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - let callable_ty = crate::codegen::functions::infer_contextual_type(callable_expr, ctx); - let Some(class_name) = crate::codegen::functions::singular_object_class(&callable_ty) else { - return false; - }; - if !ctx - .classes - .get(class_name) - .is_some_and(|class_info| class_info.methods.contains_key("__invoke")) - { - return false; - } - - emit_instance_receiver_expr_descriptor( - callable_expr, - "__invoke", - callable_expr.span, - emitter, - ctx, - data, - ) -} - -/// Emits runtime string-name descriptor selection for a Fiber callback. -fn emit_string_callable_descriptor( - callable_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - let callable_ty = crate::codegen::functions::infer_contextual_type(callable_expr, ctx); - if !matches!(callable_ty.codegen_repr(), PhpType::Str) { - return false; - } - - crate::codegen::expr::emit_expr(callable_expr, emitter, ctx, data); - emit_select_loaded_string_descriptor(emitter, ctx, data); - true -} - -/// Emits a synthetic first-class callable expression and leaves its descriptor in the result register. -fn emit_synthetic_first_class_callable( - target: CallableTarget, - source_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - let fcc_expr = Expr::new(ExprKind::FirstClassCallable(target), source_expr.span); - crate::codegen::expr::emit_expr(&fcc_expr, emitter, ctx, data); - true -} - -/// Emits a callable-array target as a descriptor suitable for Fiber storage. -fn emit_callable_array_target_descriptor( - target: CallableTarget, - source_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - match target { - CallableTarget::StaticMethod { .. } => { - emit_synthetic_first_class_callable(target, source_expr, emitter, ctx, data) - } - CallableTarget::Method { object, method } => emit_instance_receiver_expr_descriptor( - &object, - &method, - source_expr.span, - emitter, - ctx, - data, - ), - CallableTarget::Function(_) => false, - } -} - -/// Emits a descriptor whose receiver capture comes from slot zero of a stored callable array. -#[allow(clippy::too_many_arguments)] -fn emit_stored_instance_callable_array_descriptor( - var_name: &str, - object: &Expr, - method: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - let receiver_ty = crate::codegen::functions::infer_contextual_type(object, ctx); - let Some(class_name) = - crate::codegen::functions::singular_object_class(&receiver_ty).map(str::to_string) - else { - return false; - }; - let Some((resolved_method, sig)) = callable_array_method_wrapper_sig(ctx, &class_name, method) - else { - return false; - }; - - let capture_ty = PhpType::Object(class_name.clone()); - let descriptor_label = receiver_bound_instance_method_descriptor( - class_name, - resolved_method, - &sig, - data, - ctx, - ); - - emit_runtime_descriptor_with_callable_array_receiver( - var_name, - object.span, - &descriptor_label, - &capture_ty, - emitter, - ctx, - data, - ); - true -} - -/// Emits a receiver-bound instance method descriptor by evaluating the receiver expression once. -fn emit_instance_receiver_expr_descriptor( - receiver: &Expr, - method: &str, - span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - let receiver_ty = crate::codegen::functions::infer_contextual_type(receiver, ctx); - let Some(class_name) = - crate::codegen::functions::singular_object_class(&receiver_ty).map(str::to_string) - else { - return false; - }; - let Some((resolved_method, sig)) = callable_array_method_wrapper_sig(ctx, &class_name, method) - else { - return false; - }; - - let capture_ty = PhpType::Object(class_name.clone()); - let descriptor_label = receiver_bound_instance_method_descriptor( - class_name, - resolved_method, - &sig, - data, - ctx, - ); - emit_runtime_descriptor_with_receiver_expr( - receiver, - span, - &descriptor_label, - &capture_ty, - emitter, - ctx, - data, - ); - true -} - -/// Resolves the visible wrapper signature for an instance-method callable array. -fn callable_array_method_wrapper_sig( - ctx: &Context, - class_name: &str, - method: &str, -) -> Option<(String, FunctionSig)> { - let class_info = ctx.classes.get(class_name)?; - let method_key = php_symbol_key(method); - let (resolved_method, method_sig) = class_info - .methods - .iter() - .find(|(candidate, _)| php_symbol_key(candidate) == method_key)?; - Some((resolved_method.clone(), callable_wrapper_sig(method_sig))) -} - -/// Registers a receiver-bound wrapper and returns its static descriptor label. -fn receiver_bound_instance_method_descriptor( - class_name: String, - resolved_method: String, - sig: &FunctionSig, - data: &mut DataSection, - ctx: &mut Context, -) -> String { - let hidden_name = unique_hidden_param(FIBER_RECEIVER_CAPTURE_PARAM, sig); - let capture_ty = PhpType::Object(class_name.clone()); - let captures = vec![(hidden_name.clone(), capture_ty.clone(), false)]; - let hidden_params = vec![(hidden_name.clone(), capture_ty, false)]; - let wrapper_label = ctx.next_label("fiber_callable_array_method"); - let param_names: Vec = sig.params.iter().map(|(name, _)| name.clone()).collect(); - ctx.deferred_closures.push(DeferredClosure { - label: wrapper_label.clone(), - params: param_names, - body: callable_array_method_wrapper_body(&hidden_name, &resolved_method, sig), - sig: sig.clone(), - captures: captures.clone(), - hidden_params: hidden_params.clone(), - current_class: Some(class_name.clone()), - needed: true, - }); - - let invoker_label = - callable_dispatch::ensure_runtime_descriptor_invoker(ctx, &hidden_params, sig); - callable_descriptor::static_descriptor_with_optional_invoker_meta( - data, - &wrapper_label, - None, - callable_descriptor::CALLABLE_DESC_KIND_FIRST_CLASS, - Some(sig), - &captures, - &hidden_params, - CallableDescriptorInvocation::method( - CallableDescriptorShape::InstanceMethod, - Some(class_name), - resolved_method, - ), - invoker_label.as_deref(), - ) -} - -/// Builds the synthetic wrapper body for a Fiber callable-array instance method. -fn callable_array_method_wrapper_body( - receiver_param: &str, - method: &str, - sig: &FunctionSig, -) -> Vec { - let last_param_idx = sig.params.len().saturating_sub(1); - let args: Vec = sig - .params - .iter() - .enumerate() - .map(|(idx, (name, _))| { - let var_expr = Expr::new(ExprKind::Variable(name.clone()), Span::dummy()); - if sig.variadic.is_some() && idx == last_param_idx { - Expr::new(ExprKind::Spread(Box::new(var_expr)), Span::dummy()) - } else { - var_expr - } - }) - .collect(); - let call_expr = Expr::new( - ExprKind::MethodCall { - object: Box::new(Expr::new( - ExprKind::Variable(receiver_param.to_string()), - Span::dummy(), - )), - method: method.to_string(), - args, - }, - Span::dummy(), - ); - - if sig.return_type == PhpType::Void { - vec![ - Stmt::new(StmtKind::ExprStmt(call_expr), Span::dummy()), - Stmt::new(StmtKind::Return(None), Span::dummy()), - ] - } else { - vec![Stmt::new(StmtKind::Return(Some(call_expr)), Span::dummy())] - } -} - -/// Builds a runtime descriptor and stores the current callable-array receiver as capture slot zero. -#[allow(clippy::too_many_arguments)] -fn emit_runtime_descriptor_with_callable_array_receiver( - var_name: &str, - span: Span, - descriptor_label: &str, - receiver_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let descriptor_reg = abi::nested_call_reg(emitter); - let total_bytes = callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + 16; - - emitter.comment("fiber callable-array descriptor capture"); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), total_bytes as i64); - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!("mov {}, {}", descriptor_reg, abi::int_result_reg(emitter))); // keep the Fiber descriptor pointer while copying its static header - callable_descriptor::emit_copy_static_descriptor_to_runtime( - emitter, - descriptor_reg, - descriptor_label, - ); - abi::emit_push_reg(emitter, descriptor_reg); // preserve the runtime descriptor while the receiver slot is loaded - - let receiver = callable_array_slot_expr(var_name, 0, span); - let emitted_ty = crate::codegen::expr::emit_expr(&receiver, emitter, ctx, data); - if matches!(emitted_ty.codegen_repr(), PhpType::Mixed) { - crate::codegen::expr::objects::emit_unbox_mixed_object_or_fatal( - b"Fatal error: Fiber callable array receiver is not an object\n", - emitter, - ctx, - data, - ); - } - if receiver_ty.is_refcounted() - && crate::codegen::expr::expr_result_heap_ownership(&receiver) != HeapOwnership::Owned - { - abi::emit_incref_if_refcounted(emitter, receiver_ty); - } - abi::emit_pop_reg(emitter, descriptor_reg); // restore the runtime descriptor after receiver capture loading - callable_descriptor::emit_store_current_result_to_runtime_capture( - emitter, - descriptor_reg, - 0, - receiver_ty, - ); - if descriptor_reg != abi::int_result_reg(emitter) { - emitter.instruction(&format!("mov {}, {}", abi::int_result_reg(emitter), descriptor_reg)); // return the receiver-bound Fiber callable descriptor - } -} - -/// Evaluates a receiver expression and stores it as runtime descriptor capture slot zero. -#[allow(clippy::too_many_arguments)] -fn emit_runtime_descriptor_with_receiver_expr( - receiver: &Expr, - span: Span, - descriptor_label: &str, - receiver_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let emitted_ty = crate::codegen::expr::emit_expr(receiver, emitter, ctx, data); - if matches!(emitted_ty.codegen_repr(), PhpType::Mixed) { - crate::codegen::expr::objects::emit_unbox_mixed_object_or_fatal( - b"Fatal error: Fiber callable receiver is not an object\n", - emitter, - ctx, - data, - ); - } - if receiver_ty.is_refcounted() - && crate::codegen::expr::expr_result_heap_ownership(receiver) != HeapOwnership::Owned - { - abi::emit_incref_if_refcounted(emitter, receiver_ty); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the evaluated Fiber callable receiver while allocating its descriptor - emit_runtime_descriptor_with_saved_receiver(span, descriptor_label, receiver_ty, emitter, data); -} - -/// Stores the receiver currently saved on the temporary stack in descriptor capture slot zero. -fn emit_runtime_descriptor_with_saved_receiver( - _span: Span, - descriptor_label: &str, - receiver_ty: &PhpType, - emitter: &mut Emitter, - _data: &mut DataSection, -) { - let descriptor_reg = abi::nested_call_reg(emitter); - let total_bytes = callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + 16; - - emitter.comment("fiber receiver-bound descriptor capture"); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), total_bytes as i64); - abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!("mov {}, {}", descriptor_reg, abi::int_result_reg(emitter))); // keep the Fiber descriptor pointer while copying its static header - callable_descriptor::emit_copy_static_descriptor_to_runtime( - emitter, - descriptor_reg, - descriptor_label, - ); - abi::emit_push_reg(emitter, descriptor_reg); // preserve the runtime descriptor while the receiver capture is restored - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16); - callable_descriptor::emit_store_current_result_to_runtime_capture( - emitter, - descriptor_reg, - 0, - receiver_ty, - ); - abi::emit_pop_reg(emitter, descriptor_reg); // restore the runtime descriptor after receiver capture storage - abi::emit_release_temporary_stack(emitter, 16); // discard the saved receiver after it has been copied into the descriptor - if descriptor_reg != abi::int_result_reg(emitter) { - emitter.instruction(&format!("mov {}, {}", abi::int_result_reg(emitter), descriptor_reg)); // return the receiver-bound Fiber callable descriptor - } -} - -/// Emits descriptor selection for runtime callable-array Fiber callbacks. -fn emit_runtime_callable_array_descriptor( - callable_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> bool { - if crate::codegen::builtins::arrays::runtime_callable_array_callback::emit_without_saved_array( - callable_expr, - emitter, - ctx, - data, - |case, receiver_ty, emitter, ctx, data| { - emit_selected_runtime_callable_array_descriptor( - case, - receiver_ty, - callable_expr.span, - emitter, - ctx, - data, - ); - }, - ) { - return true; - } - - crate::codegen::builtins::arrays::runtime_callable_array_callback::emit_literal_without_saved_array( - callable_expr, - emitter, - ctx, - data, - |case, receiver_ty, emitter, ctx, data| { - emit_selected_runtime_callable_array_descriptor( - case, - receiver_ty, - callable_expr.span, - emitter, - ctx, - data, - ); - }, - ) -} - -/// Materializes the descriptor selected from a runtime callable array. -fn emit_selected_runtime_callable_array_descriptor( - case: &RuntimeCallableCase, - receiver_ty: Option<&PhpType>, - span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let Some(receiver_ty) = receiver_ty else { - abi::emit_symbol_address(emitter, abi::int_result_reg(emitter), &case.descriptor_label); - return; - }; - - let Some((class_name, method_name)) = runtime_case_method_target(case) else { - emit_fiber_callable_no_match_abort(emitter, data); - return; - }; - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // recover the selected runtime callable-array receiver - if receiver_ty.is_refcounted() { - abi::emit_incref_if_refcounted(emitter, receiver_ty); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the retained receiver while building its descriptor - let Some((resolved_method, sig)) = callable_array_method_wrapper_sig(ctx, &class_name, &method_name) - else { - emit_fiber_callable_no_match_abort(emitter, data); - return; - }; - let descriptor_label = receiver_bound_instance_method_descriptor( - class_name, - resolved_method, - &sig, - data, - ctx, - ); - emit_runtime_descriptor_with_saved_receiver(span, &descriptor_label, receiver_ty, emitter, data); -} - -/// Extracts the class and method names from a runtime callable case. -fn runtime_case_method_target(case: &RuntimeCallableCase) -> Option<(String, String)> { - let php_name = case.php_name.as_ref()?; - let (class_name, method_name) = php_name.split_once("::")?; - Some((class_name.to_string(), method_name.to_string())) -} - -/// Builds `$callback[$index]` for reading a stored callable-array slot. -fn callable_array_slot_expr(var_name: &str, index: i64, span: Span) -> Expr { - Expr::new( - ExprKind::ArrayAccess { - array: Box::new(Expr::new(ExprKind::Variable(var_name.to_string()), span)), - index: Box::new(Expr::new(ExprKind::IntLiteral(index), span)), - }, - span, - ) -} - -/// Returns a hidden receiver parameter name that cannot collide with visible callback params. -fn unique_hidden_param(base: &str, sig: &FunctionSig) -> String { - if !sig.params.iter().any(|(name, _)| name == base) { - return base.to_string(); - } - let mut idx = 0usize; - loop { - let candidate = format!("{}_{}", base, idx); - if !sig.params.iter().any(|(name, _)| name == &candidate) { - return candidate; - } - idx += 1; - } -} - -/// Selects a callable descriptor for the currently loaded string callback name. -fn emit_select_loaded_string_descriptor( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - emitter.comment("fiber callable string descriptor selection"); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the Fiber string callback name during descriptor selection - - let cases = callable_dispatch::runtime_callable_cases(ctx, data, &[], None); - let call_reg = abi::nested_call_reg(emitter); - let done_label = ctx.next_label("fiber_string_callable_done"); - for case in cases.iter().filter(|case| case.has_invoker) { - emit_string_case_selection(case, call_reg, &done_label, emitter, ctx, data); - } - emit_fiber_callable_no_match_abort(emitter, data); - emitter.label(&done_label); - abi::emit_release_temporary_stack(emitter, 16); // discard the saved Fiber string callback name after descriptor selection -} - -/// Emits one runtime string descriptor case for `new Fiber($callback)`. -fn emit_string_case_selection( - case: &RuntimeCallableCase, - call_reg: &str, - done_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let next_case = ctx.next_label("fiber_string_callable_next"); - let selector = RuntimeCallableSelector::StringNameStack { - ptr_offset: 0, - len_offset: 8, - call_reg, - }; - callable_dispatch::emit_branch_if_callable_case_mismatch( - &selector, case, &next_case, emitter, ctx, data, - ); - let result_reg = abi::int_result_reg(emitter); - if call_reg != result_reg { - emitter.instruction(&format!("mov {}, {}", result_reg, call_reg)); // return the selected Fiber callable descriptor - } - abi::emit_jump(emitter, done_label); - emitter.label(&next_case); -} - -/// Returns a callable target for a two-slot literal callable array supported by Fiber. -fn callable_array_literal_target(expr: &Expr, ctx: &Context) -> Option { - let (receiver, method) = callable_array_parts(expr)?; - if let Some(receiver) = static_callable_receiver(receiver, ctx) { - return Some(CallableTarget::StaticMethod { - receiver, - method: method.to_string(), - }); - } - Some(CallableTarget::Method { - object: Box::new(receiver.clone()), - method: method.to_string(), - }) -} - -/// Returns receiver and method from `[receiver, "method"]`. -fn callable_array_parts(expr: &Expr) -> Option<(&Expr, &str)> { - let ExprKind::ArrayLiteral(elems) = &expr.kind else { - return None; - }; - if elems.len() != 2 { - return None; - } - let ExprKind::StringLiteral(method) = &elems[1].kind else { - return None; - }; - Some((&elems[0], method.as_str())) -} - -/// Resolves a literal callable-array receiver to a static class target. -fn static_callable_receiver(receiver: &Expr, ctx: &Context) -> Option { - let class_name = match &receiver.kind { - ExprKind::StringLiteral(class_name) => resolve_class_name(ctx, class_name)?.to_string(), - ExprKind::ClassConstant { receiver } => resolve_static_receiver_class(receiver, ctx)?, - _ => return None, - }; - Some(StaticReceiver::Named(Name::from(class_name))) -} - -/// Resolves a scoped receiver to a concrete class name. -fn resolve_static_receiver_class(receiver: &StaticReceiver, ctx: &Context) -> Option { - match receiver { - StaticReceiver::Named(name) => resolve_class_name(ctx, name.as_str()).map(str::to_string), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.parent.clone()), - } -} - -/// Resolves a class name case-insensitively against known codegen classes. -fn resolve_class_name<'a>(ctx: &'a Context, class_name: &str) -> Option<&'a str> { - let class_key = php_symbol_key(class_name.trim_start_matches('\\')); - ctx.classes - .keys() - .find(|existing| php_symbol_key(existing) == class_key) - .map(String::as_str) -} - -/// Emits a fatal diagnostic when a runtime Fiber callable name has no descriptor case. -fn emit_fiber_callable_no_match_abort(emitter: &mut Emitter, data: &mut DataSection) { - let (message_label, message_len) = data.add_string( - b"Fatal error: Fiber callback string did not resolve to an invokable target\n", - ); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // write the Fiber callable diagnostic to stderr - abi::emit_symbol_address(emitter, "x1", &message_label); - emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the Fiber callable diagnostic byte length to write() - emitter.syscall(4); - abi::emit_exit(emitter, 1); - } - Arch::X86_64 => { - emitter.instruction("mov edi, 2"); // write the Fiber callable diagnostic to stderr - abi::emit_symbol_address(emitter, "rsi", &message_label); - emitter.instruction(&format!("mov edx, {}", message_len)); // pass the Fiber callable diagnostic byte length to write() - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the Fiber callable diagnostic - abi::emit_exit(emitter, 1); - } - } -} diff --git a/src/codegen/expr/objects/fiber_wrapper.rs b/src/codegen/expr/objects/fiber_wrapper.rs deleted file mode 100644 index 32f473c834..0000000000 --- a/src/codegen/expr/objects/fiber_wrapper.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Purpose: -//! Lowers deferred wrapper registration for object methods used as fiber callables. -//! Produces object-related expression results while respecting runtime metadata and ownership rules. -//! -//! Called from: -//! - `crate::codegen::expr::objects` -//! -//! Key details: -//! - Object handles, property storage, and class ids must stay consistent with emitted class tables. - -use crate::codegen::context::{Context, DeferredFiberWrapper}; -use crate::parser::ast::{Expr, ExprKind}; -use crate::types::{fibers, FunctionSig, PhpType}; - -/// Registers a fiber wrapper entry point for a callable used as a Fiber start routine. -/// -/// Dispatches on `callable_expr` to extract the signature and capture metadata: -/// - `ExprKind::Closure`: uses deferred closure signature, hidden params, and return analysis -/// - `ExprKind::FirstClassCallable`: uses deferred closure signature directly -/// - `ExprKind::Variable`: looks up closure captures and signatures, searches deferred closures for matching params/captures -/// -/// On success, pushes a `DeferredFiberWrapper` to `ctx.deferred_fiber_wrappers` and returns -/// the unique label for the wrapper entry point. Returns `None` if validation fails or -/// the callable kind is unsupported. -/// -/// # Panics -/// Panics if `ctx.deferred_closures` is empty when processing a closure or first-class callable. -pub(super) fn prepare_fiber_wrapper(callable_expr: &Expr, ctx: &mut Context) -> Option { - let (mut sig, visible_param_count, hidden_arg_types) = match &callable_expr.kind { - ExprKind::Closure { - params, - variadic, - body, - .. - } => { - let visible_param_count = fibers::visible_param_count(params.len(), variadic.is_some()); - let no_terminal_return = !fibers::closure_body_has_return(body); - let deferred = ctx.deferred_closures.last_mut()?; - fibers::adapt_entry_sig(&mut deferred.sig, visible_param_count, no_terminal_return); - fibers::validate_callback_signature(&deferred.sig, visible_param_count, callable_expr.span) - .ok()?; - ( - deferred.sig.clone(), - visible_param_count, - deferred - .hidden_params - .iter() - .map(hidden_capture_arg_type) - .collect(), - ) - } - ExprKind::FirstClassCallable(_) => { - let deferred = ctx.deferred_closures.last_mut()?; - let visible_param_count = deferred.sig.params.len(); - fibers::adapt_entry_sig(&mut deferred.sig, visible_param_count, false); - fibers::validate_callback_signature(&deferred.sig, visible_param_count, callable_expr.span) - .ok()?; - ( - deferred.sig.clone(), - visible_param_count, - deferred - .hidden_params - .iter() - .map(hidden_capture_arg_type) - .collect(), - ) - } - ExprKind::Variable(name) if variable_needs_descriptor_invoker(name, callable_expr, ctx) => { - return Some(prepare_descriptor_invoker_wrapper(ctx)); - } - ExprKind::Variable(name) => { - ctx.mark_fcc_used(name); - let captures = ctx.closure_captures.get(name).cloned().unwrap_or_default(); - let mut sig = ctx.closure_sigs.get(name).cloned()?; - let visible_param_count = sig.params.len(); - let mut hidden_arg_types = captures - .iter() - .map(hidden_capture_arg_type) - .collect::>(); - if let Some(deferred) = ctx.deferred_closures.iter_mut().rev().find(|deferred| { - deferred.sig.params == sig.params && deferred.captures == captures - }) { - let no_terminal_return = !fibers::closure_body_has_return(&deferred.body); - fibers::adapt_entry_sig( - &mut deferred.sig, - visible_param_count, - no_terminal_return, - ); - fibers::validate_callback_signature(&deferred.sig, visible_param_count, callable_expr.span) - .ok()?; - hidden_arg_types = deferred - .hidden_params - .iter() - .map(hidden_capture_arg_type) - .collect(); - sig = deferred.sig.clone(); - } else { - fibers::adapt_entry_sig(&mut sig, visible_param_count, false); - fibers::validate_callback_signature(&sig, visible_param_count, callable_expr.span) - .ok()?; - } - ctx.closure_sigs.insert(name.clone(), sig.clone()); - (sig, visible_param_count, hidden_arg_types) - } - _ if expr_is_descriptor_backed_callable(callable_expr, ctx) => { - return Some(prepare_descriptor_invoker_wrapper(ctx)); - } - _ => return None, - }; - - fibers::adapt_entry_sig(&mut sig, visible_param_count, false); - let label = ctx.next_label("fiber_entry_wrapper"); - ctx.deferred_fiber_wrappers.push(DeferredFiberWrapper { - label: label.clone(), - sig, - visible_param_count, - hidden_arg_types, - retain_hidden_args_for_closure_call: true, - use_descriptor_invoker: false, - }); - Some(label) -} - -/// Returns true when a variable's callable value must be invoked through its runtime descriptor. -fn variable_needs_descriptor_invoker(name: &str, callable_expr: &Expr, ctx: &Context) -> bool { - ctx.runtime_callable_vars.contains(name) - || ctx.callable_param_names.contains(name) - || (!ctx.closure_sigs.contains_key(name) && expr_is_descriptor_backed_callable(callable_expr, ctx)) -} - -/// Returns true when `expr` is already represented by a runtime callable descriptor. -fn expr_is_descriptor_backed_callable(expr: &Expr, ctx: &Context) -> bool { - matches!( - crate::codegen::functions::infer_contextual_type(expr, ctx).codegen_repr(), - PhpType::Callable - ) -} - -/// Registers or reuses the generic Fiber wrapper that calls a descriptor invoker. -pub(super) fn prepare_descriptor_invoker_wrapper(ctx: &mut Context) -> String { - if let Some(existing) = ctx - .deferred_fiber_wrappers - .iter() - .find(|wrapper| wrapper.use_descriptor_invoker) - { - return existing.label.clone(); - } - - let label = ctx.next_label("fiber_descriptor_invoker"); - ctx.deferred_fiber_wrappers.push(DeferredFiberWrapper { - label: label.clone(), - sig: descriptor_invoker_placeholder_sig(), - visible_param_count: 0, - hidden_arg_types: Vec::new(), - retain_hidden_args_for_closure_call: true, - use_descriptor_invoker: true, - }); - label -} - -/// Builds a placeholder signature for a descriptor-backed Fiber wrapper. -fn descriptor_invoker_placeholder_sig() -> FunctionSig { - FunctionSig { - params: Vec::new(), - defaults: Vec::new(), - return_type: PhpType::Mixed, - declared_return: false, - by_ref_return: false, - ref_params: Vec::new(), - declared_params: Vec::new(), - variadic: None, - deprecation: None, - } -} - -/// Maps a closure capture tuple to the `PhpType` used for the hidden argument passing slot. -/// -/// Reference captures are encoded as `PhpType::Int` in the hidden arg type vector -/// because they are passed as integer pointers in the ABI. -fn hidden_capture_arg_type((_, ty, by_ref): &(String, PhpType, bool)) -> PhpType { - if *by_ref { - PhpType::Int - } else { - ty.clone() - } -} diff --git a/src/codegen/expr/objects/instanceof.rs b/src/codegen/expr/objects/instanceof.rs deleted file mode 100644 index 3ec1a20906..0000000000 --- a/src/codegen/expr/objects/instanceof.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! Purpose: -//! Lowers instanceof checks against class, interface, and dynamic targets. -//! Produces object-related expression results while respecting runtime metadata and ownership rules. -//! -//! Called from: -//! - `crate::codegen::expr::objects` -//! -//! Key details: -//! - Object handles, property storage, and class ids must stay consistent with emitted class tables. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::names::Name; -use crate::parser::ast::{Expr, InstanceOfTarget}; -use crate::types::PhpType; - -use super::super::emit_expr; -use super::dispatch; - -/// Lowers `value instanceof Target` for named, dynamic, and late-static targets. -/// -/// Dispatches to `emit_named_instanceof` for `InstanceOfTarget::Name` and -/// `emit_dynamic_instanceof` for `InstanceOfTarget::Expr`. Returns `PhpType::Bool`. -pub(super) fn emit_instanceof( - value: &Expr, - target: &InstanceOfTarget, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - match target { - InstanceOfTarget::Name(name) => emit_named_instanceof(value, name, emitter, ctx, data), - InstanceOfTarget::Expr(target) => emit_dynamic_instanceof(value, target, emitter, ctx, data), - } -} - -/// Emits instanceof against a resolved named target (class, interface, or late-static). -/// -/// Class targets emit `class_id` with `target_kind_id = 0`. Interface targets emit -/// `interface_id` with `target_kind_id = 1`. Late-static uses `dispatch::emit_forwarded_called_class_id` -/// to resolve at runtime. Returns `PhpType::Bool`. -fn emit_named_instanceof( - value: &Expr, - target: &Name, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment(&format!("instanceof {}", target.as_str())); - let value_ty = emit_expr(value, emitter, ctx, data); - let value_repr = value_ty.codegen_repr(); - - let target_kind = match classify_named_target(target, ctx) { - Some(kind) => kind, - None => { - emit_false(emitter); - return PhpType::Bool; - } - }; - - if !can_hold_object_or_boxed_value(&value_repr) { - emit_false(emitter); - return PhpType::Bool; - } - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the tested value while materializing the target type id - let target_kind_id = match target_kind { - ResolvedInstanceOfTarget::Class(class_id) => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), class_id as i64); - 0 - } - ResolvedInstanceOfTarget::Interface(interface_id) => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), interface_id as i64); - 1 - } - ResolvedInstanceOfTarget::LateStaticClass => { - if !dispatch::emit_forwarded_called_class_id(emitter, ctx) { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // discard the preserved tested value before returning false - emit_false(emitter); - return PhpType::Bool; - } - 0 - } - }; - let matcher = if matches!(value_repr, PhpType::Mixed | PhpType::Union(_)) { - "__rt_mixed_instanceof" - } else { - "__rt_exception_matches" - }; - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the target id while loading runtime matcher arguments - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 1)); // pass target class/interface id as matcher argument 2 - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 0)); // pass the tested object pointer as matcher argument 1 - abi::emit_load_int_immediate( - emitter, - abi::int_arg_reg_name(emitter.target, 2), - target_kind_id, - ); - abi::emit_call_label(emitter, matcher); // run the object/class/interface matcher for plain or boxed values - PhpType::Bool -} - -/// Emits instanceof where the target is a dynamic expression. -/// -/// Validates the target at runtime (string class-name, object, or boxed Mixed), -/// then calls `__rt_exception_matches`. Returns `PhpType::Bool`. -fn emit_dynamic_instanceof( - value: &Expr, - target: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - emitter.comment("dynamic instanceof"); - let value_ty = emit_expr(value, emitter, ctx, data); - let value_repr = value_ty.codegen_repr(); - - let target_false = ctx.next_label("instanceof_dynamic_target_false"); - let done = ctx.next_label("instanceof_dynamic_done"); - - emit_normalized_object_value(&value_repr, emitter, ctx); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the tested object-or-null pointer while validating the dynamic target - emit_dynamic_target(target, &target_false, emitter, ctx, data); - emit_dynamic_match_call(emitter); - abi::emit_jump(emitter, &done); // skip the false paths after the runtime matcher returns a boolean - - emitter.label(&target_false); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // discard the preserved tested value for an unknown string target - emit_false(emitter); - abi::emit_jump(emitter, &done); // converge on the common dynamic instanceof result - - emitter.label(&done); - PhpType::Bool -} - -/// Named target resolved during codegen before runtime dispatch. -enum ResolvedInstanceOfTarget { - Class(u64), - Interface(u64), - LateStaticClass, -} - -/// Classifies a named target into `Class`, `Interface`, or `LateStaticClass`. -/// -/// Handles `self`, `parent`, `static`, and fully-qualified names using `ctx.classes` -/// and `ctx.interfaces`. Returns `None` for unresolved names (caller emits false). -fn classify_named_target(target: &Name, ctx: &Context) -> Option { - let target_name = match target.as_str() { - "self" => ctx.current_class.as_deref()?, - "parent" => ctx - .current_class - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.parent.as_deref())?, - "static" => return Some(ResolvedInstanceOfTarget::LateStaticClass), - other => other, - }; - if let Some(class_info) = ctx.classes.get(target_name) { - Some(ResolvedInstanceOfTarget::Class(class_info.class_id)) - } else { - ctx.interfaces - .get(target_name) - .map(|interface_info| ResolvedInstanceOfTarget::Interface(interface_info.interface_id)) - } -} - -/// Normalizes a value into an object pointer for dynamic instanceof. -/// -/// For `PhpType::Object`, the pointer is already in the result register. -/// For `PhpType::Mixed` and `PhpType::Union`, calls `__rt_mixed_unbox` and promotes -/// the object pointer from `x1`/`rdi` if the runtime tag is 6 (object). Non-object -/// payloads are zeroed so the matcher returns false. Other types emit null. -fn emit_normalized_object_value(value_repr: &PhpType, emitter: &mut Emitter, ctx: &mut Context) { - match value_repr { - PhpType::Object(_) => {} - PhpType::Mixed | PhpType::Union(_) => { - let object_label = ctx.next_label("instanceof_dynamic_value_object"); - let done = ctx.next_label("instanceof_dynamic_value_done"); - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect boxed values before validating the dynamic target - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #6"); // runtime tag 6 means the tested mixed payload is an object - emitter.instruction(&format!("b.eq {}", object_label)); // object payloads can be tested after target validation - emitter.instruction("mov x0, #0"); // non-object payloads become null so the matcher returns false - emitter.instruction(&format!("b {}", done)); // skip object-payload promotion for scalar, array, and null payloads - - emitter.label(&object_label); - emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the normal result register - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 6"); // runtime tag 6 means the tested mixed payload is an object - emitter.instruction(&format!("je {}", object_label)); // object payloads can be tested after target validation - emitter.instruction("xor eax, eax"); // non-object payloads become null so the matcher returns false - emitter.instruction(&format!("jmp {}", done)); // skip object-payload promotion for scalar, array, and null payloads - - emitter.label(&object_label); - emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the normal result register - } - } - emitter.label(&done); - } - _ => { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); - } - } -} - -/// Emits target resolution for a dynamic (expression-based) instanceof operand. -/// -/// Emits the target expression, then dispatches based on its type: -/// - `Str`: calls `__rt_instanceof_lookup` for class-string resolution -/// - `Object`: loads class id from target object header -/// - `Mixed`/`Union`: unboxes and routes to string or object path -/// - Other: calls `__rt_instanceof_invalid_target` (fatal) -fn emit_dynamic_target( - target: &Expr, - false_label: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let target_ty = emit_expr(target, emitter, ctx, data).codegen_repr(); - match target_ty { - PhpType::Str => emit_lookup_string_target(false_label, emitter), - PhpType::Object(_) => emit_object_target_metadata(emitter, ctx), - PhpType::Mixed | PhpType::Union(_) => emit_mixed_target_metadata(false_label, emitter, ctx), - _ => emit_invalid_target_fatal(emitter), - } -} - -/// Emits class-string lookup for a string-typed dynamic target. -/// -/// Calls `__rt_instanceof_lookup` and checks the returned pointer. -/// On AArch64: zero means unresolved (jumps to `false_label`), non-zero puts -/// the resolved target id in `x0` and target kind in `x1`. -fn emit_lookup_string_target(false_label: &str, emitter: &mut Emitter) { - abi::emit_call_label(emitter, "__rt_instanceof_lookup"); // resolve a dynamic class-string target to matcher metadata - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // did the dynamic string resolve to a known class/interface? - emitter.instruction(&format!("b.eq {}", false_label)); // unknown class-string targets make instanceof false - emitter.instruction("mov x0, x1"); // move the resolved target id into the target-id result register - emitter.instruction("mov x1, x2"); // move the resolved target kind into the target-kind result register - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // did the dynamic string resolve to a known class/interface? - emitter.instruction(&format!("je {}", false_label)); // unknown class-string targets make instanceof false - emitter.instruction("mov rax, rdi"); // move the resolved target id into the target-id result register - } - } -} - -/// Emits metadata extraction for an object-typed dynamic target. -/// -/// Checks the object pointer is non-null, then loads the class id from the object header. -/// Emits `target_kind = 0` (class) into the kind register. Fatal if null. -/// On AArch64: class id lands in `x0`, kind in `x1`. On x86_64: class id in `rax`, kind in `rdx`. -fn emit_object_target_metadata(emitter: &mut Emitter, ctx: &mut Context) { - let ok_label = ctx.next_label("instanceof_target_object_ok"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cbnz x0, {}", ok_label)); // non-null object targets can provide runtime class metadata - emit_invalid_target_fatal(emitter); - emitter.label(&ok_label); - emitter.instruction("ldr x0, [x0]"); // load the runtime class id from the target object header - emitter.instruction("mov x1, #0"); // dynamic object targets are always class targets - } - Arch::X86_64 => { - emitter.instruction("test rax, rax"); // null dynamic targets are not valid class-string/object targets - emitter.instruction(&format!("jne {}", ok_label)); // non-null object targets can provide runtime class metadata - emit_invalid_target_fatal(emitter); - emitter.label(&ok_label); - emitter.instruction("mov rax, QWORD PTR [rax]"); // load the runtime class id from the target object header - emitter.instruction("xor edx, edx"); // dynamic object targets are always class targets - } - } -} - -/// Emits metadata extraction for a Mixed/Union dynamic target. -/// -/// Unboxes the Mixed value and routes to the appropriate handler: -/// - Tag 1 (string): calls `emit_lookup_string_target` -/// - Tag 6 (object): promotes the object pointer and calls `emit_object_target_metadata` -/// - Other: calls `__rt_instanceof_invalid_target` (fatal) -fn emit_mixed_target_metadata(false_label: &str, emitter: &mut Emitter, ctx: &mut Context) { - let string_label = ctx.next_label("instanceof_target_string"); - let object_label = ctx.next_label("instanceof_target_object"); - let done = ctx.next_label("instanceof_target_done"); - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect a boxed dynamic target before resolving matcher metadata - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string - emitter.instruction(&format!("b.eq {}", string_label)); // resolve boxed string targets through class-string lookup - emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object - emitter.instruction(&format!("b.eq {}", object_label)); // use the target object's runtime class id - emit_invalid_target_fatal(emitter); - - emitter.label(&string_label); - emit_lookup_string_target(false_label, emitter); - abi::emit_jump(emitter, &done); // keep resolved class-string metadata as the target result - - emitter.label(&object_label); - emitter.instruction("mov x0, x1"); // move the unboxed target object pointer into the normal result register - emit_object_target_metadata(emitter, ctx); - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string - emitter.instruction(&format!("je {}", string_label)); // resolve boxed string targets through class-string lookup - emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object - emitter.instruction(&format!("je {}", object_label)); // use the target object's runtime class id - emit_invalid_target_fatal(emitter); - - emitter.label(&string_label); - emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the lookup input register - emit_lookup_string_target(false_label, emitter); - abi::emit_jump(emitter, &done); // keep resolved class-string metadata as the target result - - emitter.label(&object_label); - emitter.instruction("mov rax, rdi"); // move the unboxed target object pointer into the normal result register - emit_object_target_metadata(emitter, ctx); - } - } - emitter.label(&done); -} - -/// Emits the call to `__rt_exception_matches` for dynamic instanceof. -/// -/// Pushes the resolved target id and kind, then pops them into argument registers -/// (object pointer arg0, class/interface id arg1, target kind arg2) before the call. -/// The target kind distinguishes class (0) from interface (1) targets. -fn emit_dynamic_match_call(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - abi::emit_push_reg(emitter, "x0"); // preserve the resolved dynamic target id - abi::emit_push_reg(emitter, "x1"); // preserve the resolved dynamic target kind - } - Arch::X86_64 => { - abi::emit_push_reg(emitter, "rax"); // preserve the resolved dynamic target id - abi::emit_push_reg(emitter, "rdx"); // preserve the resolved dynamic target kind - } - } - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 2)); // pass target kind as matcher argument 3 - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 1)); // pass target class/interface id as matcher argument 2 - abi::emit_pop_reg(emitter, abi::int_arg_reg_name(emitter.target, 0)); // pass the tested object pointer as matcher argument 1 - abi::emit_call_label(emitter, "__rt_exception_matches"); // run the object/class/interface matcher for the dynamic target -} - -/// Emits a fatal trap when a dynamic target is neither string nor object. -/// -/// Called when the target expression type is invalid for instanceof (e.g., int, bool). -/// Emits a call to `__rt_instanceof_invalid_target` which aborts at runtime. -fn emit_invalid_target_fatal(emitter: &mut Emitter) { - abi::emit_call_label(emitter, "__rt_instanceof_invalid_target"); // abort when a dynamic target is neither string nor object -} - -/// Returns true if `ty` can hold an object or boxed (Mixed/Union) value at runtime. -/// -/// Used to short-circuit instanceof to false when the value type cannot possibly -/// be an object (e.g., int, float, bool, null, array, resource, callable). -fn can_hold_object_or_boxed_value(ty: &PhpType) -> bool { - match ty { - PhpType::Object(_) | PhpType::Mixed | PhpType::Union(_) => true, - _ => false, - } -} - -/// Emits a false boolean result for instanceof. -/// -/// Loads `0` into the integer result register. Used when the value type cannot -/// hold an object or when the named target cannot be resolved. -fn emit_false(emitter: &mut Emitter) { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 0); -} diff --git a/src/codegen/expr/objects/nullsafe.rs b/src/codegen/expr/objects/nullsafe.rs deleted file mode 100644 index da08a6d7ba..0000000000 --- a/src/codegen/expr/objects/nullsafe.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Purpose: -//! Lowers nullsafe property and method chains with short-circuit results. -//! Produces object-related expression results while respecting runtime metadata and ownership rules. -//! -//! Called from: -//! - `crate::codegen::expr::objects` -//! -//! Key details: -//! - Object handles, property storage, and class ids must stay consistent with emitted class tables. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::functions; -use crate::codegen::platform::Arch; -use crate::codegen::NULL_SENTINEL; -use crate::names::php_symbol_key; -use crate::parser::ast::Expr; -use crate::types::PhpType; - -use super::{access, dispatch}; -use crate::codegen::expr::emit_expr; - -/// Sentinel value representing a plain (non-boxed) null in the runtime. -/// Uses an unlikely bit pattern to distinguish from valid object pointers. - -/// Lowers `$obj?->property` with a short-circuit null result when the receiver is null. -pub(super) fn emit_nullsafe_property_access( - object: &Expr, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let Some((class_name, nullable)) = nullsafe_receiver_class(object, ctx) else { - emit_expr(object, emitter, ctx, data); - emit_plain_null(emitter); - return PhpType::Void; - }; - if !nullable { - return access::emit_property_access(object, property, emitter, ctx, data); - } - - emitter.comment(&format!("?->{}", property)); - let null_label = ctx.next_label("nullsafe_prop_null"); - let done_label = ctx.next_label("nullsafe_prop_done"); - let receiver_ty = emit_expr(object, emitter, ctx, data); - if !emit_nullable_receiver_to_object(&receiver_ty, &null_label, emitter) { - super::emit_boxed_null(emitter); - return PhpType::Mixed; - } - - let property_ty = - access::emit_loaded_object_property_access(&class_name, property, emitter, ctx, data); - super::box_nullable_result(&property_ty, emitter); - abi::emit_jump(emitter, &done_label); - emitter.label(&null_label); - super::emit_boxed_null(emitter); - emitter.label(&done_label); - PhpType::Mixed -} - -/// Lowers `$obj?->method(...)` with a short-circuit boxed null result when the receiver is null. -pub(super) fn emit_nullsafe_method_call( - object: &Expr, - method: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let Some((class_name, nullable)) = nullsafe_receiver_class(object, ctx) else { - emit_expr(object, emitter, ctx, data); - emit_plain_null(emitter); - return PhpType::Void; - }; - - emitter.comment(&format!("?->{}()", method)); - let null_label = ctx.next_label("nullsafe_method_null"); - let done_label = ctx.next_label("nullsafe_method_done"); - let receiver_ty = emit_expr(object, emitter, ctx, data); - if nullable && !emit_nullable_receiver_to_object(&receiver_ty, &null_label, emitter) { - super::emit_boxed_null(emitter); - return PhpType::Mixed; - } - - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the receiver below later argument temporaries until the nullsafe branch commits to the call - let method_key = php_symbol_key(method); - let mut dispatch_method = method_key.as_str(); - let mut magic_args = None; - let sig = ctx.classes.get(&class_name).and_then(|class_info| { - if let Some(sig) = class_info.methods.get(&method_key) { - return Some(sig.clone()); - } - if let Some(sig) = class_info.methods.get("__call") { - dispatch_method = "__call"; - magic_args = Some(super::magic_method_args(method, args, object.span)); - return Some(sig.clone()); - } - None - }); - let args_to_emit = magic_args.as_deref().unwrap_or(args); - let emitted_args = - dispatch::emit_pushed_method_args(args_to_emit, sig.as_ref(), emitter, ctx, data); - let return_ty = dispatch::emit_method_call_with_saved_receiver_below_args( - &class_name, - dispatch_method, - &emitted_args.arg_types, - emitted_args.source_temp_bytes, - emitter, - ctx, - ); - if !nullable { - return return_ty; - } - super::box_nullable_result(&return_ty, emitter); - abi::emit_jump(emitter, &done_label); - emitter.label(&null_label); - super::emit_boxed_null(emitter); - emitter.label(&done_label); - PhpType::Mixed -} - -/// Infers the class name and nullability of the receiver in a nullsafe chain. -/// Returns `None` when the receiver type cannot be resolved to an object type. -/// Handles union types by extracting the object member and tracking whether `Void` is present. -fn nullsafe_receiver_class(object: &Expr, ctx: &Context) -> Option<(String, bool)> { - match functions::infer_contextual_type(object, ctx) { - PhpType::Object(class_name) => Some((class_name, false)), - PhpType::Void => None, - PhpType::Union(members) => { - let mut class_name = None; - let mut nullable = false; - for member in members { - match member { - PhpType::Void => nullable = true, - PhpType::Object(candidate) => class_name = Some(candidate), - _ => return None, - } - } - class_name.map(|name| (name, nullable)) - } - _ => None, - } -} - -/// Emits a null-check that converts a nullable receiver to a non-null object pointer. -/// For `PhpType::Mixed`, emits a runtime unbox call and a conditional jump to `null_label` -/// when the boxed value is null. For `PhpType::Void`, returns `false` to signal that the -/// caller should fall through to the null result path. For other types, returns `true` -/// since they are already non-nullable object types. -fn emit_nullable_receiver_to_object( - receiver_ty: &PhpType, - null_label: &str, - emitter: &mut Emitter, -) -> bool { - match receiver_ty.codegen_repr() { - PhpType::Void => false, - PhpType::Object(_) => true, - PhpType::Mixed => { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // inspect a nullable receiver box before following the object member access - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #8"); // runtime tag 8 means the nullsafe receiver is null - emitter.instruction(&format!("b.eq {}", null_label)); // skip member evaluation when the receiver is null - emitter.instruction("mov x0, x1"); // move the unboxed object pointer into the normal result register - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 8"); // runtime tag 8 means the nullsafe receiver is null - emitter.instruction(&format!("je {}", null_label)); // skip member evaluation when the receiver is null - emitter.instruction("mov rax, rdi"); // move the unboxed object pointer into the normal result register - } - } - true - } - _ => true, - } -} - -/// Writes the NULL_SENTINEL into the integer result register to represent a plain null. -fn emit_plain_null(emitter: &mut Emitter) { - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), NULL_SENTINEL); -} diff --git a/src/codegen/expr/objects/reflection.rs b/src/codegen/expr/objects/reflection.rs deleted file mode 100644 index 532de03238..0000000000 --- a/src/codegen/expr/objects/reflection.rs +++ /dev/null @@ -1,293 +0,0 @@ -//! Purpose: -//! Lowers allocation for the builtin ReflectionClass, ReflectionMethod, and -//! ReflectionProperty objects. -//! -//! Called from: -//! - `crate::codegen::expr::objects::allocation::emit_new_object()` -//! -//! Key details: -//! - The public constructors are compile-time reflection lookups: they build a -//! normal object, then populate private metadata slots from class/member -//! metadata captured by the type checker. - -use crate::codegen::abi; -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::names::php_symbol_key; -use crate::parser::ast::{Expr, ExprKind, StaticReceiver}; -use crate::types::{AttrArgEntry, PhpType}; - -/// Compile-time metadata used to populate a freshly allocated reflection owner -/// object before it is returned to user code. -struct ReflectionOwnerMetadata { - reflected_name: Option, - attr_names: Vec, - attr_args: Vec>>, -} - -/// Returns true if `class_name` is one of the builtin reflection types -/// (ReflectionClass, ReflectionMethod, ReflectionProperty) that require -/// special metadata population instead of normal object construction. -pub(super) fn is_reflection_owner_class(class_name: &str) -> bool { - matches!( - class_name, - "ReflectionClass" | "ReflectionMethod" | "ReflectionProperty" - ) -} - -/// Emits the allocation sequence for a builtin reflection object. -/// -/// Builds a normal object (ignoring constructor args), saves it on the stack, -/// populates its private metadata slots from compile-time metadata, then restores -/// it as the expression result. Returns `PhpType::Object` for the given class name. -pub(super) fn emit_new_reflection_owner( - class_name: &str, - args: &[Expr], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let normalized_args = normalized_constructor_args(class_name, args, ctx); - let metadata = reflection_lookup(class_name, &normalized_args, ctx); - - super::allocation::emit_new_object_core(class_name, &[], false, emitter, ctx, data); - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // save the Reflection* object while replacing its private attribute array - if let Some(reflected_name) = metadata.reflected_name.as_deref() { - crate::codegen::reflection::emit_set_string_property( - emitter, - data, - reflected_name, - abi::symbol_scratch_reg(emitter), - 8, - 16, - ); - } - overwrite_attrs_property( - class_name, - &metadata.attr_names, - &metadata.attr_args, - emitter, - ctx, - data, - ); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the populated Reflection* object as the expression result - PhpType::Object(class_name.to_string()) -} - -/// Normalizes constructor call arguments using the signature for `class_name`'s -/// `__construct` method, falling back to the original args if no signature is -/// available or planning fails. -fn normalized_constructor_args( - class_name: &str, - args: &[Expr], - ctx: &Context, -) -> Vec { - let Some(sig) = ctx - .classes - .get(class_name) - .and_then(|class_info| class_info.methods.get("__construct")) - else { - return args.to_vec(); - }; - let span = args - .first() - .map(|arg| arg.span) - .unwrap_or_else(crate::span::Span::dummy); - crate::types::call_args::plan_call_args( - sig, - args, - span, - false, - false, - ) - .map(|plan| plan.normalized_args()) - .unwrap_or_else(|_| args.to_vec()) -} - -/// Performs compile-time reflection lookup for the given class and constructor -/// arguments, returning the reflected name and attribute metadata captured by -/// the type checker. -/// -/// - `ReflectionClass(arg)` → class attribute metadata -/// - `ReflectionMethod(class, method)` → method attribute metadata -/// - `ReflectionProperty(class, prop)` → property attribute metadata -/// -/// Returns empty metadata if any argument is non-static or the target doesn't -/// exist. -fn reflection_lookup( - class_name: &str, - args: &[Expr], - ctx: &Context, -) -> ReflectionOwnerMetadata { - match class_name { - "ReflectionClass" => { - let Some(reflected_class) = args.first().and_then(|arg| class_name_arg(arg, ctx)) else { - return empty_metadata(); - }; - ctx.classes - .get(&reflected_class) - .map(|info| ReflectionOwnerMetadata { - reflected_name: Some(reflected_class), - attr_names: info.attribute_names.clone(), - attr_args: info.attribute_args.clone(), - }) - .unwrap_or_else(empty_metadata) - } - "ReflectionMethod" => { - let Some(reflected_class) = args.first().and_then(|arg| class_name_arg(arg, ctx)) else { - return empty_metadata(); - }; - let Some(method_name) = args.get(1).and_then(string_literal_arg) else { - return empty_metadata(); - }; - let method_key = php_symbol_key(&method_name); - ctx.classes - .get(&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_metadata) - } - "ReflectionProperty" => { - let Some(reflected_class) = args.first().and_then(|arg| class_name_arg(arg, ctx)) else { - return empty_metadata(); - }; - let Some(property_name) = args.get(1).and_then(string_literal_arg) else { - return empty_metadata(); - }; - ctx.classes - .get(&reflected_class) - .and_then(|info| { - 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(), - }) - }) - .unwrap_or_else(empty_metadata) - } - _ => empty_metadata(), - } -} - -/// Overwrites the `__attrs` property of the Reflection object saved on the stack. -/// -/// First decrements the default empty `__attrs` array, then emits a new array -/// populated from `attr_names` and `attr_args`, then stores the new array pointer -/// and its kind tag (4 = indexed array) into the object's slots at offset 8 and 16. -fn overwrite_attrs_property( - class_name: &str, - attr_names: &[String], - attr_args: &[Option>], - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let (attrs_low_offset, attrs_high_offset) = reflection_attrs_offsets(class_name); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // peek the saved Reflection* object pointer - emitter.instruction(&format!("ldr x0, [x9, #{}]", attrs_low_offset)); // load the default __attrs array pointer - emitter.instruction("bl __rt_decref_array"); // release the default empty attributes array - } - Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rsp]"); // peek the saved Reflection* object pointer - emitter.instruction(&format!("mov rax, QWORD PTR [r10 + {}]", attrs_low_offset)); // load the default __attrs array pointer - emitter.instruction("call __rt_decref_array"); // release the default empty attributes array - } - } - - crate::codegen::reflection::emit_reflection_attribute_array( - attr_names, - attr_args, - emitter, - ctx, - data, - ); - - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("ldr x9, [sp]"); // reload the saved Reflection* object pointer - emitter.instruction(&format!("str x0, [x9, #{}]", attrs_low_offset)); // store the populated __attrs array pointer - emitter.instruction("mov x10, #4"); // runtime kind tag 4 = indexed array - emitter.instruction(&format!("str x10, [x9, #{}]", attrs_high_offset)); // store the __attrs array kind tag - } - Arch::X86_64 => { - emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the saved Reflection* object pointer - emitter.instruction(&format!("mov QWORD PTR [r10 + {}], rax", attrs_low_offset)); // store the populated __attrs array pointer - emitter.instruction(&format!("mov QWORD PTR [r10 + {}], 4", attrs_high_offset)); // store the __attrs array kind tag - } - } -} - -/// 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) - } else { - (8, 16) - } -} - -/// Extracts a class name from `expr` for reflection lookup. -/// -/// Handles `StringLiteral` (direct class name) and `ClassConstant` (e.g. `Foo::class`). -/// Returns `None` for other expression kinds. -fn class_name_arg(expr: &Expr, ctx: &Context) -> Option { - match &expr.kind { - ExprKind::StringLiteral(name) => crate::codegen::reflection::resolve_class_name( - &ctx.classes, - name, - ) - .map(str::to_string), - ExprKind::ClassConstant { receiver } => { - resolve_static_receiver_class(receiver, ctx) - } - _ => None, - } -} - -/// Extracts a string value from `expr` if it is a `StringLiteral`. -/// Returns `None` for any other expression kind. -fn string_literal_arg(expr: &Expr) -> Option { - match &expr.kind { - ExprKind::StringLiteral(value) => Some(value.clone()), - _ => None, - } -} - -/// Resolves the class name for a static receiver used in reflection lookups. -/// -/// - `Named(name)` → resolves via `resolve_class_name` -/// - `Self_` / `Static` → current class from context -/// - `Parent` → parent class of current class -fn resolve_static_receiver_class(receiver: &StaticReceiver, ctx: &Context) -> Option { - match receiver { - StaticReceiver::Named(name) => crate::codegen::reflection::resolve_class_name( - &ctx.classes, - &name.as_canonical(), - ) - .map(str::to_string), - StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), - StaticReceiver::Parent => ctx - .current_class - .as_ref() - .and_then(|class_name| ctx.classes.get(class_name)) - .and_then(|class_info| class_info.parent.clone()), - } -} - -/// Returns empty metadata, used as the fallback when reflection lookup fails. -fn empty_metadata() -> ReflectionOwnerMetadata { - ReflectionOwnerMetadata { - reflected_name: None, - attr_names: Vec::new(), - attr_args: Vec::new(), - } -} diff --git a/src/codegen/expr/objects/static_properties.rs b/src/codegen/expr/objects/static_properties.rs deleted file mode 100644 index a08192ca2f..0000000000 --- a/src/codegen/expr/objects/static_properties.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! Purpose: -//! Lowers static property reads with late-bound receiver handling. -//! Produces object-related expression results while respecting runtime metadata and ownership rules. -//! -//! Called from: -//! - `crate::codegen::expr::objects` -//! -//! Key details: -//! - Object handles, property storage, and class ids must stay consistent with emitted class tables. - -use crate::codegen::abi; -use crate::codegen::context::Context; -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::names::static_property_symbol; -use crate::parser::ast::{StaticReceiver, Visibility}; -use crate::types::PhpType; - -const STATIC_PROP_PRIVATE_ACCESS_LABEL: &str = "_static_prop_private_access_msg"; -const STATIC_PROP_PRIVATE_ACCESS_MSG: &str = - "Fatal error: Cannot access private static property\n"; - -/// A single dispatch branch for a redeclared static property. -/// Tracks the runtime class id, the class that actually declares the property, -/// and whether the current context is forbidden from accessing it (private visibility). -#[derive(Clone)] -struct StaticPropertyBranch { - class_id: u64, - declaring_class: String, - private_inaccessible: bool, -} - -/// Emits the static property access sequence for a given receiver and property name. -/// -/// Late-bound receiver resolution uses the forwarded `__elephc_called_class_id` or `$this` -/// to determine the declaring class at runtime when `StaticReceiver::Static` is used. -/// For static receivers without redeclarations, emits a direct symbol load. -/// Returns the declared `PhpType` of the property. -pub(super) fn emit_static_property_access( - receiver: &StaticReceiver, - property: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let Some((class_name, declaring_class, prop_ty)) = - resolve_static_property(receiver, property, ctx, emitter) - else { - return PhpType::Int; - }; - - emitter.comment(&format!("{}::${}", class_name, property)); - let branches = dynamic_static_property_branches(receiver, property, &declaring_class, ctx); - if branches.is_empty() { - let symbol = static_property_symbol(&declaring_class, property); - if static_property_has_declared_type(&declaring_class, property, ctx) { - emit_uninitialized_static_property_guard(&declaring_class, property, &symbol, emitter, ctx, data); - } - abi::emit_load_symbol_to_result(emitter, &symbol, &prop_ty); - } else if emit_called_class_id_into(emitter, ctx, class_id_work_reg(emitter)) { - emit_dynamic_load_static_property_result( - property, - class_id_work_reg(emitter), - &declaring_class, - &branches, - &prop_ty, - emitter, - ctx, - data, - ); - } else { - emitter.comment("WARNING: missing forwarded called class id"); - let symbol = static_property_symbol(&declaring_class, property); - if static_property_has_declared_type(&declaring_class, property, ctx) { - emit_uninitialized_static_property_guard(&declaring_class, property, &symbol, emitter, ctx, data); - } - abi::emit_load_symbol_to_result(emitter, &symbol, &prop_ty); - } - prop_ty -} - -/// Emits a branch-dispatch sequence for late-bound static property access. -/// -/// Generates an if-else chain: one branch per redeclared static property owner -/// (ordered by class id), plus a fallback to `fallback_declaring_class`. -/// Each branch checks the uninitialized sentinel for typed properties before loading. -/// Terminates with a fatal if a private property is inaccessible from the current context. -fn emit_dynamic_load_static_property_result( - property: &str, - class_id_reg: &str, - fallback_declaring_class: &str, - branches: &[StaticPropertyBranch], - prop_ty: &PhpType, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let done = ctx.next_label("static_prop_load_done"); - let mut labels = Vec::new(); - for branch in branches { - let label = ctx.next_label("static_prop_load_branch"); - emit_branch_if_class_id_matches(emitter, class_id_reg, branch.class_id, &label); - labels.push((label, branch)); - } - let fallback_symbol = static_property_symbol(fallback_declaring_class, property); - if static_property_has_declared_type(fallback_declaring_class, property, ctx) { - emit_uninitialized_static_property_guard( - fallback_declaring_class, - property, - &fallback_symbol, - emitter, - ctx, - data, - ); - } - abi::emit_load_symbol_to_result(emitter, &fallback_symbol, prop_ty); - emit_jump(emitter, &done); - for (label, branch) in labels { - emitter.label(&label); - if branch.private_inaccessible { - emit_private_static_property_access_fatal(emitter); - continue; - } - let symbol = static_property_symbol(&branch.declaring_class, property); - if static_property_has_declared_type(&branch.declaring_class, property, ctx) { - emit_uninitialized_static_property_guard( - &branch.declaring_class, - property, - &symbol, - emitter, - ctx, - data, - ); - } - abi::emit_load_symbol_to_result(emitter, &symbol, prop_ty); - emit_jump(emitter, &done); - } - emitter.label(&done); -} - -/// Loads the late-bound "called class id" into `dest` and returns `true`. -/// -/// Preference order: -/// 1. `__elephc_called_class_id` variable (forwarded from static method frame) -/// 2. `$this` variable (use its runtime class id for late static binding) -/// 3. Neither available → returns `false` and `dest` is unchanged. -/// -/// Used by dynamic static property dispatch to determine which class's -/// redeclared property slot to access at runtime. -fn emit_called_class_id_into(emitter: &mut Emitter, ctx: &Context, dest: &str) -> bool { - if let Some(var) = ctx.variables.get("__elephc_called_class_id") { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), var.stack_offset); // load the forwarded called-class id from the current static method frame - } else if let Some(var) = ctx.variables.get("this") { - abi::load_at_offset(emitter, abi::int_result_reg(emitter), var.stack_offset); // load $this so its runtime class id can drive late static storage - abi::emit_load_from_address( - emitter, - abi::int_result_reg(emitter), - abi::int_result_reg(emitter), - 0, - ); - } else { - return false; - } - emitter.instruction(&format!("mov {}, {}", dest, abi::int_result_reg(emitter))); // copy the called class id into a scratch register for branch dispatch - true -} - -/// Emits a compare-and-branch instruction for a specific class id. -/// -/// Compares `class_id_reg` (runtime called class id) against `class_id`. -/// On AArch64 emits `cmp`/`b.eq`; on x86_64 emits `cmp`/`je`. -fn emit_branch_if_class_id_matches( - emitter: &mut Emitter, - class_id_reg: &str, - class_id: u64, - label: &str, -) { - let compare_reg = class_id_compare_reg(emitter); - abi::emit_load_int_immediate(emitter, compare_reg, class_id as i64); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, {}", class_id_reg, compare_reg)); // compare the runtime called class id to a redeclared static property owner - emitter.instruction(&format!("b.eq {}", label)); // read this static property slot when the called class id matches - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", class_id_reg, compare_reg)); // compare the runtime called class id to a redeclared static property owner - emitter.instruction(&format!("je {}", label)); // read this static property slot when the called class id matches - } - } -} - -/// Emits an unconditional jump to `label`. -/// AArch64 uses `b`; x86_64 uses `jmp`. -fn emit_jump(emitter: &mut Emitter, label: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("b {}", label)); // jump to the end of the static property dispatch chain - } - Arch::X86_64 => { - emitter.instruction(&format!("jmp {}", label)); // jump to the end of the static property dispatch chain - } - } -} - -/// Returns the scratch register used to hold a class id during static property dispatch. -/// AArch64: `x13`; x86_64: `r13`. -fn class_id_work_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "x13", - Arch::X86_64 => "r13", - } -} - -/// Returns the scratch register used to hold the immediate class id in branch comparisons. -/// AArch64: `x14`; x86_64: `r14`. -fn class_id_compare_reg(emitter: &Emitter) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => "x14", - Arch::X86_64 => "r14", - } -} - -/// Collects all redeclared static property branches for a late-bound static receiver. -/// -/// Only applies when `receiver` is `StaticReceiver::Static` and the current class context -/// is set. Walks the class hierarchy from the current class, collecting descendants that -/// redeclare the property with a different declaring class. Private properties that the -/// current context cannot access are marked `private_inaccessible`. -/// Returns branches sorted and deduplicated by `class_id`. -fn dynamic_static_property_branches( - receiver: &StaticReceiver, - property: &str, - fallback_declaring_class: &str, - ctx: &Context, -) -> Vec { - if !matches!(receiver, StaticReceiver::Static) { - return Vec::new(); - } - let Some(base_class) = ctx.current_class.as_deref() else { - return Vec::new(); - }; - let mut branches = Vec::new(); - for (class_name, class_info) in &ctx.classes { - if !is_same_or_descendant(class_name, base_class, ctx) { - continue; - } - let Some(declaring_class) = class_info.static_property_declaring_classes.get(property) else { - continue; - }; - if declaring_class == fallback_declaring_class { - continue; - } - let visibility = class_info - .static_property_visibilities - .get(property) - .unwrap_or(&Visibility::Public); - branches.push(StaticPropertyBranch { - class_id: class_info.class_id, - declaring_class: declaring_class.clone(), - private_inaccessible: matches!(visibility, Visibility::Private) - && Some(declaring_class.as_str()) != ctx.current_class.as_deref(), - }); - } - branches.sort_by_key(|branch| branch.class_id); - branches.dedup_by_key(|branch| branch.class_id); - branches -} - -/// Returns `true` if `class_name` is `ancestor` or a descendant of it. -/// Walks the parent chain via `ctx.classes`. -fn is_same_or_descendant(class_name: &str, ancestor: &str, ctx: &Context) -> bool { - let mut cursor = Some(class_name); - while let Some(name) = cursor { - if name == ancestor { - return true; - } - cursor = ctx - .classes - .get(name) - .and_then(|class_info| class_info.parent.as_deref()); - } - false -} - -/// Resolves a static property receiver and property name to class metadata. -/// -/// Translates `StaticReceiver` variants to concrete class names: -/// - `Named` → the specified class -/// - `Self_` / `Static` → the current class from `ctx` -/// - `Parent` → the parent of the current class -/// -/// Returns `None` if no class context is available, the class is undefined, -/// or the property does not exist on that class. On success returns -/// `(class_name, declaring_class, prop_ty)`. -pub(crate) fn resolve_static_property( - receiver: &StaticReceiver, - property: &str, - ctx: &Context, - emitter: &mut Emitter, -) -> Option<(String, String, PhpType)> { - let class_name = match receiver { - StaticReceiver::Named(class_name) => class_name.as_str().to_string(), - StaticReceiver::Self_ | StaticReceiver::Static => match &ctx.current_class { - Some(class_name) => class_name.clone(), - None => { - emitter.comment("WARNING: self::/static:: used outside class scope"); - return None; - } - }, - StaticReceiver::Parent => { - let current_class = match &ctx.current_class { - Some(class_name) => class_name.clone(), - None => { - emitter.comment("WARNING: parent:: used outside class scope"); - return None; - } - }; - match ctx.classes.get(¤t_class).and_then(|info| info.parent.clone()) { - Some(parent_name) => parent_name, - None => { - emitter.comment(&format!("WARNING: class {} has no parent", current_class)); - return None; - } - } - } - }; - - let class_info = match ctx.classes.get(&class_name) { - Some(class_info) => class_info, - None => { - emitter.comment(&format!("WARNING: undefined class {}", class_name)); - return None; - } - }; - let prop_ty = match class_info - .static_properties - .iter() - .find(|(name, _)| name == property) - .map(|(_, ty)| ty.clone()) - { - Some(prop_ty) => prop_ty, - None => { - emitter.comment(&format!( - "WARNING: undefined static property {}::${}", - class_name, property - )); - return None; - } - }; - let declaring_class = class_info - .static_property_declaring_classes - .get(property) - .cloned() - .unwrap_or_else(|| class_name.clone()); - Some((class_name, declaring_class, prop_ty)) -} - -/// Emits a fatal error and terminates the process when a private static property -/// cannot be accessed due to visibility rules. -/// -/// Writes a fixed diagnostic string to stderr and exits with status 1. -/// Target-specific: AArch64 uses `adrp`/`add-lo12` + syscall; x86_64 uses direct register setup + syscall. -fn emit_private_static_property_access_fatal(emitter: &mut Emitter) { - let len = STATIC_PROP_PRIVATE_ACCESS_MSG.len(); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // fd = stderr for the private static property fatal diagnostic - abi::emit_symbol_address(emitter, "x1", STATIC_PROP_PRIVATE_ACCESS_LABEL); - emitter.instruction(&format!("mov x2, #{}", len)); // pass the private static property fatal diagnostic byte length to write() - emitter.syscall(4); - emitter.instruction("mov x0, #1"); // exit status 1 indicates abnormal termination - emitter.syscall(1); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", STATIC_PROP_PRIVATE_ACCESS_LABEL); // point the Linux write buffer at the private static property fatal diagnostic - emitter.instruction(&format!("mov edx, {}", len)); // pass the private static property fatal diagnostic byte length to write() - emitter.instruction("mov edi, 2"); // fd = stderr for the private static property fatal diagnostic - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the private static property fatal diagnostic before terminating - emitter.instruction("mov edi, 1"); // exit status 1 indicates abnormal termination - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate the process after reporting the private static property access - } - } -} - -/// Returns `true` if `declaring_class` has an explicit declared type for `property`. -/// -/// Checks whether `property` appears in `class_info.declared_static_properties`, -/// indicating a typed static property that requires the uninitialized sentinel guard. -fn static_property_has_declared_type( - declaring_class: &str, - property: &str, - ctx: &Context, -) -> bool { - ctx.classes - .get(declaring_class) - .is_some_and(|class_info| class_info.declared_static_properties.contains(property)) -} - -/// Emits a guard that checks the typed-property sentinel before loading. -/// -/// Loads the sentinel value and compares it against the property's current value. -/// Skips the load and jumps to `initialized_label` if the property is already initialized. -/// Otherwise calls `emit_uninitialized_static_property_fatal` and terminates. -fn emit_uninitialized_static_property_guard( - class_name: &str, - property: &str, - symbol: &str, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let initialized_label = ctx.next_label("static_prop_initialized"); - let marker_reg = abi::secondary_scratch_reg(emitter); - let sentinel_reg = abi::tertiary_scratch_reg(emitter); - abi::emit_load_symbol_to_reg(emitter, marker_reg, symbol, 8); - abi::emit_load_int_immediate(emitter, sentinel_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // check whether the static typed property is still uninitialized - emitter.instruction(&format!("b.ne {}", initialized_label)); // continue the static property read once initialized - } - Arch::X86_64 => { - emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // check whether the static typed property is still uninitialized - emitter.instruction(&format!("jne {}", initialized_label)); // continue the static property read once initialized - } - } - emit_uninitialized_static_property_fatal(class_name, property, emitter, data); - emitter.label(&initialized_label); -} - -/// Emits a fatal error and terminates the process when a typed static property -/// is accessed before initialization. -/// -/// Formats the diagnostic message with the class and property name, -/// adds it to the data section as a static string, writes it to stderr, -/// and exits with status 1. Target-specific register sequences apply. -fn emit_uninitialized_static_property_fatal( - class_name: &str, - property: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) { - let message = format!( - "Fatal error: Typed static property {}::${} must not be accessed before initialization\n", - class_name, property - ); - let (label, len) = data.add_string(message.as_bytes()); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #2"); // fd = stderr for the static typed-property initialization fatal - abi::emit_symbol_address(emitter, "x1", &label); // point write() at the static typed-property diagnostic - emitter.instruction(&format!("mov x2, #{}", len)); // pass the diagnostic byte length to write() - emitter.syscall(4); - emitter.instruction("mov x0, #1"); // exit status 1 indicates abnormal termination - emitter.syscall(1); - } - Arch::X86_64 => { - abi::emit_symbol_address(emitter, "rsi", &label); // point write() at the static typed-property diagnostic - emitter.instruction(&format!("mov edx, {}", len)); // pass the diagnostic byte length to write() - emitter.instruction("mov edi, 2"); // fd = stderr for the static typed-property initialization fatal - emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - emitter.instruction("syscall"); // emit the fatal diagnostic before terminating - emitter.instruction("mov edi, 1"); // exit status 1 indicates abnormal termination - emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit - emitter.instruction("syscall"); // terminate after the static typed-property initialization fatal - } - } -} diff --git a/src/codegen/expr/ownership.rs b/src/codegen/expr/ownership.rs deleted file mode 100644 index 1ded6348ea..0000000000 --- a/src/codegen/expr/ownership.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Purpose: -//! Classifies expression results as owned, borrowed, persistent, or non-refcounted for cleanup decisions. -//! Provides retain and release helpers used around heap-valued temporaries and arguments. -//! -//! Called from: -//! - `crate::codegen::expr` and statement cleanup paths -//! -//! Key details: -//! - Ownership answers must stay conservative to avoid leaks, double frees, and borrowed-value releases. - -use crate::codegen::context::{Context, HeapOwnership}; -use crate::parser::ast::{BinOp, CastType, Expr, ExprKind}; -use crate::types::PhpType; - -/// Classifies what kind of heap ownership an expression result carries. -/// -/// Returns `Owned` for heap-allocated values that the callee owns (strings, arrays, -/// objects, call results). Returns `Borrowed` for variables and property accesses -/// that alias existing storage. Returns `Persistent` for values that outlive the -/// current scope. Returns `NonHeap` for scalars that need no cleanup. -/// -/// This is used to decide whether to emit `retain` or `release` around expression -/// results in codegen cleanup paths. -pub(crate) fn expr_result_heap_ownership(expr: &Expr) -> HeapOwnership { - match &expr.kind { - ExprKind::Variable(_) - | ExprKind::ArrayAccess { .. } - | ExprKind::PropertyAccess { .. } - | ExprKind::DynamicPropertyAccess { .. } - | ExprKind::NullsafePropertyAccess { .. } - | ExprKind::NullsafeDynamicPropertyAccess { .. } - | ExprKind::StaticPropertyAccess { .. } - | ExprKind::This => HeapOwnership::Borrowed, - ExprKind::Spread(inner) - | ExprKind::PtrCast { expr: inner, .. } - | ExprKind::Cast { expr: inner, .. } => expr_result_heap_ownership(inner), - ExprKind::Print(_) => HeapOwnership::NonHeap, - ExprKind::Throw(_) => HeapOwnership::NonHeap, - ExprKind::NullCoalesce { value, default } => { - expr_result_heap_ownership(value).merge(expr_result_heap_ownership(default)) - } - ExprKind::Pipe { .. } => HeapOwnership::Owned, - ExprKind::Ternary { - then_expr, - else_expr, - .. - } => expr_result_heap_ownership(then_expr).merge(expr_result_heap_ownership(else_expr)), - ExprKind::ShortTernary { value, default } => { - expr_result_heap_ownership(value).merge(expr_result_heap_ownership(default)) - } - ExprKind::Match { arms, default, .. } => { - let mut ownership = default - .as_ref() - .map(|expr| expr_result_heap_ownership(expr)) - .unwrap_or(HeapOwnership::NonHeap); - for (_, expr) in arms { - ownership = ownership.merge(expr_result_heap_ownership(expr)); - } - ownership - } - ExprKind::StringLiteral(_) - | ExprKind::ArrayLiteral(_) - | ExprKind::ArrayLiteralAssoc(_) - | ExprKind::Closure { .. } - | ExprKind::FirstClassCallable(_) - | ExprKind::FunctionCall { .. } - | ExprKind::ClosureCall { .. } - | ExprKind::ExprCall { .. } - | ExprKind::MethodCall { .. } - | ExprKind::NullsafeMethodCall { .. } - | ExprKind::StaticMethodCall { .. } - | ExprKind::NewObject { .. } => HeapOwnership::Owned, - ExprKind::BinaryOp { op, .. } => { - if matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Concat) { - HeapOwnership::Owned - } else { - HeapOwnership::NonHeap - } - } - _ => HeapOwnership::NonHeap, - } -} - -/// Returns `true` if the expression produces a string result that uses a transient -/// concatenation buffer (stack-allocated, not leak-safe across yields or exceptions). -/// -/// This applies to binary concat operations and to casts/spreads/error-suppress -/// wrappers around such concat chains. Codegen uses this to decide whether to copy -/// the result before a potential yield or control-flow merge. -pub(crate) fn string_result_uses_transient_concat_buffer(expr: &Expr) -> bool { - match &expr.kind { - ExprKind::BinaryOp { - op: BinOp::Concat, .. - } => true, - ExprKind::Spread(inner) - | ExprKind::PtrCast { expr: inner, .. } - | ExprKind::ErrorSuppress(inner) => string_result_uses_transient_concat_buffer(inner), - ExprKind::Cast { - target: CastType::String, - expr: inner, - } => string_result_uses_transient_concat_buffer(inner), - _ => false, - } -} - -/// Returns `true` if a call expression produces an owned string temporary that -/// requires release when the call's result is not consumed. -/// -/// This is true for user-defined function calls that return strings (non-extern, -/// non-builtin), method calls, closure calls, and expressions that involve types -/// with `__toString`. Builtin functions that return owned strings are also included. -pub(crate) fn string_result_is_owned_call_temp(value: &Expr, ctx: &Context) -> bool { - match &value.kind { - ExprKind::FunctionCall { name, .. } => { - let name = name.as_str(); - builtin_returns_owned_string(name) - || (ctx.functions.contains_key(name) - && !ctx.extern_functions.contains_key(name) - && !crate::types::checker::builtins::is_supported_builtin_function(name)) - } - ExprKind::MethodCall { .. } - | ExprKind::NullsafeMethodCall { .. } - | ExprKind::StaticMethodCall { .. } - | ExprKind::ClosureCall { .. } - | ExprKind::ExprCall { .. } => true, - ExprKind::Cast { - target: CastType::String, - expr, - } => string_result_is_owned_call_temp(expr, ctx), - ExprKind::Variable(name) => ctx - .variables - .get(name) - .is_some_and(|var| { - type_has_tostring(&var.ty, ctx) || type_has_tostring(&var.static_ty, ctx) - }), - ExprKind::This => ctx - .current_class - .as_deref() - .is_some_and(|class_name| class_has_tostring(ctx, class_name)), - ExprKind::NewObject { class_name, .. } => class_has_tostring(ctx, class_name.as_str()), - _ => false, - } -} - -/// Returns `true` for builtin functions that return an owned heap-allocated string -/// requiring release. -/// -/// Currently only `ptr_read_string` returns owned strings among builtins. -fn builtin_returns_owned_string(name: &str) -> bool { - matches!(name, "ptr_read_string") -} - -/// Returns `true` if a PHP type has a `__toString` method, making its runtime -/// representation an owned string when coerced. -fn type_has_tostring(ty: &PhpType, ctx: &Context) -> bool { - match ty.codegen_repr() { - PhpType::Object(class_name) => class_has_tostring(ctx, &class_name), - _ => false, - } -} - -/// Returns `true` if a class defines a `__toString` method. -fn class_has_tostring(ctx: &Context, class_name: &str) -> bool { - ctx.classes - .get(class_name) - .is_some_and(|class_info| class_info.methods.contains_key("__tostring")) -} diff --git a/src/codegen/expr/scalars.rs b/src/codegen/expr/scalars.rs deleted file mode 100644 index 7cfb0d595e..0000000000 --- a/src/codegen/expr/scalars.rs +++ /dev/null @@ -1,262 +0,0 @@ -//! Purpose: -//! Lowers scalar literals and constants into target result registers or data-section labels. -//! Handles integers, floats, booleans, nulls, strings, and compile-time constant references. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Literal storage must match data-section labels and the result conventions expected by coercion helpers. - -use crate::codegen::platform::Arch; -use crate::codegen::NULL_SENTINEL; - -use super::super::abi; -use super::super::context::Context; -use super::super::data_section::DataSection; -use super::super::emit::Emitter; -use super::{Expr, PhpType}; - - -/// Emits a boolean literal as an integer into the integer result register. -/// `true` becomes 1, `false` becomes 0. -pub(super) fn emit_bool_literal(b: bool, emitter: &mut Emitter) -> PhpType { - emitter.comment(&format!("bool {}", b)); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), if b { 1 } else { 0 }); - PhpType::Bool -} - -/// Emits a null literal using a sentinel value into the integer result register. -/// Uses `NULL_SENTINEL` (0x7fff_ffff_ffff_fffe) to represent null at runtime. -pub(super) fn emit_null_literal(emitter: &mut Emitter) -> PhpType { - emitter.comment("null"); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), NULL_SENTINEL); - PhpType::Void -} - -/// Adds a string literal to the data section and loads its address and length -/// into the string result registers (ptr in first reg, len in second reg). -pub(super) fn emit_string_literal( - value: &str, - emitter: &mut Emitter, - data: &mut DataSection, -) -> PhpType { - let bytes = crate::string_bytes::literal_bytes(value); - let (label, len) = data.add_string(&bytes); - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - emitter.comment(&format!("load string \"{}\"", value.escape_default())); - abi::emit_symbol_address(emitter, ptr_reg, &label); - abi::emit_load_int_immediate(emitter, len_reg, len as i64); - PhpType::Str -} - -/// Loads an integer literal directly into the integer result register. -pub(super) fn emit_int_literal(value: i64, emitter: &mut Emitter) -> PhpType { - emitter.comment(&format!("load int {}", value)); - abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), value); - PhpType::Int -} - -/// Adds a float literal to the data section and loads it via a symbol address -/// into the float result register (xmm0 on ARM64, xmm0 on x86_64). -pub(super) fn emit_float_literal( - value: f64, - emitter: &mut Emitter, - data: &mut DataSection, -) -> PhpType { - let label = data.add_float(value); - let scratch = abi::symbol_scratch_reg(emitter); - emitter.comment(&format!("load float {}", value)); - abi::emit_symbol_address(emitter, scratch, &label); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("ldr {}, [{}]", abi::float_result_reg(emitter), scratch)); // load the 64-bit float literal through the symbol scratch register - } - Arch::X86_64 => { - emitter.instruction(&format!( // load the 64-bit float literal through the symbol scratch register - "movsd {}, QWORD PTR [{}]", - abi::float_result_reg(emitter), - scratch - )); - } - } - PhpType::Float -} - -/// Evaluates the inner expression, then negates it in place. -/// For floats uses `fneg` (ARM64) or `subsd` with 0.0 (x86_64). -/// For integers uses two's-complement `neg`. -/// Coerces null to zero before negating. -pub(super) fn emit_negate( - inner: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let ty = super::emit_expr(inner, emitter, ctx, data); - emitter.comment("negate"); - if ty == PhpType::TaggedScalar { - // narrow a tagged scalar (null -> 0) before the two's-complement negate - crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(emitter); - } - if ty == PhpType::Float { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!( // flip the sign bit of the current floating-point result - "fneg {}, {}", - abi::float_result_reg(emitter), - abi::float_result_reg(emitter) - )); - } - Arch::X86_64 => { - emitter.instruction("xorpd xmm15, xmm15"); // materialize +0.0 in a scratch xmm register before subtracting the value - emitter.instruction(&format!( // compute 0.0 - value to negate the current floating-point result - "subsd xmm15, {}", - abi::float_result_reg(emitter) - )); - emitter.instruction(&format!( // move the negated floating-point result back into the ABI return register - "movsd {}, xmm15", - abi::float_result_reg(emitter) - )); - } - } - PhpType::Float - } else { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!( // two's-complement negate the current integer result in place - "neg {}, {}", - abi::int_result_reg(emitter), - abi::int_result_reg(emitter) - )); - } - Arch::X86_64 => { - emitter.instruction(&format!("neg {}", abi::int_result_reg(emitter))); // two's-complement negate the current integer result in place - } - } - PhpType::Int - } -} - -/// Evaluates the inner expression, coerces null to zero, then applies -/// bitwise NOT (`mvn` on ARM64, `not` on x86_64) in place. -pub(super) fn emit_bit_not( - inner: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let ty = super::emit_expr(inner, emitter, ctx, data); - super::coerce_null_to_zero(emitter, &ty); - emitter.comment("bitwise not"); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!( // invert every bit of the current integer result in place - "mvn {}, {}", - abi::int_result_reg(emitter), - abi::int_result_reg(emitter) - )); - } - Arch::X86_64 => { - emitter.instruction(&format!("not {}", abi::int_result_reg(emitter))); // invert every bit of the current integer result in place - } - } - PhpType::Int -} - -/// Evaluates the inner expression, coerces it to a truthiness value, -/// then applies logical NOT (compare to 0, set result to 1 if equal). -/// Returns a boolean (0 or 1) in the integer result register. -pub(super) fn emit_not( - inner: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let ty = super::emit_expr(inner, emitter, ctx, data); - emitter.comment("logical not"); - super::coerce_to_truthiness(emitter, ctx, &ty); - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("cmp x0, #0"); // test if the coerced truthiness result is falsy - emitter.instruction("cset x0, eq"); // return 1 when the coerced truthiness result was false, else 0 - } - Arch::X86_64 => { - emitter.instruction("cmp rax, 0"); // test if the coerced truthiness result is falsy - emitter.instruction("sete al"); // write 1 to the low byte when the coerced truthiness result was false - emitter.instruction("movzx rax, al"); // widen the boolean low byte back into the full integer result register - } - } - PhpType::Bool -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::codegen::context::Context; - use crate::codegen::data_section::DataSection; - use crate::codegen::platform::{Arch, Platform, Target}; - use crate::parser::ast::Expr; - - /// Verifies emitter x86. - fn test_emitter_x86() -> Emitter { - Emitter::new(Target::new(Platform::Linux, Arch::X86_64)) - } - - /// Verifies emit scalar literals for linux x86_64 use native result registers. - #[test] - fn test_emit_scalar_literals_for_linux_x86_64_use_native_result_registers() { - let mut emitter = test_emitter_x86(); - let mut data = DataSection::new(); - - emit_bool_literal(true, &mut emitter); - emit_null_literal(&mut emitter); - emit_string_literal("hi", &mut emitter, &mut data); - emit_int_literal(42, &mut emitter); - emit_float_literal(3.5, &mut emitter, &mut data); - - let out = emitter.output(); - assert!(out.contains(" mov rax, 1\n")); - assert!(out.contains(&format!(" mov rax, {}\n", NULL_SENTINEL))); - assert!(out.contains(" lea rax, [rip + ")); - assert!(out.contains(" mov rdx, 2\n")); - assert!(out.contains(" mov rax, 42\n")); - assert!(out.contains(" lea r11, [rip + ")); - assert!(out.contains(" movsd xmm0, QWORD PTR [r11]\n")); - } - - /// Verifies emit negate and bit not for linux x86_64 use native instructions. - #[test] - fn test_emit_negate_and_bit_not_for_linux_x86_64_use_native_instructions() { - let mut emitter = test_emitter_x86(); - let mut ctx = Context::new(); - let mut data = DataSection::new(); - - emit_negate(&Expr::int_lit(7), &mut emitter, &mut ctx, &mut data); - emit_bit_not(&Expr::int_lit(3), &mut emitter, &mut ctx, &mut data); - - let out = emitter.output(); - assert!(out.contains(" mov rax, 7\n")); - assert!(out.contains(" neg rax\n")); - assert!(out.contains(" mov rax, 3\n")); - assert!(out.contains(" not rax\n")); - } - - /// Verifies emit not for linux x86_64 uses native boolean normalization. - #[test] - fn test_emit_not_for_linux_x86_64_uses_native_boolean_normalization() { - let mut emitter = test_emitter_x86(); - let mut ctx = Context::new(); - let mut data = DataSection::new(); - - emit_not(&Expr::int_lit(0), &mut emitter, &mut ctx, &mut data); - emit_not(&Expr::string_lit("0"), &mut emitter, &mut ctx, &mut data); - - let out = emitter.output(); - assert!(out.contains(" cmp rax, 0\n")); - assert!(out.contains(" sete al\n")); - assert!(out.contains(" movzx rax, al\n")); - assert!(out.contains(" test rdx, rdx\n")); - assert!(out.contains(" movzx r10d, BYTE PTR [rax]\n")); - } -} diff --git a/src/codegen/expr/ternary.rs b/src/codegen/expr/ternary.rs deleted file mode 100644 index a916743413..0000000000 --- a/src/codegen/expr/ternary.rs +++ /dev/null @@ -1,214 +0,0 @@ -//! Purpose: -//! Lowers PHP ternary and elvis expressions with branch labels and merged result storage. -//! Preserves short-circuit behavior while producing one expression result for callers. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Only the selected branch may run, and branch result types must be coerced into a common register shape. - -use crate::codegen::context::Context; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::{abi, functions}; -use crate::parser::ast::Expr; -use crate::types::{FunctionSig, PhpType}; - -use super::{coerce_result_to_type, coerce_to_string, coerce_to_truthiness, emit_expr}; - -/// Emits a full ternary expression (`cond ? then : else`). -/// -/// Evaluates `condition`, branches to `else_label` if zero, then emits `then_expr` and jumps to `end_label`. -/// Falls through to `else_label` for the else branch. Both branches are coerced to a common `result_ty`. -/// Returns the unified result type after both branches have been emitted. -pub(super) fn emit_ternary( - condition: &Expr, - then_expr: &Expr, - else_expr: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let else_label = ctx.next_label("tern_else"); - let end_label = ctx.next_label("tern_end"); - emitter.comment("ternary"); - - let cond_ty = emit_expr(condition, emitter, ctx, data); - coerce_to_truthiness(emitter, ctx, &cond_ty); - - // -- branch based on ternary condition -- - abi::emit_branch_if_int_result_zero(emitter, &else_label); - - let result_ty = infer_branch_result_type(then_expr, else_expr, ctx); - - let then_ty = emit_expr(then_expr, emitter, ctx, data); - coerce_branch_result(emitter, ctx, data, then_expr, &then_ty, &result_ty); - abi::emit_jump(emitter, &end_label); // skip else branch after evaluating then-expr - - emitter.label(&else_label); - let else_ty = emit_expr(else_expr, emitter, ctx, data); - coerce_branch_result(emitter, ctx, data, else_expr, &else_ty, &result_ty); - - emitter.label(&end_label); - result_ty -} - -/// Emits the short ternary / elvis operator (`value ?: default`). -/// -/// Emits `value` and saves the result before testing its truthiness. -/// If truthy, restores the saved value and jumps to `end_label`. Otherwise falls through to emit `default`. -/// Both branches are coerced to a common `result_ty`. Returns the unified result type. -pub(super) fn emit_short_ternary( - value: &Expr, - default: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let default_label = ctx.next_label("short_tern_default"); - let end_label = ctx.next_label("short_tern_end"); - emitter.comment("short ternary"); - - let result_ty = infer_branch_result_type(value, default, ctx); - let value_ty = emit_expr(value, emitter, ctx, data); - abi::emit_push_result_value(emitter, &value_ty); - coerce_to_truthiness(emitter, ctx, &value_ty); - - // -- branch based on the saved left value's truthiness -- - abi::emit_branch_if_int_result_zero(emitter, &default_label); - - pop_saved_result_value(emitter, &value_ty); - coerce_branch_result(emitter, ctx, data, value, &value_ty, &result_ty); - abi::emit_jump(emitter, &end_label); // skip fallback after restoring truthy left value - - emitter.label(&default_label); - discard_saved_result_value(emitter, &value_ty); - let default_ty = emit_expr(default, emitter, ctx, data); - coerce_branch_result(emitter, ctx, data, default, &default_ty, &result_ty); - - emitter.label(&end_label); - result_ty -} - -/// Infers the unified result type for the two ternary branches. -/// -/// Uses a dummy signature to infer the type of each branch via `functions::infer_local_type_with_ctx`. -/// Returns the common type: exact match if equal, `Mixed` if one branch is `Void`, -/// `Str` if either is `Str`, `Float` if either is `Float`, otherwise the left type. -fn infer_branch_result_type(left: &Expr, right: &Expr, ctx: &Context) -> PhpType { - let dummy_sig = FunctionSig { - params: vec![], - defaults: vec![], - return_type: PhpType::Int, - declared_return: false, - by_ref_return: false, - ref_params: vec![], - declared_params: vec![], - variadic: None, - deprecation: None, - }; - let left_ty = functions::infer_local_type_with_ctx(left, &dummy_sig, ctx); - let right_ty = functions::infer_local_type_with_ctx(right, &dummy_sig, ctx); - if left_ty == right_ty { - left_ty - } else if left_ty == PhpType::Void || right_ty == PhpType::Void { - PhpType::Mixed - } else if left_ty == PhpType::Str || right_ty == PhpType::Str { - PhpType::Str - } else if left_ty == PhpType::Float || right_ty == PhpType::Float { - PhpType::Float - } else { - left_ty - } -} - -/// Coerces a branch result from `branch_ty` to `result_ty` in place. -/// -/// No-op if types already match. Handles `Mixed`/`Union` boxing, string coercion, -/// int-to-float promotion, and general type coercion via `coerce_result_to_type`. -/// When the branch expression owns a `Mixed` that is being coerced to a non-Mixed type, -/// preserves the mixed value across coercion and releases it afterward to keep ownership balanced. -fn coerce_branch_result( - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, - branch_expr: &Expr, - branch_ty: &PhpType, - result_ty: &PhpType, -) { - if result_ty == branch_ty { - return; - } - if matches!(result_ty, PhpType::Mixed | PhpType::Union(_)) { - crate::codegen::emit_box_current_value_as_mixed(emitter, branch_ty); - } else if *result_ty == PhpType::Str { - coerce_to_string(emitter, ctx, data, branch_ty); - } else if *result_ty == PhpType::Float && *branch_ty == PhpType::Int { - abi::emit_int_result_to_float_result(emitter); // convert int to float for unified result type - } else if crate::codegen::expr::can_coerce_result_to_type(branch_ty, result_ty) { - let release_mixed_after_coerce = !matches!(result_ty, PhpType::Mixed | PhpType::Union(_)) - && crate::codegen::stmt::helpers::should_release_owned_mixed_after_coerce( - branch_expr, - branch_ty, - result_ty, - ); - if release_mixed_after_coerce { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - } - coerce_result_to_type(emitter, ctx, data, branch_ty, result_ty); - if release_mixed_after_coerce { - crate::codegen::stmt::helpers::release_preserved_mixed_after_coercion( - emitter, - result_ty, - ); - } - } -} - -/// Pops the saved result value from the runtime stack into the appropriate result register(s). -/// -/// Matches on `PhpType::codegen_repr()` to emit the correct pop instruction(s): -/// - Scalar/integer types → pop `int_result_reg` -/// - Float → pop `float_result_reg` -/// - String → pop register pair (ptr + len) -/// - Void/Never → nothing -fn pop_saved_result_value(emitter: &mut Emitter, ty: &PhpType) { - match ty.codegen_repr() { - PhpType::Bool - | PhpType::Int - | PhpType::Resource(_) - | PhpType::Iterable - | PhpType::Mixed - | PhpType::Union(_) - | PhpType::Array(_) - | PhpType::AssocArray { .. } - | PhpType::Buffer(_) - | PhpType::Callable - | PhpType::Object(_) - | PhpType::Packed(_) - | PhpType::Pointer(_) => { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); - } - PhpType::Float => { - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); - } - PhpType::Void | PhpType::Never => {} - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); - } - PhpType::TaggedScalar => { - let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(emitter); - abi::emit_pop_reg_pair(emitter, abi::int_result_reg(emitter), tag_reg); - } - } -} - -/// Discards the saved result value from the runtime stack without materializing it as a result. -/// -/// Simply pops the value off the stack; used when the short-ternary condition is falsy -/// and the saved left value is not needed. -fn discard_saved_result_value(emitter: &mut Emitter, ty: &PhpType) { - pop_saved_result_value(emitter, ty); -} diff --git a/src/codegen/expr/variables.rs b/src/codegen/expr/variables.rs deleted file mode 100644 index 9bf560f021..0000000000 --- a/src/codegen/expr/variables.rs +++ /dev/null @@ -1,468 +0,0 @@ -//! Purpose: -//! Lowers variable reads from locals, globals, static storage, and special compiler-managed slots. -//! Loads values into the standard expression result registers for downstream consumers. -//! -//! Called from: -//! - `crate::codegen::expr::emit_expr()` -//! -//! Key details: -//! - Variable reads must respect slot ownership and static/global symbol storage conventions. - -use crate::codegen::platform::Arch; - -use super::super::abi; -use super::super::context::{Context, HeapOwnership}; -use super::super::data_section::DataSection; -use super::super::emit::Emitter; -use super::{expr_result_heap_ownership, Expr, PhpType}; - -/// Emits code to read a variable by name, dispatching on storage class. -/// -/// Checks for FCC deferred closures, extern globals, global vars, ref params, -/// and local stack slots. Loads the value into the standard expression result -/// register(s) and returns the PHP type. -/// -/// - **FCC closures**: marks the deferred wrapper as needed before dispatching. -/// - **Extern globals**: delegates to `emit_global_load`. -/// - **Global vars**: delegates to `emit_global_load`. -/// - **Ref params**: delegates to `emit_ref_variable`. -/// - **Local slots**: loads from the stack offset via `abi::emit_load`. -pub(super) fn emit_variable(name: &str, emitter: &mut Emitter, ctx: &mut Context) -> PhpType { - // Loading the variable's value as an Expr means the FCC pointer escapes the - // short-circuit path (`emit_closure_call` bypasses this function when it - // short-circuits, so we don't see those reads here). Mark the wrapper as - // needed so the dead-wrapper optimisation emits its full body. - if let Some(label) = ctx.variable_fcc_label.get(name).cloned() { - if let Some(deferred) = ctx.deferred_closures.iter_mut().find(|d| d.label == label) { - deferred.needed = true; - } - } - - if let Some(ty) = ctx.extern_globals.get(name).cloned() { - super::super::stmt::emit_global_load(emitter, ctx, name, &ty); - return ty; - } - - if ctx.global_vars.contains(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)) { - let Some(var) = ctx.variables.get(name) else { - emitter.comment(&format!("WARNING: undefined variable ${}", name)); - return PhpType::Int; - }; - let ty = var.ty.clone(); - super::super::stmt::emit_global_load(emitter, ctx, name, &ty); - return ty; - } - - if ctx.ref_params.contains(name) { - return emit_ref_variable(name, emitter, ctx); - } - - let Some(var) = ctx.variables.get(name) else { - emitter.comment(&format!("WARNING: undefined variable ${}", name)); - return PhpType::Int; - }; - let offset = var.stack_offset; - let ty = var.ty.clone(); - emitter.comment(&format!("load ${}", name)); - abi::emit_load(emitter, &ty, offset); - ty -} - -/// Emits code to throw an exception. -/// -/// Evaluates `inner`, retains borrowed refcounted heap values before publishing -/// them as the active exception, stores the result in `_exc_value`, and calls -/// `__rt_throw_current` to unwind to the nearest handler. Returns `PhpType::Void`. -/// -/// - `inner` is evaluated first (source order preserved). -/// - Retains the value if it is refcounted and not already owned. -/// - Uses `abi::int_result_reg(emitter)` as the temporary register for the value. -pub(super) fn emit_throw( - inner: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let thrown_ty = super::emit_expr(inner, emitter, ctx, data); - if thrown_ty.is_refcounted() && expr_result_heap_ownership(inner) != HeapOwnership::Owned { - abi::emit_incref_if_refcounted(emitter, &thrown_ty); // retain borrowed heap values before publishing them as the active exception - } - abi::emit_store_reg_to_symbol( - emitter, - abi::int_result_reg(emitter), - "_exc_value", - 0, - ); - abi::emit_call_label(emitter, "__rt_throw_current"); // unwind to the nearest active exception handler - PhpType::Void -} - -/// Emits code for a pre-increment operation (`++$name`). -/// -/// Reads the current value, increments it in place, stores back to the original -/// slot, and returns `PhpType::Int`. Supports global vars, ref params, and local -/// stack slots. -/// -/// - **Undefined var**: emits a warning comment and returns `PhpType::Int`. -/// - **Global vars**: loads via `emit_global_load`, increments, stores inline to the global symbol. -/// - **Ref params**: loads the pointer from the stack slot, dereferences, increments, stores back. -/// - **Local slots**: uses `abi::load_at_offset` / `abi::store_at_offset`. -pub(super) fn emit_pre_increment(name: &str, emitter: &mut Emitter, ctx: &mut Context) -> PhpType { - let Some(var) = ctx.variables.get(name) else { - emitter.comment(&format!("WARNING: undefined variable ${}", name)); - return PhpType::Int; - }; - - if ctx.global_vars.contains(name) { - let ty = var.ty.clone(); - emitter.comment(&format!("++${} (global)", name)); - super::super::stmt::emit_global_load(emitter, ctx, name, &ty); - emit_add_one(emitter, abi::int_result_reg(emitter)); - emit_global_store_inline(emitter, name, abi::int_result_reg(emitter)); - return PhpType::Int; - } - - if ctx.ref_params.contains(name) { - let offset = var.stack_offset; - let pointer_reg = abi::symbol_scratch_reg(emitter); - emitter.comment(&format!("++${} (ref)", name)); - abi::load_at_offset(emitter, pointer_reg, offset); - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - emit_add_one(emitter, abi::int_result_reg(emitter)); - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - return PhpType::Int; - } - - let offset = var.stack_offset; - emitter.comment(&format!("++${}", name)); - abi::load_at_offset(emitter, abi::int_result_reg(emitter), offset); - emit_add_one(emitter, abi::int_result_reg(emitter)); - abi::store_at_offset(emitter, abi::int_result_reg(emitter), offset); - if ctx.in_main && ctx.all_global_var_names.contains(name) { - emit_global_store_inline(emitter, name, abi::int_result_reg(emitter)); - } - PhpType::Int -} - -/// Emits code for a post-increment operation (`$name++`). -/// -/// Copies the current value to the result register, increments a scratch copy, -/// stores back to the original slot, and returns the original value as -/// `PhpType::Int`. Supports global vars, ref params, and local stack slots. -/// -/// - **Undefined var**: emits a warning comment and returns `PhpType::Int`. -/// - **Global vars**: loads via `emit_global_load`, copies to scratch, increments scratch, stores inline. -/// - **Ref params**: loads the pointer from the stack slot, dereferences, copies result, increments scratch, stores back. -/// - **Local slots**: uses `abi::load_at_offset` to result reg, copies to scratch, increments scratch, stores back. -pub(super) fn emit_post_increment( - name: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let Some(var) = ctx.variables.get(name) else { - emitter.comment(&format!("WARNING: undefined variable ${}", name)); - return PhpType::Int; - }; - - let result_reg = abi::int_result_reg(emitter); - let scratch_reg = abi::temp_int_reg(emitter.target); - - if ctx.global_vars.contains(name) { - let ty = var.ty.clone(); - emitter.comment(&format!("${}++ (global)", name)); - super::super::stmt::emit_global_load(emitter, ctx, name, &ty); - emit_copy_int_reg(emitter, scratch_reg, result_reg); - emit_add_one(emitter, scratch_reg); - emit_global_store_inline(emitter, name, scratch_reg); - return PhpType::Int; - } - - if ctx.ref_params.contains(name) { - let offset = var.stack_offset; - let pointer_reg = abi::symbol_scratch_reg(emitter); - emitter.comment(&format!("${}++ (ref)", name)); - abi::load_at_offset(emitter, pointer_reg, offset); - abi::emit_load_from_address(emitter, result_reg, pointer_reg, 0); - emit_copy_int_reg(emitter, scratch_reg, result_reg); - emit_add_one(emitter, scratch_reg); - abi::emit_store_to_address(emitter, scratch_reg, pointer_reg, 0); - return PhpType::Int; - } - - let offset = var.stack_offset; - emitter.comment(&format!("${}++", name)); - abi::load_at_offset(emitter, result_reg, offset); - emit_copy_int_reg(emitter, scratch_reg, result_reg); - emit_add_one(emitter, scratch_reg); - abi::store_at_offset(emitter, scratch_reg, offset); - if ctx.in_main && ctx.all_global_var_names.contains(name) { - emit_global_store_inline(emitter, name, scratch_reg); - } - PhpType::Int -} - -/// Emits code for a pre-decrement operation (`--$name`). -/// -/// Reads the current value, decrements it in place, stores back to the original -/// slot, and returns `PhpType::Int`. Supports global vars, ref params, and local -/// stack slots. -/// -/// - **Undefined var**: emits a warning comment and returns `PhpType::Int`. -/// - **Global vars**: loads via `emit_global_load`, decrements, stores inline to the global symbol. -/// - **Ref params**: loads the pointer from the stack slot, dereferences, decrements, stores back. -/// - **Local slots**: uses `abi::load_at_offset` / `abi::store_at_offset`. -pub(super) fn emit_pre_decrement(name: &str, emitter: &mut Emitter, ctx: &mut Context) -> PhpType { - let Some(var) = ctx.variables.get(name) else { - emitter.comment(&format!("WARNING: undefined variable ${}", name)); - return PhpType::Int; - }; - - if ctx.global_vars.contains(name) { - let ty = var.ty.clone(); - emitter.comment(&format!("--${} (global)", name)); - super::super::stmt::emit_global_load(emitter, ctx, name, &ty); - emit_sub_one(emitter, abi::int_result_reg(emitter)); - emit_global_store_inline(emitter, name, abi::int_result_reg(emitter)); - return PhpType::Int; - } - - if ctx.ref_params.contains(name) { - let offset = var.stack_offset; - let pointer_reg = abi::symbol_scratch_reg(emitter); - emitter.comment(&format!("--${} (ref)", name)); - abi::load_at_offset(emitter, pointer_reg, offset); - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - emit_sub_one(emitter, abi::int_result_reg(emitter)); - abi::emit_store_to_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - return PhpType::Int; - } - - let offset = var.stack_offset; - emitter.comment(&format!("--${}", name)); - abi::load_at_offset(emitter, abi::int_result_reg(emitter), offset); - emit_sub_one(emitter, abi::int_result_reg(emitter)); - abi::store_at_offset(emitter, abi::int_result_reg(emitter), offset); - PhpType::Int -} - -/// Emits code for a post-decrement operation (`$name--`). -/// -/// Copies the current value to the result register, decrements a scratch copy, -/// stores back to the original slot, and returns the original value as -/// `PhpType::Int`. Supports ref params and local stack slots. Note: global vars -/// are not handled for post-decrement (no inline global store path). -/// -/// - **Undefined var**: emits a warning comment and returns `PhpType::Int`. -/// - **Ref params**: loads the pointer from the stack slot, dereferences, copies result, decrements scratch, stores back. -/// - **Local slots**: uses `abi::load_at_offset` to result reg, copies to scratch, decrements scratch, stores back. -pub(super) fn emit_post_decrement( - name: &str, - emitter: &mut Emitter, - ctx: &mut Context, -) -> PhpType { - let Some(var) = ctx.variables.get(name) else { - emitter.comment(&format!("WARNING: undefined variable ${}", name)); - return PhpType::Int; - }; - - let result_reg = abi::int_result_reg(emitter); - let scratch_reg = abi::temp_int_reg(emitter.target); - - if ctx.ref_params.contains(name) { - let offset = var.stack_offset; - let pointer_reg = abi::symbol_scratch_reg(emitter); - emitter.comment(&format!("${}-- (ref)", name)); - abi::load_at_offset(emitter, pointer_reg, offset); - abi::emit_load_from_address(emitter, result_reg, pointer_reg, 0); - emit_copy_int_reg(emitter, scratch_reg, result_reg); - emit_sub_one(emitter, scratch_reg); - abi::emit_store_to_address(emitter, scratch_reg, pointer_reg, 0); - return PhpType::Int; - } - - let offset = var.stack_offset; - emitter.comment(&format!("${}--", name)); - abi::load_at_offset(emitter, result_reg, offset); - emit_copy_int_reg(emitter, scratch_reg, result_reg); - emit_sub_one(emitter, scratch_reg); - abi::store_at_offset(emitter, scratch_reg, offset); - PhpType::Int -} - -/// Emits code to read the `$this` variable. -/// -/// Loads `$this` from its stack slot into the integer result register and returns -/// `PhpType::Object(class_name)` where `class_name` is the current class or empty -/// string if outside a class scope. -/// -/// - Emits a warning and returns `PhpType::Int` if `$this` is not in scope. -pub(super) fn emit_this(emitter: &mut Emitter, ctx: &mut Context) -> PhpType { - emitter.comment("$this"); - let var = match ctx.variables.get("this") { - Some(v) => v, - None => { - emitter.comment("WARNING: $this used outside class scope"); - return PhpType::Int; - } - }; - let offset = var.stack_offset; - abi::load_at_offset(emitter, abi::int_result_reg(emitter), offset); - let class_name = ctx.current_class.clone().unwrap_or_default(); - PhpType::Object(class_name) -} - -/// Emits code to read a variable passed by reference (ref param). -/// -/// Loads the pointer stored in the stack slot, then dereferences it and loads -/// the value into the appropriate result register(s) based on type: -/// - `Int`/`Bool` → `abi::int_result_reg` -/// - `Float` → `abi::float_result_reg` -/// - `Str` → `abi::string_result_regs` (pointer in first reg, length in second) -/// - Other types → `abi::int_result_reg` -/// -/// Returns the variable's `PhpType`. -fn emit_ref_variable(name: &str, emitter: &mut Emitter, ctx: &mut Context) -> PhpType { - let Some(var) = ctx.variables.get(name) else { - emitter.comment(&format!("WARNING: undefined variable ${}", name)); - return PhpType::Int; - }; - let offset = var.stack_offset; - let ty = var.ty.clone(); - let pointer_reg = abi::symbol_scratch_reg(emitter); - emitter.comment(&format!("load ref ${}", name)); - abi::load_at_offset(emitter, pointer_reg, offset); - match &ty { - PhpType::Bool | PhpType::Int => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - } - PhpType::Float => { - abi::emit_load_from_address(emitter, abi::float_result_reg(emitter), pointer_reg, 0); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_from_address(emitter, ptr_reg, pointer_reg, 0); - abi::emit_load_from_address(emitter, len_reg, pointer_reg, 8); - } - _ => { - abi::emit_load_from_address(emitter, abi::int_result_reg(emitter), pointer_reg, 0); - } - } - ty -} - -/// Emits a store of `reg` into the global variable symbol `_gvar_{name}`. -/// -/// Uses `abi::emit_store_reg_to_symbol` with offset 0. -fn emit_global_store_inline(emitter: &mut Emitter, name: &str, reg: &str) { - let label = format!("_gvar_{}", name); - abi::emit_store_reg_to_symbol(emitter, reg, &label, 0); -} - -/// Copies the integer value from `src` to `dst` using a target-specific mov instruction. -fn emit_copy_int_reg(emitter: &mut Emitter, dst: &str, src: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov {}, {}", dst, src)); // copy the integer result into a scratch register before mutating it - } - Arch::X86_64 => { - emitter.instruction(&format!("mov {}, {}", dst, src)); // copy the integer result into a scratch register before mutating it - } - } -} - -/// Emits an in-place increment of `reg` by 1 using a target-specific add instruction. -fn emit_add_one(emitter: &mut Emitter, reg: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("add {}, {}, #1", reg, reg)); // increment the integer value in place - } - Arch::X86_64 => { - emitter.instruction(&format!("add {}, 1", reg)); // increment the integer value in place - } - } -} - -/// Emits an in-place decrement of `reg` by 1 using a target-specific sub instruction. -fn emit_sub_one(emitter: &mut Emitter, reg: &str) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("sub {}, {}, #1", reg, reg)); // decrement the integer value in place - } - Arch::X86_64 => { - emitter.instruction(&format!("sub {}, 1", reg)); // decrement the integer value in place - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::codegen::context::Context; - use crate::codegen::platform::{Arch, Platform, Target}; - use crate::parser::ast::{Expr, ExprKind}; - - /// Verifies emitter x86. - fn test_emitter_x86() -> Emitter { - Emitter::new(Target::new(Platform::Linux, Arch::X86_64)) - } - - /// Verifies emit ref variable linux x86_64 uses native indirect loads. - #[test] - fn test_emit_ref_variable_linux_x86_64_uses_native_indirect_loads() { - let mut emitter = test_emitter_x86(); - let mut ctx = Context::new(); - ctx.alloc_var("value", PhpType::Int); - ctx.ref_params.insert("value".into()); - - let ty = emit_variable("value", &mut emitter, &mut ctx); - let out = emitter.output(); - - assert_eq!(ty, PhpType::Int); - assert!(out.contains(" mov r11, QWORD PTR [rbp - 8]\n")); - assert!(out.contains(" mov rax, QWORD PTR [r11]\n")); - } - - /// Verifies emit local pre and post increment linux x86_64 use native registers. - #[test] - fn test_emit_local_pre_and_post_increment_linux_x86_64_use_native_registers() { - let mut emitter = test_emitter_x86(); - let mut ctx = Context::new(); - ctx.alloc_var("value", PhpType::Int); - - emit_pre_increment("value", &mut emitter, &mut ctx); - emit_post_increment("value", &mut emitter, &mut ctx); - - let out = emitter.output(); - assert!(out.contains(" mov rax, QWORD PTR [rbp - 8]\n")); - assert!(out.contains(" add rax, 1\n")); - assert!(out.contains(" mov QWORD PTR [rbp - 8], rax\n")); - assert!(out.contains(" mov r10, rax\n")); - assert!(out.contains(" add r10, 1\n")); - assert!(out.contains(" mov QWORD PTR [rbp - 8], r10\n")); - } - - /// Verifies emit throw linux x86_64 uses native result register. - #[test] - fn test_emit_throw_linux_x86_64_uses_native_result_register() { - let mut emitter = test_emitter_x86(); - let mut ctx = Context::new(); - let mut data = DataSection::new(); - let expr = Expr::new(ExprKind::Throw(Box::new(Expr::int_lit(7))), crate::span::Span::dummy()); - - let ty = emit_throw( - match &expr.kind { - ExprKind::Throw(inner) => inner, - _ => unreachable!(), - }, - &mut emitter, - &mut ctx, - &mut data, - ); - - let out = emitter.output(); - assert_eq!(ty, PhpType::Void); - assert!(out.contains(" mov rax, 7\n")); - assert!(out.contains(" mov QWORD PTR [rip + _exc_value], rax\n")); - assert!(out.contains(" call __rt_throw_current\n")); - } -} diff --git a/src/codegen/ffi.rs b/src/codegen/ffi.rs deleted file mode 100644 index 2126aaf068..0000000000 --- a/src/codegen/ffi.rs +++ /dev/null @@ -1,612 +0,0 @@ -//! Purpose: -//! Lowers extern declarations and calls into target ABI-compatible assembly boundaries. -//! Handles C-facing symbols, argument movement, return values, and required library metadata. -//! -//! Called from: -//! - `crate::codegen::generate()` and extern call expression lowering -//! -//! Key details: -//! - Extern lowering follows platform ABI rules and must not use PHP call normalization for C-only details. - -use crate::codegen::abi; -use crate::codegen::builtins::callable_lookup::{lookup_function, FunctionLookup}; -use crate::codegen::context::{ - Context, DeferredExternCallbackTrampoline, HeapOwnership, -}; -use crate::codegen::data_section::DataSection; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::{can_coerce_result_to_type, coerce_result_to_type, emit_expr}; -use crate::codegen::platform::Arch; -use crate::names::function_symbol; -use crate::parser::ast::{BinOp, Expr, ExprKind}; -use crate::span::Span; -use crate::types::{FunctionSig, PhpType}; - -/// Lowers an extern (FFI) call into target C ABI. -/// Handles argument preevaluation, cleanup slot reservation, string conversion, -/// register allocation, the foreign call, and borrowed-string cleanup after the call. -/// Returns the PHP return type derived from the extern function's signature. -pub fn emit_extern_call( - name: &str, - args: &[Expr], - call_span: Span, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - let sig = ctx - .extern_functions - .get(name) - .cloned() - .unwrap_or_else(|| panic!("codegen bug: extern function '{}' not found", name)); - let call_sig = ctx - .functions - .get(name) - .cloned() - .unwrap_or_else(|| 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, - }); - let regular_param_count = - crate::codegen::expr::calls::args::regular_param_count(Some(&call_sig), args.len()); - let normalized = if crate::codegen::expr::calls::args::has_named_args(args) { - crate::codegen::expr::calls::args::preevaluate_named_call_args_to_temps( - &call_sig, - args, - call_span, - regular_param_count, - false, - emitter, - ctx, - data, - ) - } else { - crate::codegen::expr::calls::args::normalize_named_call_args_with_checks( - &call_sig, - args, - regular_param_count, - ) - }; - crate::codegen::expr::calls::args::emit_spread_length_checks( - &normalized.spread_length_checks, - emitter, - ctx, - data, - ); - let normalized_args = normalized.args; - let args = normalized_args.as_slice(); - - emitter.comment(&format!("extern call: {}()", name)); - - let string_arg_count = sig - .params - .iter() - .take(args.len()) - .filter(|(_, ty)| *ty == PhpType::Str) - .count(); - let cleanup_bytes = string_arg_count * 16; - - let source_temp_types = preevaluate_extern_args(args, &sig, emitter, ctx, data); - let source_temp_bytes = pushed_temp_bytes(&source_temp_types); - - if cleanup_bytes > 0 { - abi::emit_reserve_temporary_stack(emitter, cleanup_bytes); // reserve per-call cleanup slots for borrowed C-string temporaries - } - - // -- push already-evaluated arguments onto the C ABI stack in reverse order -- - let mut final_pushed_bytes = 0usize; - for (i, _) in args.iter().enumerate().rev() { - let param_ty = sig - .params - .get(i) - .map(|(_, t)| t.clone()) - .unwrap_or(PhpType::Int); - let actual_ty = load_extern_source_temp_to_result( - i, - &source_temp_types, - cleanup_bytes + final_pushed_bytes, - emitter, - ); - - if param_ty == PhpType::Float && actual_ty != PhpType::Float { - emit_widen_int_like_to_float(emitter); // widen integer-like value to C double in the native return register - } else if matches!(param_ty, PhpType::Pointer(_)) && actual_ty == PhpType::Void { - emit_zero_int_result(emitter); // PHP null becomes a null pointer for C - } - - // Convert elephc string (x1, x2) to a dedicated null-terminated C string (x0) - if param_ty == PhpType::Str && actual_ty == PhpType::Str { - abi::emit_call_label(emitter, "__rt_str_to_cstr"); // allocate a null-terminated copy for the foreign C ABI - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // push the returned C string pointer onto the temporary arg stack - } else if param_ty == PhpType::Float { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // push the floating-point argument onto the temporary arg stack - } else { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // push the integer or pointer argument onto the temporary arg stack - } - final_pushed_bytes += 16; - } - - // -- pop arguments into registers (C ABI: x0-x7, d0-d7) -- - let mut int_reg = 0usize; - let mut float_reg = 0usize; - let mut cleanup_idx = 0usize; - let temp_arg_bytes = args.len() * 16; - let cleanup_base_reg = abi::temp_int_reg(emitter.target); - if cleanup_bytes > 0 { - abi::emit_temporary_stack_address(emitter, cleanup_base_reg, temp_arg_bytes); // compute the base address of the borrowed C-string cleanup slots above the temporary arg stack - } - for (i, _) in args.iter().enumerate() { - let param_ty = sig - .params - .get(i) - .map(|(_, t)| t.clone()) - .unwrap_or(PhpType::Int); - if param_ty == PhpType::Float { - abi::emit_pop_float_reg(emitter, float_abi_arg_reg(emitter, float_reg)); // pop the floating-point argument into the next ABI float register - float_reg += 1; - } else { - // String args were already converted to char* (single x register) - let arg_reg = int_abi_arg_reg(emitter, int_reg); - abi::emit_pop_reg(emitter, arg_reg); // pop the integer, pointer, or converted C-string argument into the next ABI int register - if param_ty == PhpType::Str { - abi::emit_store_to_address(emitter, arg_reg, cleanup_base_reg, cleanup_idx * 16); // record the borrowed C-string pointer so it can be freed after the foreign call - cleanup_idx += 1; - } - int_reg += 1; - } - } - - // -- call the C function -- - crate::codegen::expr::save_concat_offset_before_nested_call(emitter, ctx); - let c_sym = emitter.target.extern_symbol(name); - abi::emit_call_label(emitter, &c_sym); // call the extern C function symbol through the target-aware direct-call helper - if sig.return_type == PhpType::Int { - emit_sign_extend_i32_result(emitter); // sign-extend 32-bit C int returns before PHP comparisons use the native integer result register - } - let nested_return_ty = if sig.return_type == PhpType::Str { - PhpType::Pointer(None) - } else { - sig.return_type.clone() - }; - crate::codegen::expr::restore_concat_offset_after_nested_call(emitter, ctx, &nested_return_ty); - - // -- handle return value -- - if sig.return_type == PhpType::Str { - // C returned char* in x0 — convert to owned elephc string (x1, x2) - abi::emit_call_label(emitter, "__rt_cstr_to_str"); // convert the returned C string into the elephc string result convention - } - - if cleanup_bytes > 0 { - // -- preserve the extern return value while borrowed C-string temps are released -- - let saved_return_bytes = push_ffi_return_value(emitter, &sig.return_type); - - // -- borrowed C-string arguments are call-scoped and freed immediately after the call -- - for idx in 0..string_arg_count { - abi::emit_load_temporary_stack_slot( - emitter, - abi::int_result_reg(emitter), - saved_return_bytes + idx * 16, - ); // reload one borrowed temporary C-string pointer from the cleanup area - abi::emit_call_label(emitter, "__rt_heap_free"); // release the call-scoped C-string copy after the extern call returns - } - - pop_ffi_return_value(emitter, &sig.return_type); - abi::emit_release_temporary_stack(emitter, cleanup_bytes); // release the borrowed C-string cleanup area after all temporaries are freed - } - abi::emit_release_temporary_stack(emitter, source_temp_bytes); // release source-order extern argument temporaries after the call - - sig.return_type -} - -/// Evaluates all extern call arguments in source order before the C ABI stack is set up. -/// Emits each argument expression, coerces it to the target parameter type, and pushes the result -/// onto a temporary stack. Returns a vector of the emitted PHP types for each argument (after coercion). -/// Callable arguments are resolved to symbol addresses at emit time. -fn preevaluate_extern_args( - args: &[Expr], - sig: &crate::types::ExternFunctionSig, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> Vec { - let mut source_temp_types = Vec::new(); - for (i, arg) in args.iter().enumerate() { - let param_ty = sig - .params - .get(i) - .map(|(_, t)| t.clone()) - .unwrap_or(PhpType::Int); - let mut actual_ty = if param_ty == PhpType::Callable { - emit_extern_callable_arg(arg, emitter, ctx, data) - } else { - emit_expr(arg, emitter, ctx, data) - }; - if can_coerce_result_to_type(&actual_ty, ¶m_ty) { - if should_release_owned_mixed_after_extern_arg_coerce(arg, &actual_ty, ¶m_ty) { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the owned Mixed argument while coercing it to the extern parameter type - coerce_result_to_type(emitter, ctx, data, &actual_ty, ¶m_ty); - crate::codegen::expr::calls::args::release_preserved_mixed_after_arg_coercion( - emitter, - ¶m_ty, - ); - } else { - coerce_result_to_type(emitter, ctx, data, &actual_ty, ¶m_ty); - } - actual_ty = param_ty.codegen_repr(); - } - if !matches!(actual_ty, PhpType::Void | PhpType::Never) { - abi::emit_push_result_value(emitter, &actual_ty); - } - source_temp_types.push(actual_ty); - } - source_temp_types -} - -/// Materializes an extern `callable` argument as a raw C function pointer. -/// -/// String literals still lower to direct user-function symbols. Descriptor-backed -/// callables lower to a generated C-ABI trampoline that reloads the selected -/// descriptor from global storage, preserving closure captures and receivers for -/// C APIs that only accept a plain function pointer. -fn emit_extern_callable_arg( - arg: &Expr, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) -> PhpType { - match &arg.kind { - ExprKind::StringLiteral(func_name) => { - let resolved_name = match lookup_function(ctx, func_name) { - Some(FunctionLookup::UserFunction(name)) - | Some(FunctionLookup::IncludeVariant(name)) => name, - _ => func_name.clone(), - }; - let label = function_symbol(&resolved_name); - abi::emit_symbol_address(emitter, abi::int_result_reg(emitter), &label); // materialize the callback target address in the integer result register - } - _ => { - let precomputed_sig = crate::codegen::callables::callable_sig(arg, ctx); - let actual_ty = emit_expr(arg, emitter, ctx, data); - debug_assert_eq!(actual_ty, PhpType::Callable); - let Some(callback_sig) = precomputed_sig.or_else(|| inline_callable_sig_after_emit(arg, ctx)) else { - crate::codegen::callable_descriptor::emit_load_entry_from_descriptor( - emitter, - abi::int_result_reg(emitter), - abi::int_result_reg(emitter), - ); - return PhpType::Callable; - }; - emit_stateful_extern_callback_trampoline(arg, &callback_sig, emitter, ctx, data); - } - } - PhpType::Callable -} - -/// Returns the signature for an inline callable after its descriptor was emitted. -fn inline_callable_sig_after_emit(arg: &Expr, ctx: &Context) -> Option { - match &arg.kind { - ExprKind::Closure { .. } => ctx.deferred_closures.last().map(|closure| closure.sig.clone()), - ExprKind::Assignment { value, .. } => inline_callable_sig_after_emit(value, ctx), - _ => None, - } -} - -/// Stores the current descriptor in a global slot and returns a trampoline address. -fn emit_stateful_extern_callback_trampoline( - arg: &Expr, - callback_sig: &FunctionSig, - emitter: &mut Emitter, - ctx: &mut Context, - data: &mut DataSection, -) { - let slot_label = data.add_comm(ctx.next_label("extern_callback_descriptor"), 8); - let trampoline_label = ctx.next_label("extern_callback_trampoline"); - ctx.deferred_extern_callback_trampolines - .push(DeferredExternCallbackTrampoline { - label: trampoline_label.clone(), - descriptor_slot_label: slot_label.clone(), - visible_arg_types: callback_sig - .params - .iter() - .map(|(_, ty)| ty.codegen_repr()) - .collect(), - return_type: callback_sig.return_type.codegen_repr(), - }); - - emitter.comment("extern callback: bind descriptor trampoline"); - if expr_result_needs_retain_for_extern_callback_slot(arg) { - crate::codegen::callable_descriptor::emit_retain_current_descriptor(emitter); - } - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the new extern callback descriptor while replacing the slot owner - abi::emit_load_symbol_to_reg(emitter, abi::int_result_reg(emitter), &slot_label, 0); - crate::codegen::callable_descriptor::emit_release_current_descriptor(emitter); - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the descriptor that will back the C callback trampoline - abi::emit_store_reg_to_symbol(emitter, abi::int_result_reg(emitter), &slot_label, 0); - abi::emit_symbol_address(emitter, abi::int_result_reg(emitter), &trampoline_label); -} - -/// Returns whether the current descriptor result must be retained for global storage. -fn expr_result_needs_retain_for_extern_callback_slot(arg: &Expr) -> bool { - !matches!( - crate::codegen::expr::expr_result_heap_ownership(arg), - HeapOwnership::Owned - ) -} - -/// Determines whether an owned `Mixed` or `Union` source value must be preserved on the stack -/// while coercing it to a non-Mixed extern parameter type, to avoid leaking the heap value. -/// Returns true when the argument is owned and not itself being coerced to a reference type. -fn should_release_owned_mixed_after_extern_arg_coerce( - arg: &Expr, - source_ty: &PhpType, - target_ty: &PhpType, -) -> bool { - let source_repr = source_ty.codegen_repr(); - let target_repr = target_ty.codegen_repr(); - matches!(source_repr, PhpType::Mixed | PhpType::Union(_)) - && !matches!(target_repr, PhpType::Mixed | PhpType::Union(_)) - && (crate::codegen::expr::expr_result_heap_ownership(arg) == HeapOwnership::Owned - || matches!( - arg.kind, - ExprKind::BinaryOp { - op: BinOp::Add | BinOp::Sub | BinOp::Mul, - .. - } - )) -} - -/// Returns the stack slot size for a PHP type used as an extern argument or return value. -/// `Void` and `Never` types occupy 0 bytes; all other types occupy 16 bytes (one slot). -fn temp_slot_size(ty: &PhpType) -> usize { - if matches!(ty, PhpType::Void | PhpType::Never) { - 0 - } else { - 16 - } -} - -/// Computes the total bytes occupied by all extern argument temporaries on the stack, -/// using `temp_slot_size` for each type. -fn pushed_temp_bytes(types: &[PhpType]) -> usize { - types.iter().map(temp_slot_size).sum() -} - -/// Computes the byte offset of each extern argument temporary from the top of the stack, -/// iterating in reverse order so later arguments have higher offsets (matching the C call convention). -fn temp_offsets(types: &[PhpType]) -> Vec { - let mut offsets = vec![0usize; types.len()]; - let mut running = 0usize; - for idx in (0..types.len()).rev() { - offsets[idx] = running; - running += temp_slot_size(&types[idx]); - } - offsets -} - -/// Computes the absolute stack byte offset for a given extern argument temporary index, -/// adding `extra_bytes` to account for cleanup slots reserved below the argument area. -fn source_temp_offset(source_temp_types: &[PhpType], temp_idx: usize, extra_bytes: usize) -> usize { - extra_bytes + temp_offsets(source_temp_types)[temp_idx] -} - -/// Loads an extern argument temporary from the stack into the appropriate result register(s) -/// based on its type: float to `d0`, string to `(x1, x2)`, scalar/pointer to `x0`. -/// `extra_bytes` accounts for any cleanup slots positioned below the argument area on the stack. -/// Returns the PHP type of the loaded value. -fn load_extern_source_temp_to_result( - temp_idx: usize, - source_temp_types: &[PhpType], - extra_bytes: usize, - emitter: &mut Emitter, -) -> PhpType { - let ty = source_temp_types[temp_idx].clone(); - let offset = source_temp_offset(source_temp_types, temp_idx, extra_bytes); - match ty.codegen_repr() { - PhpType::Float => { - abi::emit_load_temporary_stack_slot(emitter, abi::float_result_reg(emitter), offset); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_load_temporary_stack_slot(emitter, ptr_reg, offset); - abi::emit_load_temporary_stack_slot(emitter, len_reg, offset + 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), offset); - } - } - ty -} - -/// Returns the name of the Nth integer/pointer argument register for the current target's C ABI. -/// ARM64: x0–x7; x86_64: rdi, rsi, rdx, rcx, r8, r9. -fn int_abi_arg_reg(emitter: &Emitter, idx: usize) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"][idx], - Arch::X86_64 => ["rdi", "rsi", "rdx", "rcx", "r8", "r9"][idx], - } -} - -/// Returns the name of the Nth floating-point argument register for the current target's C ABI. -/// ARM64: d0–d7; x86_64: xmm0–xmm7. -fn float_abi_arg_reg(emitter: &Emitter, idx: usize) -> &'static str { - match emitter.target.arch { - Arch::AArch64 => ["d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7"][idx], - Arch::X86_64 => ["xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7"][idx], - } -} - -/// Widens an integer-like result (in the integer result register) to a C double in the -/// floating-point result register. Used when a PHP integer is passed to a C float parameter. -fn emit_widen_int_like_to_float(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("scvtf d0, x0"); // widen the integer-like result in x0 into the floating-point result register - } - Arch::X86_64 => { - emitter.instruction("cvtsi2sd xmm0, rax"); // widen the integer-like result in rax into the floating-point result register - } - } -} - -/// Emits a zero literal into the integer result register to represent a null pointer -/// when a PHP `Void` value (null) is passed to a C `Pointer` parameter. -fn emit_zero_int_result(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // materialize a null C pointer in the integer result register - } - Arch::X86_64 => { - emitter.instruction("mov rax, 0"); // materialize a null C pointer in the integer result register - } - } -} - -/// Sign-extends a 32-bit C integer return value into the native integer register. -/// Required so PHP comparisons use the full 64-bit result after a C `int` return. -fn emit_sign_extend_i32_result(emitter: &mut Emitter) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("sxtw x0, w0"); // sign-extend the 32-bit C integer return into the 64-bit result register - } - Arch::X86_64 => { - emitter.instruction("movsxd rax, eax"); // sign-extend the 32-bit C integer return into the 64-bit result register - } - } -} - -/// Pushes the current FFI return value (in result registers) onto the temporary stack to -/// preserve it while borrowed C-string temporaries are cleaned up. Returns the number of bytes pushed (0 for void). -fn push_ffi_return_value(emitter: &mut Emitter, ty: &PhpType) -> usize { - match ty { - PhpType::Void => 0, - PhpType::Float => { - abi::emit_push_float_reg(emitter, abi::float_result_reg(emitter)); // preserve the floating-point return value while borrowed C-string temporaries are freed - 16 - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // preserve the string return register pair while borrowed C-string temporaries are freed - 16 - } - _ => { - abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the scalar or pointer return value while borrowed C-string temporaries are freed - 16 - } - } -} - -/// Pops a previously preserved FFI return value off the temporary stack back into the -/// result registers. No-op for void return type. -fn pop_ffi_return_value(emitter: &mut Emitter, ty: &PhpType) { - match ty { - PhpType::Void => {} - PhpType::Float => { - abi::emit_pop_float_reg(emitter, abi::float_result_reg(emitter)); // restore the floating-point return value after borrowed C-string cleanup - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::emit_pop_reg_pair(emitter, ptr_reg, len_reg); // restore the string return register pair after borrowed C-string cleanup - } - _ => { - abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the scalar or pointer return value after borrowed C-string cleanup - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::codegen::context::Context; - use crate::codegen::platform::{Arch, Platform, Target}; - use crate::parser::ast::Expr; - use crate::types::ExternFunctionSig; - - /// Builds a Linux x86_64 emitter for FFI unit tests. - fn test_emitter_x86() -> Emitter { - Emitter::new(Target::new(Platform::Linux, Arch::X86_64)) - } - - /// Verifies Linux x86_64 extern calls use native call lowering and sign-extend - /// 32-bit integer returns. - #[test] - fn test_emit_extern_call_linux_x86_64_uses_native_call_and_sign_extend() { - let mut emitter = test_emitter_x86(); - let mut ctx = Context::new(); - let mut data = DataSection::new(); - ctx.extern_functions.insert( - "abs".into(), - ExternFunctionSig { - name: "abs".into(), - params: vec![("n".into(), PhpType::Int)], - return_type: PhpType::Int, - library: None, - }, - ); - - let ret_ty = emit_extern_call( - "abs", - &[Expr::int_lit(-42)], - Span::dummy(), - &mut emitter, - &mut ctx, - &mut data, - ); - let out = emitter.output(); - - assert_eq!(ret_ty, PhpType::Int); - assert!(out.contains(" mov rax, -42\n")); - assert!(out.contains(" sub rsp, 16\n")); - assert!(out.contains(" mov QWORD PTR [rsp], rax\n")); - assert!(out.contains(" mov rdi, QWORD PTR [rsp]\n")); - assert!(out.contains(" call abs\n")); - assert!(out.contains(" movsxd rax, eax\n")); - } - - /// Verifies Linux x86_64 extern calls with string arguments reserve cleanup - /// stack space for borrowed C string temporaries. - #[test] - fn test_emit_extern_call_linux_x86_64_string_args_use_cleanup_stack() { - let mut emitter = test_emitter_x86(); - let mut ctx = Context::new(); - let mut data = DataSection::new(); - ctx.extern_functions.insert( - "strlen".into(), - ExternFunctionSig { - name: "strlen".into(), - params: vec![("s".into(), PhpType::Str)], - return_type: PhpType::Int, - library: None, - }, - ); - - let ret_ty = emit_extern_call( - "strlen", - &[Expr::string_lit("hello")], - Span::dummy(), - &mut emitter, - &mut ctx, - &mut data, - ); - let out = emitter.output(); - - assert_eq!(ret_ty, PhpType::Int); - assert!(out.contains(" sub rsp, 16\n")); - assert!(out.contains(" call __rt_str_to_cstr\n")); - assert!(out.contains(" lea r10, [rsp + 16]\n")); - assert!(out.contains(" mov QWORD PTR [r10], rdi\n")); - assert!(out.contains(" call strlen\n")); - assert!(out.contains(" movsxd rax, eax\n")); - assert!(out.contains(" mov QWORD PTR [rsp], rax\n")); - assert!(out.contains(" mov rax, QWORD PTR [rsp + 16]\n")); - assert!(out.contains(" call __rt_heap_free\n")); - assert!(out.contains(" add rsp, 16\n")); - } -} diff --git a/src/codegen/fiber_sigs.rs b/src/codegen/fiber_sigs.rs deleted file mode 100644 index f47f3d7826..0000000000 --- a/src/codegen/fiber_sigs.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Purpose: -//! Tracks statically known Fiber callback parameter names for `Fiber::start()`. -//! Supplies start-call signatures so associative spreads can be reordered before -//! arguments are stored in the Fiber runtime object. -//! -//! Called from: -//! - `crate::codegen::generate_user_asm()` and Fiber method-call lowering. -//! -//! Key details: -//! - Start arguments are always boxed as `Mixed`; only parameter names/defaults -//! are borrowed from the callback signature for named/spread normalization. - -use std::collections::HashMap; - -use crate::codegen::context::Context; -use crate::names::php_symbol_key; -use crate::parser::ast::{Expr, ExprKind, Program, Stmt, StmtKind}; -use crate::types::{FunctionSig, PhpType}; - -/// Collects functions whose body directly returns `new Fiber()`. -pub(crate) fn collect_fiber_return_sigs(program: &Program) -> HashMap { - let mut sigs = HashMap::new(); - collect_fiber_return_sigs_from_stmts(program, &mut sigs); - sigs -} - -/// Returns the known Fiber callback start signature associated with an expression. -pub(crate) fn fiber_start_sig_for_expr(expr: &Expr, ctx: &Context) -> Option { - match &expr.kind { - ExprKind::Variable(name) => ctx.fiber_start_sigs.get(name).cloned(), - ExprKind::FunctionCall { name, .. } => ctx.fiber_return_sigs.get(name.as_str()).cloned(), - ExprKind::NewObject { .. } => fiber_start_sig_from_new_object(expr, ctx), - _ => None, - } -} - -/// Returns the Fiber callback start signature for a `new Fiber(...)` expression. -pub(crate) fn fiber_start_sig_from_new_object(expr: &Expr, ctx: &Context) -> Option { - let ExprKind::NewObject { class_name, args } = &expr.kind else { - return None; - }; - if php_symbol_key(class_name.as_str()) != php_symbol_key("Fiber") { - return None; - } - let callback = args.first()?; - fiber_start_sig_from_callable_expr(callback, ctx) -} - -/// Recursively scans statements for function declarations with direct Fiber returns. -fn collect_fiber_return_sigs_from_stmts( - stmts: &[Stmt], - sigs: &mut HashMap, -) { - let ctx = Context::new(); - for stmt in stmts { - match &stmt.kind { - StmtKind::FunctionDecl { name, body, .. } => { - if let Some(sig) = fiber_return_sig_from_body(body, &ctx) { - sigs.insert(name.clone(), sig); - } - } - StmtKind::NamespaceBlock { body, .. } | StmtKind::Synthetic(body) => { - collect_fiber_return_sigs_from_stmts(body, sigs); - } - _ => {} - } - } -} - -/// Finds a direct `return new Fiber()` in a function body. -fn fiber_return_sig_from_body(body: &[Stmt], ctx: &Context) -> Option { - for stmt in body { - match &stmt.kind { - StmtKind::Return(Some(expr)) => { - if let Some(sig) = fiber_start_sig_from_new_object(expr, ctx) { - return Some(sig); - } - } - StmtKind::Synthetic(body) => { - if let Some(sig) = fiber_return_sig_from_body(body, ctx) { - return Some(sig); - } - } - _ => {} - } - } - None -} - -/// Builds a Fiber start-call signature from a supported callable expression. -fn fiber_start_sig_from_callable_expr(callback: &Expr, ctx: &Context) -> Option { - match &callback.kind { - ExprKind::Closure { - params, variadic, .. - } => fiber_start_sig_from_closure_params(params, variadic.as_ref()), - ExprKind::Variable(name) => ctx - .closure_sigs - .get(name) - .and_then(fiber_start_sig_from_callback_sig), - ExprKind::FirstClassCallable(target) => { - crate::codegen::expr::calls::first_class_callable_sig(target, ctx) - .and_then(|sig| fiber_start_sig_from_callback_sig(&sig)) - } - _ => crate::codegen::callables::callable_sig(callback, ctx) - .and_then(|sig| fiber_start_sig_from_callback_sig(&sig)), - } -} - -/// Builds a start-call signature from closure parameter syntax. -fn fiber_start_sig_from_closure_params( - params: &[(String, Option, Option, bool)], - variadic: Option<&String>, -) -> Option { - if variadic.is_some() { - return None; - } - Some(FunctionSig { - params: params - .iter() - .map(|(name, _, _, _)| (name.clone(), PhpType::Mixed)) - .collect(), - defaults: params - .iter() - .map(|(_, _, default, _)| default.clone()) - .collect(), - return_type: PhpType::Mixed, - declared_return: false, - by_ref_return: false, - ref_params: params.iter().map(|(_, _, _, is_ref)| *is_ref).collect(), - declared_params: vec![false; params.len()], - variadic: None, - deprecation: None, - }) -} - -/// Converts a known callback signature into the synthetic `Fiber::start()` view. -fn fiber_start_sig_from_callback_sig(sig: &FunctionSig) -> Option { - if sig.variadic.is_some() { - return None; - } - Some(FunctionSig { - params: sig - .params - .iter() - .map(|(name, _)| (name.clone(), PhpType::Mixed)) - .collect(), - defaults: sig.defaults.clone(), - return_type: PhpType::Mixed, - declared_return: false, - by_ref_return: false, - ref_params: sig.ref_params.clone(), - declared_params: vec![false; sig.params.len()], - variadic: None, - deprecation: None, - }) -} diff --git a/src/codegen_ir/fibers.rs b/src/codegen/fibers.rs similarity index 98% rename from src/codegen_ir/fibers.rs rename to src/codegen/fibers.rs index 3da89ffedd..6e5741c398 100644 --- a/src/codegen_ir/fibers.rs +++ b/src/codegen/fibers.rs @@ -3,8 +3,8 @@ //! Keeps wrapper label selection shared between wrapper emission and `new Fiber` lowering. //! //! Called from: -//! - `crate::codegen_ir::block_emit` when emitting deferred wrapper functions. -//! - `crate::codegen_ir::lower_inst::objects` when lowering Fiber construction. +//! - `crate::codegen::block_emit` when emitting deferred wrapper functions. +//! - `crate::codegen::lower_inst::objects` when lowering Fiber construction. //! //! Key details: //! - Closure wrappers are per closure signature because they adapt boxed Fiber diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs new file mode 100644 index 0000000000..08b23438d3 --- /dev/null +++ b/src/codegen/frame.rs @@ -0,0 +1,997 @@ +//! Purpose: +//! Computes and emits stack-frame setup/teardown for the EIR backend. +//! Reuses the target-aware ABI frame helpers shared by the assembly emitter. +//! +//! Called from: +//! - `crate::codegen::block_emit`. +//! +//! Key details: +//! - Frame size is value-placement bytes plus the target frame footer, rounded to 16 bytes. +//! - Main currently exits through the process syscall used by normal executable output. +//! - Each frame stores the inherited concat-buffer offset so statement resets do not clobber +//! `_concat_buf` slices that were passed in by the caller. + +use std::collections::{HashMap, HashSet}; + +use crate::codegen::abi; +use crate::codegen::platform::{Arch, Target}; +use crate::codegen::{ + emit_box_current_value_as_mixed, emit_write_current_string_stderr, emit_write_literal_stderr, +}; +use crate::codegen_support::try_handlers::TRY_HANDLER_SLOT_SIZE; +use crate::ir::{Function, Immediate, LocalKind, LocalSlotId, Op, ValueDef, ValueId}; +use crate::ir_passes::{allocate_registers, Allocation}; +use crate::names::ir_global_symbol; +use crate::types::PhpType; + +use super::context::FunctionContext; +use super::value_placement::{self, ValuePlacement}; + +const FRAME_FOOTER_BYTES: usize = 16; + +/// Symbol name for the C-callable `--web` top-level handler. +/// +/// Emitted as a global label on the handler body and referenced by the +/// process-entry stub when it materializes the handler address for +/// `elephc_web_run`. Keeping it as one constant guarantees the label and the +/// reference never drift. +const WEB_HANDLER_SYMBOL: &str = "_elephc_web_handler"; + +/// Complete fixed frame layout for spill slots, addressable locals, and the +/// callee-saved registers the register allocator decided to use. +pub(super) struct FrameLayout { + pub(super) value_placement: ValuePlacement, + pub(super) local_offsets: HashMap, + pub(super) try_handler_offsets: HashMap, + pub(super) concat_base_offset: usize, + pub(super) frame_size: usize, + pub(super) allocation: Allocation, + pub(super) callee_saved_offsets: Vec<(&'static str, usize)>, +} + +/// Computes the register allocation and fixed stack slots for a function. +/// +/// Every SSA value keeps a spill slot (register-allocated values simply leave +/// theirs unused), and each callee-saved register the allocator uses gets a +/// dedicated save slot so the prologue/epilogue can preserve it. When +/// `regalloc_linear` is false the allocation is all-spilled, reproducing the +/// original stack-only behavior. +pub(super) fn layout_for_function( + function: &Function, + target: Target, + regalloc_linear: bool, +) -> FrameLayout { + let allocation = if regalloc_linear { + allocate_registers(function, target) + } else { + Allocation::all_spilled() + }; + + let value_placement = value_placement::allocate(function); + let mut local_offsets = HashMap::new(); + let mut offset = value_placement.total_slot_bytes; + for local in &function.locals { + let bytes = value_placement::bytes_for(local.ir_type) + .max(local.php_type.codegen_repr().stack_size()); + if bytes == 0 { + continue; + } + offset += bytes; + local_offsets.insert(local.id, offset); + } + let mut try_handler_offsets = HashMap::new(); + for token in try_handler_tokens(function) { + offset += TRY_HANDLER_SLOT_SIZE; + try_handler_offsets.insert(token, offset); + } + let mut callee_saved_offsets = Vec::new(); + for reg in allocation.used_callee_saved() { + offset += 8; + callee_saved_offsets.push((*reg, offset)); + } + offset += 8; + let concat_base_offset = offset; + let frame_size = align_to_16(offset + FRAME_FOOTER_BYTES); + FrameLayout { + value_placement, + local_offsets, + try_handler_offsets, + concat_base_offset, + frame_size, + allocation, + callee_saved_offsets, + } +} + +/// Saves the callee-saved registers the allocator used into their reserved +/// frame slots, preserving the caller's values for the function's lifetime. +fn emit_callee_saved_saves(ctx: &mut FunctionContext<'_>) { + if ctx.callee_saved_offsets.is_empty() { + return; + } + ctx.emitter + .comment("save callee-saved registers used by the register allocator"); + for (reg, offset) in ctx.callee_saved_offsets.clone() { + abi::store_at_offset(ctx.emitter, reg, offset); + } +} + +/// Restores the callee-saved registers saved by `emit_callee_saved_saves`, +/// returning the caller's values before frame teardown. +fn emit_callee_saved_restores(ctx: &mut FunctionContext<'_>) { + if ctx.callee_saved_offsets.is_empty() { + return; + } + ctx.emitter + .comment("restore callee-saved registers used by the register allocator"); + for (reg, offset) in ctx.callee_saved_offsets.clone() { + abi::load_at_offset(ctx.emitter, reg, offset); + } +} + +/// Returns the unique try-handler tokens used by EIR `try_push_handler` opcodes. +fn try_handler_tokens(function: &Function) -> Vec { + let mut tokens = Vec::new(); + for inst in &function.instructions { + if inst.op != Op::TryPushHandler { + continue; + } + let Some(Immediate::I64(token)) = inst.immediate else { + continue; + }; + if !tokens.contains(&token) { + tokens.push(token); + } + } + tokens +} + +/// Emits the process-entry prologue for the EIR main function. +pub(super) fn emit_main_prologue(ctx: &mut FunctionContext<'_>) { + if ctx.emitter.target.arch == Arch::AArch64 { + ctx.emitter.raw(".align 2"); + } + ctx.emitter.blank(); + ctx.emitter.entry_label(); + abi::emit_frame_prologue(ctx.emitter, ctx.frame_size); + capture_concat_base(ctx); + emit_callee_saved_saves(ctx); + ctx.emitter.comment("save argc/argv to globals"); + abi::emit_store_process_args_to_globals(ctx.emitter); + if ctx.heap_debug { + 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); +} + +/// Emits a callable function prologue using an already-resolved entry label. +pub(super) fn emit_function_prologue_with_label( + ctx: &mut FunctionContext<'_>, + entry_label: &str, +) -> crate::codegen::Result<()> { + if ctx.emitter.target.arch == Arch::AArch64 { + ctx.emitter.raw(".align 2"); + } + ctx.emitter.blank(); + ctx.emitter.label_global(entry_label); + abi::emit_frame_prologue(ctx.emitter, ctx.frame_size); + capture_concat_base(ctx); + emit_callee_saved_saves(ctx); + let mut incoming_args = abi::IncomingArgCursor::for_target(ctx.emitter.target, 0); + for (index, param) in ctx.function.params.iter().enumerate() { + let slot = LocalSlotId::from_raw(index as u32); + let offset = ctx.local_offset(slot)?; + abi::emit_store_incoming_param( + ctx.emitter, + ¶m.name, + ¶m.php_type, + offset, + param.by_ref, + &mut incoming_args, + ); + let local_ty = ctx.local_php_type(slot)?; + if !param.by_ref + && local_ty.codegen_repr() == PhpType::Mixed + && param.php_type.codegen_repr() != PhpType::Mixed + { + abi::emit_load(ctx.emitter, ¶m.php_type.codegen_repr(), offset); + emit_box_current_value_as_mixed(ctx.emitter, ¶m.php_type.codegen_repr()); + abi::emit_store(ctx.emitter, &PhpType::Mixed, offset); + } + } + zero_initialize_function_cleanup_locals(ctx); + zero_initialize_ref_cell_owner_locals(ctx); + Ok(()) +} + +/// Captures the caller-visible concat-buffer offset as this frame's reset base. +fn capture_concat_base(ctx: &mut FunctionContext<'_>) { + let scratch = abi::temp_int_reg(ctx.emitter.target); + abi::emit_load_symbol_to_reg(ctx.emitter, scratch, "_concat_off", 0); + abi::store_at_offset(ctx.emitter, scratch, ctx.concat_base_offset); +} + +/// Emits frame teardown and exits the process with status 0. +/// +/// The top-level body emits this epilogue INLINE at every `return` terminator +/// (it has no shared epilogue label to jump to, unlike user functions). It must +/// therefore emit a full self-contained epilogue on EVERY call — a one-shot guard +/// would leave all but the first `return` falling through into later blocks. The +/// trailing caller in `block_emit` is already gated on `!epilogue_emitted`, so the +/// final epilogue is still emitted at most once when the body has no `return`. +pub(super) fn emit_main_epilogue(ctx: &mut FunctionContext<'_>) { + ctx.emitter.blank(); + ctx.emitter.comment("epilogue + exit(0)"); + emit_main_local_epilogue_cleanup(ctx); + emit_main_static_local_cleanup(ctx); + emit_main_global_epilogue_cleanup(ctx); + emit_callee_saved_restores(ctx); + abi::emit_frame_restore(ctx.emitter, ctx.frame_size); + if ctx.gc_stats { + emit_gc_stats(ctx); + } + if ctx.heap_debug { + ctx.emitter + .comment("heap-debug: print allocator summary and leak report to stderr"); + abi::emit_call_label(ctx.emitter, "__rt_heap_debug_report"); + } + abi::emit_exit(ctx.emitter, 0); + ctx.epilogue_emitted = true; +} + +/// Releases initialized function static locals before process-exit diagnostics. +fn emit_main_static_local_cleanup(ctx: &mut FunctionContext<'_>) { + let static_locals = ctx.data.static_locals().to_vec(); + for record in static_locals { + let ty = record.php_type.codegen_repr(); + if !(matches!(ty, PhpType::Str | PhpType::Callable) || ty.is_refcounted()) { + continue; + } + let done = ctx.next_label("static_local_cleanup_done"); + ctx.emitter + .comment(&format!("epilogue cleanup static local {}", record.symbol)); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &record.init_symbol, + 0, + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done); + emit_static_symbol_value_cleanup(ctx, &record.symbol, &ty); + abi::emit_store_zero_to_symbol(ctx.emitter, &record.symbol, 0); + abi::emit_store_zero_to_symbol(ctx.emitter, &record.symbol, 8); + abi::emit_store_zero_to_symbol(ctx.emitter, &record.init_symbol, 0); + ctx.emitter.label(&done); + } +} + +/// Releases global symbol storage owned by the top-level EIR body before diagnostics. +fn emit_main_global_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { + let globals = ctx.module.data.global_names.clone(); + for name in globals { + if ctx.module.extern_globals.contains_key(&name) { + continue; + } + let ty = if crate::superglobals::is_superglobal(&name) { + crate::superglobals::superglobal_type().codegen_repr() + } else { + PhpType::Mixed + }; + if !cleanup_tracked_codegen_type(&ty) { + continue; + } + let symbol = ir_global_symbol(&name); + 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); + if ty == PhpType::Str { + abi::emit_store_zero_to_symbol(ctx.emitter, &symbol, 8); + } + } +} + +/// Releases the refcounted value stored in a static-local symbol. +fn emit_static_symbol_value_cleanup(ctx: &mut FunctionContext<'_>, symbol: &str, ty: &PhpType) { + match ty { + PhpType::Str => { + abi::emit_load_symbol_to_reg(ctx.emitter, abi::int_result_reg(ctx.emitter), symbol, 0); + abi::emit_call_label(ctx.emitter, "__rt_heap_free_safe"); + } + PhpType::Callable => { + abi::emit_load_symbol_to_result(ctx.emitter, symbol, ty); + abi::emit_decref_if_refcounted(ctx.emitter, ty); + } + other if other.is_refcounted() => { + abi::emit_load_symbol_to_result(ctx.emitter, symbol, other); + abi::emit_decref_if_refcounted(ctx.emitter, other); + } + _ => {} + } +} + +/// Emits the C-callable `--web` top-level handler prologue. +/// +/// Mirrors `emit_main_prologue` but labels the body `_elephc_web_handler` (a +/// C-ABI `extern "C" fn()`) and never stores argc/argv. At `handler()` entry +/// those registers are not the OS-provided values — the process-entry stub +/// stores them to `_global_argc`/`_global_argv` once before calling the bridge, +/// so the handler must not overwrite them. Consequently `$argc`/`$argv` are not +/// populated inside a `--web` top-level body in Phase 1 (acceptable for echo). +pub(super) fn emit_web_handler_prologue(ctx: &mut FunctionContext<'_>) { + if ctx.emitter.target.arch == Arch::AArch64 { + ctx.emitter.raw(".align 2"); + } + ctx.emitter.blank(); + ctx.emitter.label_global(WEB_HANDLER_SYMBOL); + abi::emit_frame_prologue(ctx.emitter, ctx.frame_size); + // Reset all process-persistent state (function static locals, refcounted + // static property values, and `_concat_off`) BEFORE this frame captures the + // concat base and BEFORE the body's re-run static/enum initializers, so each + // request sees clean state. `__rt_web_reset` is generated per program after + // every function is emitted; the call here forward-references its label. + ctx.emitter.comment("reset per-request persistent state"); + abi::emit_call_label(ctx.emitter, "__rt_web_reset"); + capture_concat_base(ctx); + emit_callee_saved_saves(ctx); + zero_initialize_main_cleanup_locals(ctx); + zero_initialize_ref_cell_owner_locals(ctx); +} + +/// Emits the `--web` top-level handler epilogue and returns to the bridge. +/// +/// Like `emit_main_epilogue` it runs the per-request main local cleanup (so +/// owned refcounted top-level locals are released each request) and restores the +/// frame, but it `ret`s instead of exiting and skips the process-end gc-stats and +/// heap-debug diagnostics, which are wrong to report per request. +pub(super) fn emit_web_handler_epilogue(ctx: &mut FunctionContext<'_>) { + ctx.emitter.blank(); + ctx.emitter.comment("web handler epilogue + ret"); + emit_main_local_epilogue_cleanup(ctx); + emit_callee_saved_restores(ctx); + abi::emit_frame_restore(ctx.emitter, ctx.frame_size); + abi::emit_return(ctx.emitter); + ctx.epilogue_emitted = true; +} + +/// Emits the `--web` process-entry stub that drives the bridge server entry. +/// +/// The stub is the real process entry (`_main`/`main`). It stores the OS argc/argv +/// to globals once, loads them plus the handler address into the first three +/// C-ABI integer argument registers, calls `elephc_web_run(argc, argv, &handler)`, +/// and exits the process with the bridge's integer return value. The handler +/// address (arg 2) is materialized last so a destination-register page load on +/// AArch64 cannot clobber the already-loaded argc/argv argument registers. +pub(super) fn emit_web_entry_stub(ctx: &mut FunctionContext<'_>) { + let target = ctx.emitter.target; + if target.arch == Arch::AArch64 { + ctx.emitter.raw(".align 2"); + } + ctx.emitter.blank(); + ctx.emitter + .comment("--web process entry: call elephc_web_run(argc, argv, &handler)"); + ctx.emitter.entry_label(); + abi::emit_frame_prologue(ctx.emitter, ctx.frame_size); + ctx.emitter + .comment("save argc/argv to globals for the bridge and handler"); + abi::emit_store_process_args_to_globals(ctx.emitter); + let argc_reg = abi::int_arg_reg_name(target, 0); + let argv_reg = abi::int_arg_reg_name(target, 1); + let handler_reg = abi::int_arg_reg_name(target, 2); + abi::emit_load_symbol_to_reg(ctx.emitter, argc_reg, "_global_argc", 0); + abi::emit_load_symbol_to_reg(ctx.emitter, argv_reg, "_global_argv", 0); + abi::emit_symbol_address(ctx.emitter, handler_reg, WEB_HANDLER_SYMBOL); + // `elephc_web_run` is a `#[no_mangle] extern "C"` Rust symbol in the bridge + // staticlib, so it carries the platform's C-ABI underscore: resolve it through + // `extern_symbol` (`_elephc_web_run` on macOS, `elephc_web_run` on Linux). + let bridge_entry = target.extern_symbol("elephc_web_run"); + abi::emit_call_label(ctx.emitter, &bridge_entry); + abi::emit_exit_with_result_reg(ctx.emitter); +} + +/// Zero-initializes cleanup-tracked locals so skipped assignments stay safe at epilogue. +fn zero_initialize_main_cleanup_locals(ctx: &mut FunctionContext<'_>) { + for (_, _, ty, offset) in main_cleanup_locals(ctx) { + match ty { + PhpType::Str => { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + abi::emit_store_zero_to_local_slot(ctx.emitter, offset - 8); + } + _ => { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } + } + } +} + +/// Releases owned main locals that still hold refcounted storage at process exit. +fn emit_main_local_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { + emit_ref_cell_owner_epilogue_cleanup(ctx); + for (name, _, ty, offset) in main_cleanup_locals(ctx) { + ctx.emitter.comment(&format!("epilogue cleanup ${}", name)); + match ty { + PhpType::Str => emit_main_string_cleanup(ctx, offset), + PhpType::Callable => emit_main_refcounted_cleanup(ctx, offset, &ty), + other if other.is_refcounted() => emit_main_refcounted_cleanup(ctx, offset, &other), + _ => {} + } + } +} + +/// Returns main local slots that receive owned refcounted values through `StoreLocal`. +fn main_cleanup_locals(ctx: &FunctionContext<'_>) -> Vec<(String, LocalSlotId, PhpType, usize)> { + let param_names = ctx + .function + .params + .iter() + .map(|param| param.name.as_str()) + .collect::>(); + let mut locals = ctx + .function + .locals + .iter() + .filter(|local| local_kind_needs_epilogue_cleanup(local.kind)) + .filter(|local| !promoted_ref_cell_local_slots(ctx.function).contains(&local.id)) + .filter(|local| { + local + .name + .as_deref() + .is_none_or(|name| !param_names.contains(name)) + }) + .filter(|local| local_slot_has_store(ctx.function, local.id)) + .filter_map(|local| { + let ty = local.php_type.codegen_repr(); + if !(matches!(ty, PhpType::Str | PhpType::Callable) || ty.is_refcounted()) { + return None; + } + let offset = ctx.local_offset(local.id).ok()?; + let name = local + .name + .clone() + .unwrap_or_else(|| format!("slot{}", local.id.as_raw())); + Some((name, local.id, ty, offset)) + }) + .collect::>(); + locals.sort_by_key(|(_, _, _, offset)| *offset); + locals +} + +/// Zero-initializes hidden ref-cell owner slots before any fallback promotion can run. +fn zero_initialize_ref_cell_owner_locals(ctx: &mut FunctionContext<'_>) { + for (_, _, _, offset) in ref_cell_owner_locals(ctx) { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } +} + +/// Releases hidden ref-cell owner slots that still hold fallback cells at exit. +fn emit_ref_cell_owner_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { + let owners = ref_cell_owner_locals(ctx); + emit_ref_cell_owner_epilogue_cleanup_for(ctx, owners); +} + +/// Releases a precomputed set of hidden ref-cell owner slots. +fn emit_ref_cell_owner_epilogue_cleanup_for( + ctx: &mut FunctionContext<'_>, + owners: Vec<(String, LocalSlotId, PhpType, usize)>, +) { + for (name, _, ty, offset) in owners { + ctx.emitter + .comment(&format!("epilogue cleanup ref-cell owner ${}", name)); + emit_ref_cell_owner_cleanup(ctx, offset, &ty); + } +} + +/// Releases the owner slot's ref-cell pointer when it is non-null, then clears the owner. +fn emit_ref_cell_owner_cleanup(ctx: &mut FunctionContext<'_>, offset: usize, ty: &PhpType) { + let done = ctx.next_label("ref_cell_owner_cleanup_done"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::load_at_offset_scratch(ctx.emitter, "x9", offset, "x11"); + ctx.emitter.instruction(&format!("cbz x9, {}", done)); // skip released or never-created fallback ref-cells + abi::emit_release_local_ref_cell(ctx.emitter, "x9", ty); + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } + Arch::X86_64 => { + abi::load_at_offset_scratch(ctx.emitter, "r11", offset, "r10"); + ctx.emitter.instruction("test r11, r11"); // check whether this owner still holds a fallback ref-cell + ctx.emitter.instruction(&format!("je {}", done)); // skip released or never-created fallback ref-cells + abi::emit_release_local_ref_cell(ctx.emitter, "r11", ty); + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } + } + ctx.emitter.label(&done); +} + +/// Returns hidden owner locals that track promoted fallback ref-cells. +fn ref_cell_owner_locals(ctx: &FunctionContext<'_>) -> Vec<(String, LocalSlotId, PhpType, usize)> { + let mut locals = ctx + .function + .locals + .iter() + .filter(|local| local.kind == LocalKind::RefCell) + .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, local.id, local.php_type.codegen_repr(), offset)) + }) + .collect::>(); + locals.sort_by_key(|(_, _, _, offset)| *offset); + locals +} + +/// 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| { + inst.op == Op::StoreLocal + && matches!(inst.immediate, Some(Immediate::LocalSlot(candidate)) if candidate == slot) + }) +} + +/// Returns PHP-visible locals whose slot is rewritten to a ref-cell pointer. +fn promoted_ref_cell_local_slots(function: &Function) -> HashSet { + let mut slots = function + .instructions + .iter() + .filter_map(|inst| match inst.immediate { + Some(Immediate::LocalSlotPair { first, .. }) if inst.op == Op::PromoteLocalRefCell => { + Some(first) + } + Some(Immediate::LocalSlotPair { first, .. }) if inst.op == Op::AliasLocalRefCell => { + Some(first) + } + _ => None, + }) + .collect::>(); + slots.extend(closure_ref_capture_local_slots(function)); + slots +} + +/// Returns local slots whose value is captured by reference into a closure descriptor. +fn closure_ref_capture_local_slots(function: &Function) -> HashSet { + function + .instructions + .iter() + .filter(|inst| inst.op == Op::ClosureCapture) + .filter(|inst| inst.immediate == Some(Immediate::I64(1))) + .filter_map(|inst| inst.operands.first().copied()) + .filter_map(|value| loaded_local_slot(function, value)) + .collect() +} + +/// Resolves a lowered local read value back to its source slot. +fn loaded_local_slot(function: &Function, value: ValueId) -> Option { + let value = function.value(value)?; + let ValueDef::Instruction { inst, .. } = value.def else { + return None; + }; + let inst = function.instruction(inst)?; + match (inst.op, inst.immediate.as_ref()) { + (Op::LoadLocal | Op::LoadRefCell, Some(Immediate::LocalSlot(slot))) => Some(*slot), + _ => None, + } +} + +/// Releases a string local through the validating heap-free helper. +/// +/// `__rt_heap_free_safe` skips non-heap pointers (null for uninitialized locals, +/// .rodata, out-of-range) and frees plausible live heap blocks, so it safely handles +/// the zero-length owned strings that `__rt_str_persist` now allocates. The previous +/// `cbz len` guard skipped them and leaked every owned empty string at scope exit. +fn emit_main_string_cleanup(ctx: &mut FunctionContext<'_>, offset: usize) { + let (ptr_reg, _) = abi::string_result_regs(ctx.emitter); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::load_at_offset(ctx.emitter, ptr_reg, offset); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("mov {}, {}", result_reg, ptr_reg)); // pass the local string pointer to the validating heap-free helper + abi::emit_call_label(ctx.emitter, "__rt_heap_free_safe"); + } + Arch::X86_64 => { + if ptr_reg != result_reg { + ctx.emitter + .instruction(&format!("mov {}, {}", result_reg, ptr_reg)); // pass the local string pointer to the validating heap-free helper + } + abi::emit_call_label(ctx.emitter, "__rt_heap_free_safe"); + } + } +} + +/// Releases a refcounted local when the slot contains a non-null heap pointer. +fn emit_main_refcounted_cleanup(ctx: &mut FunctionContext<'_>, offset: usize, ty: &PhpType) { + let result_reg = abi::int_result_reg(ctx.emitter); + let done = ctx.next_label("main_refcounted_cleanup_done"); + abi::load_at_offset(ctx.emitter, result_reg, offset); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cbz {}, {}", result_reg, done)); // skip uninitialized refcounted locals + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", result_reg, result_reg)); // check whether the refcounted local is initialized + ctx.emitter.instruction(&format!("je {}", done)); // skip uninitialized refcounted locals + } + } + abi::emit_decref_if_refcounted(ctx.emitter, ty); + ctx.emitter.label(&done); +} + +/// Zero-initializes function locals that may be released by the shared epilogue. +fn zero_initialize_function_cleanup_locals(ctx: &mut FunctionContext<'_>) { + for (_, _, ty, offset) in function_cleanup_locals(ctx, None) { + match ty { + PhpType::Str => { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + abi::emit_store_zero_to_local_slot(ctx.emitter, offset - 8); + } + _ => { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } + } + } +} + +/// Releases owned function locals that do not transfer ownership to this return path. +fn emit_function_local_epilogue_cleanup( + ctx: &mut FunctionContext<'_>, + skip_return_slot: Option, +) { + 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() { + return; + } + let return_ty = ctx.function.return_php_type.codegen_repr(); + let preserves_return = !matches!(return_ty, PhpType::Void | PhpType::Never); + if preserves_return { + push_return_value(ctx, &return_ty); + } + emit_ref_cell_owner_epilogue_cleanup_for(ctx, ref_cell_owners); + for (name, _, ty, offset) in cleanup_locals { + ctx.emitter.comment(&format!("epilogue cleanup ${}", name)); + match ty { + PhpType::Str => emit_main_string_cleanup(ctx, offset), + PhpType::Callable => emit_main_refcounted_cleanup(ctx, offset, &ty), + other if other.is_refcounted() => emit_main_refcounted_cleanup(ctx, offset, &other), + _ => {} + } + } + if preserves_return { + pop_return_value(ctx, &return_ty); + } +} + +/// Returns function local slots that receive owned refcounted values through `StoreLocal`. +/// +/// `skip_return_slot` excludes the one local whose refcounted owner is transferred by the +/// current return terminator. It is deliberately path-local: another `return` in the same +/// function may return a scalar or a different value and must still release this slot. +fn function_cleanup_locals( + ctx: &FunctionContext<'_>, + skip_return_slot: Option, +) -> Vec<(String, LocalSlotId, PhpType, usize)> { + let param_names = ctx + .function + .params + .iter() + .map(|param| param.name.as_str()) + .collect::>(); + let mut locals = ctx + .function + .locals + .iter() + .filter(|local| local_kind_needs_epilogue_cleanup(local.kind)) + .filter(|local| !promoted_ref_cell_local_slots(ctx.function).contains(&local.id)) + .filter(|local| { + local + .name + .as_deref() + .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_map(|local| { + let ty = local.php_type.codegen_repr(); + if !cleanup_tracked_codegen_type(&ty) { + return None; + } + let offset = ctx.local_offset(local.id).ok()?; + let name = local + .name + .clone() + .unwrap_or_else(|| format!("slot{}", local.id.as_raw())); + Some((name, local.id, ty, offset)) + }) + .collect::>(); + locals.sort_by_key(|(_, _, _, offset)| *offset); + locals +} + +/// Returns whether a local kind can own values through ordinary `StoreLocal`. +fn local_kind_needs_epilogue_cleanup(kind: LocalKind) -> bool { + matches!( + kind, + LocalKind::PhpLocal + | LocalKind::HiddenTemp + | LocalKind::OwnedTemp + | LocalKind::NamedArgTemp + ) +} + +/// Returns the local slot whose cleanup this return path must skip, if ownership is transferred. +pub(super) fn return_cleanup_skip_slot(function: &Function, value: ValueId) -> Option { + let result_ty = function.value(value)?.php_type.codegen_repr(); + let return_ty = function.return_php_type.codegen_repr(); + let mut visited = HashSet::new(); + return_cleanup_skip_slot_inner(function, value, &result_ty, &return_ty, &mut visited) +} + +/// Recursively traces forwarding return values back to the owned local they transfer. +fn return_cleanup_skip_slot_inner( + function: &Function, + value: ValueId, + result_ty: &PhpType, + return_ty: &PhpType, + visited: &mut HashSet, +) -> Option { + if !visited.insert(value) { + return None; + } + let value_ref = function.value(value)?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return None; + }; + let inst = function.instruction(inst)?; + match inst.op { + Op::LoadLocal => { + let Some(Immediate::LocalSlot(slot)) = inst.immediate else { + return None; + }; + let local_ty = local_codegen_type(function, slot)?; + if local_load_transfers_stored_owner(&local_ty, result_ty) + && return_preserves_result_owner(result_ty, return_ty) + { + Some(slot) + } else { + None + } + } + Op::ArrayToMixed | Op::HashToMixed => { + let source = *inst.operands.first()?; + let slot = direct_return_local_slot_inner(function, source, visited)?; + let local_ty = local_codegen_type(function, slot)?; + if cleanup_tracked_codegen_type(&local_ty) + && return_preserves_result_owner(result_ty, return_ty) + { + Some(slot) + } else { + None + } + } + Op::Move | Op::Borrow => { + let source = *inst.operands.first()?; + return_cleanup_skip_slot_inner(function, source, result_ty, return_ty, visited) + } + _ => None, + } +} + +/// Recursively traces forwarding values to the local slot that backs them. +fn direct_return_local_slot_inner( + function: &Function, + value: crate::ir::ValueId, + visited: &mut HashSet, +) -> Option { + if !visited.insert(value) { + return None; + } + let value = function.value(value)?; + let ValueDef::Instruction { inst, .. } = value.def else { + return None; + }; + let inst = function.instruction(inst)?; + match inst.op { + Op::LoadLocal => match inst.immediate { + Some(Immediate::LocalSlot(slot)) => Some(slot), + _ => None, + }, + Op::ArrayToMixed | Op::HashToMixed => { + let source = *inst.operands.first()?; + direct_return_local_slot_inner(function, source, visited) + } + Op::Move | Op::Borrow => { + let source = *inst.operands.first()?; + direct_return_local_slot_inner(function, source, visited) + } + _ => None, + } +} + +/// Returns a local slot's codegen PHP type. +fn local_codegen_type(function: &Function, slot: LocalSlotId) -> Option { + function + .locals + .get(slot.as_raw() as usize) + .filter(|local| local.id == slot) + .map(|local| local.php_type.codegen_repr()) +} + +/// Returns true when a codegen type carries refcounted ownership to release or transfer. +fn cleanup_tracked_codegen_type(ty: &PhpType) -> bool { + matches!(ty, PhpType::Str | PhpType::Callable) || ty.is_refcounted() +} + +/// Returns true when loading a local into an SSA result leaves the same owner in the result. +fn local_load_transfers_stored_owner(local_ty: &PhpType, result_ty: &PhpType) -> bool { + if !cleanup_tracked_codegen_type(local_ty) { + return false; + } + if local_ty == result_ty { + return true; + } + matches!( + (local_ty, result_ty), + (PhpType::Array(_), PhpType::Array(_)) + | (PhpType::AssocArray { .. }, PhpType::AssocArray { .. }) + ) +} + +/// Returns true when final return lowering preserves the loaded refcounted result owner. +fn return_preserves_result_owner(result_ty: &PhpType, return_ty: &PhpType) -> bool { + if !cleanup_tracked_codegen_type(result_ty) || !cleanup_tracked_codegen_type(return_ty) { + return false; + } + if result_ty == return_ty { + return true; + } + matches!( + (result_ty, return_ty), + (PhpType::Array(_), PhpType::Array(_)) + | (PhpType::AssocArray { .. }, PhpType::AssocArray { .. }) + ) +} + +/// Preserves the current typed return value on the temporary stack. +fn push_return_value(ctx: &mut FunctionContext<'_>, ty: &PhpType) { + match ty.codegen_repr() { + PhpType::Float => { + abi::emit_push_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); + } + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); + } + PhpType::TaggedScalar => { + abi::emit_push_reg_pair( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter), + ); + } + _ => { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + } + } +} + +/// Restores a typed return value preserved by `push_return_value`. +fn pop_return_value(ctx: &mut FunctionContext<'_>, ty: &PhpType) { + match ty.codegen_repr() { + PhpType::Float => { + abi::emit_pop_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); + } + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_pop_reg_pair(ctx.emitter, ptr_reg, len_reg); + } + PhpType::TaggedScalar => { + abi::emit_pop_reg_pair( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter), + ); + } + _ => { + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + } + } +} + +/// Emits allocation/free totals to stderr using the shared runtime counters. +fn emit_gc_stats(ctx: &mut FunctionContext<'_>) { + ctx.emitter + .comment("gc-stats: print allocation statistics to stderr"); + let (allocs_label, allocs_len) = ctx.data.add_string(b"GC: allocs="); + emit_write_literal_stderr(ctx.emitter, &allocs_label, allocs_len); + let int_result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, int_result_reg, "_gc_allocs", 0); + abi::emit_call_label(ctx.emitter, "__rt_itoa"); + emit_write_current_string_stderr(ctx.emitter); + let (frees_label, frees_len) = ctx.data.add_string(b" frees="); + emit_write_literal_stderr(ctx.emitter, &frees_label, frees_len); + abi::emit_load_symbol_to_reg(ctx.emitter, int_result_reg, "_gc_frees", 0); + abi::emit_call_label(ctx.emitter, "__rt_itoa"); + emit_write_current_string_stderr(ctx.emitter); + let (newline_label, _) = ctx.data.add_string(b"\n"); + emit_write_literal_stderr(ctx.emitter, &newline_label, 1); +} + +/// Emits a path-specific epilogue for one user-function return terminator. +pub(super) fn emit_function_return_epilogue( + ctx: &mut FunctionContext<'_>, + skip_return_slot: Option, +) { + emit_function_local_epilogue_cleanup(ctx, skip_return_slot); + emit_callee_saved_restores(ctx); + abi::emit_frame_restore(ctx.emitter, ctx.frame_size); + abi::emit_return(ctx.emitter); +} + +/// Emits the shared epilogue for a direct-callable user function. +pub(super) fn emit_function_epilogue(ctx: &mut FunctionContext<'_>) { + if ctx.epilogue_emitted { + return; + } + let label = ctx + .epilogue_label + .clone() + .expect("codegen bug: user function has no epilogue label"); + ctx.emitter.label(&label); + emit_function_local_epilogue_cleanup(ctx, None); + emit_callee_saved_restores(ctx); + abi::emit_frame_restore(ctx.emitter, ctx.frame_size); + abi::emit_return(ctx.emitter); + ctx.epilogue_emitted = true; +} + +/// Rounds a byte count up to a 16-byte stack alignment boundary. +fn align_to_16(bytes: usize) -> usize { + (bytes + 15) & !15 +} + +/// 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 + .function + .locals + .iter() + .find(|local| local.name.as_deref() == Some("argc")) + .map(|local| local.id) + 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, + ); +} + +/// 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 + .function + .locals + .iter() + .find(|local| local.name.as_deref() == Some("argv")) + .map(|local| local.id) + else { + return; + }; + let Ok(offset) = ctx.local_offset(argv_slot) else { + return; + }; + 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); +} diff --git a/src/codegen/function_variants.rs b/src/codegen/function_variants.rs index cb580aa8e0..a06df24483 100644 --- a/src/codegen/function_variants.rs +++ b/src/codegen/function_variants.rs @@ -1,51 +1,68 @@ //! Purpose: -//! Emits include-aware function variant thunks and active-symbol checks for resolved includes. -//! Keeps multiple discovered function bodies callable through a stable PHP function name. +//! Emits EIR backend dispatchers for include-loaded function variants. +//! Interprets variant metadata lowered from resolver-produced synthetic statements. //! //! Called from: -//! - `crate::codegen::generate()` after resolver-provided variant metadata +//! - `crate::codegen::block_emit` before user functions are emitted. +//! - `crate::codegen::lower_inst` when a concrete include path activates a variant. //! //! Key details: -//! - Variant symbols are coupled to include statements and must preserve PHP load-order behavior. - -use std::collections::HashMap; +//! - Dispatchers use the public PHP function symbol and tail-dispatch through an +//! active function-pointer slot populated by `FunctionVariantMark`. +use crate::codegen::abi; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; +use crate::ir::{function_variants, Function, Module}; use crate::names::{function_symbol, function_variant_active_symbol}; -use crate::parser::ast::{Program, Stmt, StmtKind}; -use super::abi; -use super::data_section::DataSection; -use super::emit::Emitter; +// Delegate pure variant resolution/collect to the canonical ir module (single source of truth). +pub(super) use function_variants::{ + collect_dispatch_groups, parse_variant_label, FunctionVariantLabel, +}; -/// Walks the program AST and collects all `FunctionVariantGroup` nodes into a map -/// keyed by group name. Each group maps to the ordered list of variant names -/// discovered in that group. -/// -/// Called from: -/// - `emit_function_variant_dispatcher` to build the variant dispatch table -pub(crate) fn collect_function_variant_groups(program: &Program) -> HashMap> { - let mut groups = HashMap::new(); - collect_from_stmts(program, &mut groups); - groups +/// Returns a representative concrete variant function for a public function group. +pub(super) fn variant_callee_for_group<'a>(module: &'a Module, name: &str) -> Option<&'a Function> { + function_variants::variant_callee_for_group(module, name) } -/// Emits a thunk that dispatches to the active function variant for a given name. -/// -/// The dispatcher is a global symbol named after the PHP function. It checks an -/// active-symbol slot (initialized by include loading) and tail-dispatches to the -/// loaded variant. If no variant is active, it writes a "undefined function" diagnostic -/// to stderr and exits with code 1. -/// -/// Arguments: -/// - `emitter` — target code emitter -/// - `data` — data section for constants and strings -/// - `name` — PHP function name (used to derive symbol and active-symbol names) -/// -/// ABI notes: -/// - AArch64: uses `cbz` to test the active-symbol pointer, then `br` to tail-dispatch -/// - X86_64: uses `test`/`je` to test and `jmp` to tail-dispatch -pub(crate) fn emit_function_variant_dispatcher( +/// Emits every include-variant dispatcher required by the EIR module. +pub(super) fn emit_dispatchers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + for group in collect_dispatch_groups(module) { + emit_function_variant_dispatcher(emitter, data, &group.name); + } +} + +/// Emits the runtime mark that makes one concrete include-loaded function active. +pub(super) fn emit_variant_mark( + emitter: &mut Emitter, + data: &mut DataSection, + label: &FunctionVariantLabel, +) -> crate::codegen::Result<()> { + if label.variants.len() != 1 { + return Err(crate::codegen::CodegenIrError::invalid_module(format!( + "function variant mark for '{}' names {} variants", + label.name, + label.variants.len() + ))); + } + let variant = &label.variants[0]; + let active_symbol = function_variant_active_symbol(&label.name); + data.add_comm(active_symbol.clone(), 8); + + let variant_reg = abi::temp_int_reg(emitter.target); + abi::emit_symbol_address(emitter, variant_reg, &function_symbol(variant)); + abi::emit_store_reg_to_symbol(emitter, variant_reg, &active_symbol, 0); + Ok(()) +} + +/// Emits a public-name thunk that tail-dispatches to the currently active variant. +fn emit_function_variant_dispatcher( emitter: &mut Emitter, data: &mut DataSection, name: &str, @@ -64,13 +81,13 @@ pub(crate) fn emit_function_variant_dispatcher( abi::emit_load_symbol_to_reg(emitter, target_reg, &active_symbol, 0); match emitter.target.arch { Arch::AArch64 => { - emitter.instruction(&format!("cbz {}, {}", target_reg, fail_label)); // abort if no include has loaded this function implementation - emitter.instruction(&format!("br {}", target_reg)); // tail-dispatch to the loaded function variant without changing arguments + emitter.instruction(&format!("cbz {}, {}", target_reg, fail_label)); // abort if no include has activated this function variant + emitter.instruction(&format!("br {}", target_reg)); // tail-dispatch to the active function variant with existing arguments } Arch::X86_64 => { - emitter.instruction(&format!("test {}, {}", target_reg, target_reg)); // abort if no include has loaded this function implementation - emitter.instruction(&format!("je {}", fail_label)); // jump to the fatal path when the active function pointer is missing - emitter.instruction(&format!("jmp {}", target_reg)); // tail-dispatch to the loaded function variant without changing arguments + emitter.instruction(&format!("test {}, {}", target_reg, target_reg)); // abort if no include has activated this function variant + emitter.instruction(&format!("je {}", fail_label)); // jump to the undefined-function fatal path + emitter.instruction(&format!("jmp {}", target_reg)); // tail-dispatch to the active function variant with existing arguments } } @@ -78,7 +95,8 @@ pub(crate) fn emit_function_variant_dispatcher( match emitter.target.arch { Arch::AArch64 => { emitter.instruction("mov x0, #2"); // write the undefined-function diagnostic to stderr - crate::codegen::abi::emit_symbol_address(emitter, "x1", &message_label); // load the diagnostic string page for stderr output + emitter.adrp("x1", &message_label); // load the diagnostic string page for stderr output + emitter.add_lo12("x1", "x1", &message_label); // resolve the diagnostic string address for stderr output emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the diagnostic byte length to write emitter.syscall(4); abi::emit_exit(emitter, 1); @@ -94,24 +112,4 @@ pub(crate) fn emit_function_variant_dispatcher( } } -/// Recursively walks a statement list and populates `groups` with any -/// `FunctionVariantGroup` declarations found. -/// -/// Handles `StmtKind::FunctionVariantGroup` directly, and recurses into -/// `Synthetic`, `NamespaceBlock`, and `IncludeOnceGuard` bodies to find nested groups. -fn collect_from_stmts(stmts: &[Stmt], groups: &mut HashMap>) { - for stmt in stmts { - match &stmt.kind { - StmtKind::FunctionVariantGroup { name, variants } => { - groups.insert(name.clone(), variants.clone()); - } - StmtKind::Synthetic(body) | StmtKind::NamespaceBlock { body, .. } => { - collect_from_stmts(body, groups); - } - StmtKind::IncludeOnceGuard { body, .. } => { - collect_from_stmts(body, groups); - } - _ => {} - } - } -} +// (pure helpers moved to crate::ir::function_variants for canonical single implementation) diff --git a/src/codegen/functions/callback_wrapper.rs b/src/codegen/functions/callback_wrapper.rs deleted file mode 100644 index 86761535ef..0000000000 --- a/src/codegen/functions/callback_wrapper.rs +++ /dev/null @@ -1,488 +0,0 @@ -//! Purpose: -//! Emits native callback wrappers that adapt external callbacks into PHP-callable function bodies. -//! Moves callback arguments through compiler ABI slots and returns runtime-compatible values. -//! -//! Called from: -//! - `crate::codegen::functions` when FFI callback metadata is required -//! -//! Key details: -//! - Wrapper signatures must satisfy both the external ABI and the internal PHP function lowering contract. - -use crate::codegen::abi; -use crate::codegen::context::{DeferredCallbackWrapper, DeferredExternCallbackTrampoline}; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::types::PhpType; - -mod descriptor; - -/// Emits a native callback wrapper that adapts an external ABI caller into a PHP-callable -/// function body. Dispatches to the x86_64 variant; ARM64 uses the general path below. -/// The wrapper preserves callee-saved registers, spills incoming arguments and captures -/// from the environment struct, then calls the original closure entry point before returning. -pub(crate) fn emit_callback_wrapper(emitter: &mut Emitter, wrapper: &DeferredCallbackWrapper) { - if let Some(return_ty) = &wrapper.descriptor_return_type { - descriptor::emit_descriptor_callback_wrapper(emitter, wrapper, return_ty); - return; - } - - if emitter.target.arch == Arch::X86_64 { - emit_x86_64_callback_wrapper(emitter, wrapper); - return; - } - - let target_visible_arg_types = wrapper_target_visible_arg_types(wrapper); - let arg_types = wrapper_arg_types(wrapper); - let slot_count = arg_types.len().max(1); - let frame_size = align16(slot_count * 16 + 32); - let saved_callee_offset = frame_size - 32; - - emitter.blank(); - emitter.comment(&format!("callback wrapper: {}", wrapper.label)); - emitter.raw(".align 2"); - emitter.label_global(&wrapper.label); - abi::emit_frame_prologue(emitter, frame_size); - emitter.instruction(&format!("stp x19, x20, [sp, #{}]", saved_callee_offset)); // preserve wrapper callee-saved registers - - let env_reg = incoming_env_reg(emitter, &wrapper.visible_arg_types); - emitter.instruction(&format!("mov x20, {}", env_reg)); // keep the callback environment pointer across argument reshuffling - emitter.instruction("ldr x19, [x20]"); // load the original captured closure entry point from env slot zero - - spill_visible_args(emitter, &wrapper.visible_arg_types); - spill_captures( - emitter, - wrapper.visible_arg_types.len(), - &wrapper.capture_types, - "x20", - ); - - let overflow_bytes = materialize_spilled_args_for_callback( - emitter, - &wrapper.visible_arg_types, - &target_visible_arg_types, - &wrapper.capture_types, - frame_size, - ); - abi::emit_call_reg(emitter, "x19"); - abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop stack-passed closure arguments after the adapted callback returns - - emitter.instruction(&format!("ldp x19, x20, [sp, #{}]", saved_callee_offset)); // restore wrapper callee-saved registers - abi::emit_frame_restore(emitter, frame_size); - abi::emit_return(emitter); -} - -/// Emits a C-ABI trampoline that reloads a descriptor from global storage. -/// -/// The generated symbol has the callback signature expected by the extern C API, -/// boxes incoming scalar/pointer arguments for the descriptor invoker, and casts -/// the boxed result back to the C-compatible callback return type. -pub(crate) fn emit_extern_callback_trampoline( - emitter: &mut Emitter, - trampoline: &DeferredExternCallbackTrampoline, -) { - descriptor::emit_extern_callback_trampoline(emitter, trampoline); -} - -/// Emits the x86_64-specific callback wrapper. Follows the same general pattern as the ARM64 -/// path but uses x86_64 callee-saved registers (r12, r13), different frame layout, and -/// stdarg-style argument push for overflow parameters. -fn emit_x86_64_callback_wrapper(emitter: &mut Emitter, wrapper: &DeferredCallbackWrapper) { - let target_visible_arg_types = wrapper_target_visible_arg_types(wrapper); - let arg_types = wrapper_arg_types(wrapper); - let slot_count = arg_types.len().max(1); - let frame_size = align16(slot_count * 16 + 48); - let saved_callback_offset = slot_count * 16 + 16; - let saved_env_offset = slot_count * 16 + 24; - - emitter.blank(); - emitter.comment(&format!("callback wrapper: {}", wrapper.label)); - emitter.raw(".align 16"); - emitter.label_global(&wrapper.label); - abi::emit_frame_prologue(emitter, frame_size); - abi::store_at_offset(emitter, "r12", saved_callback_offset); - abi::store_at_offset(emitter, "r13", saved_env_offset); - - let env_reg = incoming_env_reg(emitter, &wrapper.visible_arg_types); - emitter.instruction(&format!("mov r13, {}", env_reg)); // keep the callback environment pointer across argument reshuffling - emitter.instruction("mov r12, QWORD PTR [r13]"); // load the original captured closure entry point from env slot zero - - spill_visible_args(emitter, &wrapper.visible_arg_types); - spill_captures( - emitter, - wrapper.visible_arg_types.len(), - &wrapper.capture_types, - "r13", - ); - - let overflow_bytes = materialize_spilled_args_for_callback_x86_64( - emitter, - &wrapper.visible_arg_types, - &target_visible_arg_types, - &wrapper.capture_types, - ); - abi::emit_call_reg(emitter, "r12"); - abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop stack-passed closure arguments after the adapted callback returns - - abi::load_at_offset(emitter, "r13", saved_env_offset); - abi::load_at_offset(emitter, "r12", saved_callback_offset); - abi::emit_frame_restore(emitter, frame_size); - abi::emit_return(emitter); -} - -/// Returns the ordered list of PHP types for all arguments the wrapper will pass to the -/// adapted callback: visible arg types first (in incoming ABI order), then capture types. -fn wrapper_arg_types(wrapper: &DeferredCallbackWrapper) -> Vec { - wrapper_target_visible_arg_types(wrapper) - .iter() - .chain(wrapper.capture_types.iter()) - .map(PhpType::codegen_repr) - .collect() -} - -/// Provides the Wrapper target visible arg types helper used by the callback wrapper module. -fn wrapper_target_visible_arg_types(wrapper: &DeferredCallbackWrapper) -> Vec { - wrapper - .target_visible_arg_types - .clone() - .unwrap_or_else(|| wrapper.visible_arg_types.clone()) -} - -/// Returns the ABI register name that holds the incoming environment pointer (the closure -/// struct passed by the external caller). The environment pointer is the last argument in -/// the incoming type list; this function reverses the outgoing assignment logic to find -/// which register it occupies on entry. -fn incoming_env_reg(emitter: &Emitter, visible_arg_types: &[PhpType]) -> &'static str { - let mut incoming_types: Vec = - visible_arg_types.iter().map(PhpType::codegen_repr).collect(); - incoming_types.push(PhpType::Pointer(None)); - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &incoming_types, 0); - let env_assignment = assignments - .last() - .expect("callback wrapper always has an environment pointer argument"); - debug_assert!(env_assignment.in_register()); - abi::int_arg_reg_name(emitter.target, env_assignment.start_reg) -} - -/// Spills every incoming visible argument from ABI registers to fixed stack slots in the -/// wrapper frame. This must happen before `spill_captures` loads from the environment struct, -/// because the environment pointer lives in a register that may clobber one of the arg regs. -fn spill_visible_args(emitter: &mut Emitter, visible_arg_types: &[PhpType]) { - let visible_types: Vec = visible_arg_types.iter().map(PhpType::codegen_repr).collect(); - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &visible_types, 0); - for (idx, (ty, assignment)) in visible_types.iter().zip(assignments.iter()).enumerate() { - debug_assert!(assignment.in_register()); - match (emitter.target.arch, ty) { - (Arch::AArch64, PhpType::Float) => { - let reg = abi::float_arg_reg_name(emitter.target, assignment.start_reg); - emitter.instruction(&format!("str {}, [sp, #{}]", reg, idx * 16)); // spill the incoming float callback argument before loading captures - } - (Arch::AArch64, PhpType::Str) => { - let ptr_reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg); - let len_reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1); - emitter.instruction(&format!("stp {}, {}, [sp, #{}]", ptr_reg, len_reg, idx * 16)); // spill the incoming string callback argument before loading captures - } - (Arch::AArch64, _) => { - let reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg); - emitter.instruction(&format!("str {}, [sp, #{}]", reg, idx * 16)); // spill the incoming scalar callback argument before loading captures - } - (Arch::X86_64, PhpType::Float) => { - let reg = abi::float_arg_reg_name(emitter.target, assignment.start_reg); - abi::store_at_offset(emitter, reg, frame_arg_slot_offset(idx)); - } - (Arch::X86_64, PhpType::Str) => { - let ptr_reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg); - let len_reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1); - abi::store_at_offset(emitter, ptr_reg, frame_arg_slot_offset(idx)); - abi::store_at_offset(emitter, len_reg, frame_arg_slot_offset(idx) - 8); - } - (Arch::X86_64, _) => { - let reg = abi::int_arg_reg_name(emitter.target, assignment.start_reg); - abi::store_at_offset(emitter, reg, frame_arg_slot_offset(idx)); - } - } - } -} - -/// Loads captured values from the closure environment struct (starting at offset 16, slot 0 -/// is the entry point) and spills them to stack slots after the visible args. `env_reg` -/// holds the pointer to the environment struct. -fn spill_captures( - emitter: &mut Emitter, - visible_count: usize, - capture_types: &[PhpType], - env_reg: &str, -) { - for (idx, ty) in capture_types.iter().map(PhpType::codegen_repr).enumerate() { - let arg_idx = visible_count + idx; - let env_offset = (idx + 1) * 16; - match (emitter.target.arch, ty) { - (Arch::AArch64, PhpType::Float) => { - emitter.instruction(&format!("ldr d0, [{}, #{}]", env_reg, env_offset)); // load a captured float from the callback environment - emitter.instruction(&format!("str d0, [sp, #{}]", arg_idx * 16)); // spill the captured float for the final closure call - } - (Arch::AArch64, PhpType::Str) => { - emitter.instruction(&format!("ldr x9, [{}, #{}]", env_reg, env_offset)); // load the captured string pointer from the callback environment - emitter.instruction(&format!("ldr x10, [{}, #{}]", env_reg, env_offset + 8)); // load the captured string length from the callback environment - emitter.instruction(&format!("stp x9, x10, [sp, #{}]", arg_idx * 16)); // spill the captured string pair for the final closure call - } - (Arch::AArch64, PhpType::Void | PhpType::Never) => {} - (Arch::AArch64, _) => { - emitter.instruction(&format!("ldr x9, [{}, #{}]", env_reg, env_offset)); // load a captured scalar/pointer from the callback environment - emitter.instruction(&format!("str x9, [sp, #{}]", arg_idx * 16)); // spill the captured scalar/pointer for the final closure call - } - (Arch::X86_64, PhpType::Float) => { - emitter.instruction(&format!("movsd xmm0, QWORD PTR [{} + {}]", env_reg, env_offset)); // load a captured float from the callback environment - abi::store_at_offset(emitter, "xmm0", frame_arg_slot_offset(arg_idx)); - } - (Arch::X86_64, PhpType::Str) => { - emitter.instruction(&format!("mov r10, QWORD PTR [{} + {}]", env_reg, env_offset)); // load the captured string pointer from the callback environment - emitter.instruction(&format!("mov r11, QWORD PTR [{} + {}]", env_reg, env_offset + 8)); // load the captured string length from the callback environment - abi::store_at_offset(emitter, "r10", frame_arg_slot_offset(arg_idx)); - abi::store_at_offset(emitter, "r11", frame_arg_slot_offset(arg_idx) - 8); - } - (Arch::X86_64, PhpType::Void | PhpType::Never) => {} - (Arch::X86_64, _) => { - emitter.instruction(&format!("mov r10, QWORD PTR [{} + {}]", env_reg, env_offset)); // load a captured scalar/pointer from the callback environment - abi::store_at_offset(emitter, "r10", frame_arg_slot_offset(arg_idx)); - } - } - } -} - -/// Takes the spilled arguments and pushes them onto the standard temporary call stack in -/// preparation for the adapted callback call. Returns the number of overflow bytes pushed -/// so the caller can release them after the call returns. -fn materialize_spilled_args_for_callback( - emitter: &mut Emitter, - incoming_visible_arg_types: &[PhpType], - target_visible_arg_types: &[PhpType], - capture_types: &[PhpType], - frame_size: usize, -) -> usize { - let arg_types = callback_target_arg_types(target_visible_arg_types, capture_types); - push_spilled_args_as_call_temporaries( - emitter, - incoming_visible_arg_types, - target_visible_arg_types, - capture_types, - frame_size, - ); - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - abi::materialize_outgoing_args(emitter, &assignments) -} - -/// ARM64 path: pushes each spilled argument (float, string pair, or scalar) onto the -/// standard temporary call stack for the adapted closure invocation. Arguments are pushed -/// in reverse order so the called function can consume them as overflow parameters. -fn push_spilled_args_as_call_temporaries( - emitter: &mut Emitter, - incoming_visible_arg_types: &[PhpType], - target_visible_arg_types: &[PhpType], - capture_types: &[PhpType], - frame_size: usize, -) { - for (idx, (incoming_ty, target_ty)) in incoming_visible_arg_types - .iter() - .zip(target_visible_arg_types.iter()) - .enumerate() - { - let slot_offset = idx * 16; - let frame_slot_offset = frame_size - 16 - slot_offset; - push_aarch64_visible_arg_as_target(emitter, frame_slot_offset, incoming_ty, target_ty); - } - for (capture_idx, ty) in capture_types.iter().enumerate() { - let idx = incoming_visible_arg_types.len() + capture_idx; - let slot_offset = idx * 16; - let frame_slot_offset = frame_size - 16 - slot_offset; - push_aarch64_prepared_arg(emitter, frame_slot_offset, ty); - } -} - -/// x86_64 path: materializes spilled arguments for the adapted callback call. Returns -/// the number of overflow bytes pushed so the caller can release them after the call. -fn materialize_spilled_args_for_callback_x86_64( - emitter: &mut Emitter, - incoming_visible_arg_types: &[PhpType], - target_visible_arg_types: &[PhpType], - capture_types: &[PhpType], -) -> usize { - let arg_types = callback_target_arg_types(target_visible_arg_types, capture_types); - push_spilled_args_as_call_temporaries_x86_64( - emitter, - incoming_visible_arg_types, - target_visible_arg_types, - capture_types, - ); - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, &arg_types, 0); - abi::materialize_outgoing_args(emitter, &assignments) -} - -/// x86_64 path: pushes each spilled argument onto the standard temporary call stack -/// so the called function consumes them as overflow parameters. Visible arguments may -/// be coerced to target callback types before capture arguments are appended. -fn push_spilled_args_as_call_temporaries_x86_64( - emitter: &mut Emitter, - incoming_visible_arg_types: &[PhpType], - target_visible_arg_types: &[PhpType], - capture_types: &[PhpType], -) { - for (idx, (incoming_ty, target_ty)) in incoming_visible_arg_types - .iter() - .zip(target_visible_arg_types.iter()) - .enumerate() - { - let slot_offset = frame_arg_slot_offset(idx); - push_x86_64_visible_arg_as_target(emitter, slot_offset, incoming_ty, target_ty); - } - for (capture_idx, ty) in capture_types.iter().enumerate() { - let idx = incoming_visible_arg_types.len() + capture_idx; - push_x86_64_prepared_arg(emitter, frame_arg_slot_offset(idx), ty); - } -} - -/// Provides the Callback target arg types helper used by the callback wrapper module. -fn callback_target_arg_types( - target_visible_arg_types: &[PhpType], - capture_types: &[PhpType], -) -> Vec { - target_visible_arg_types - .iter() - .chain(capture_types.iter()) - .map(PhpType::codegen_repr) - .collect() -} - -/// Pushes AArch64 visible arg as target onto the temporary call stack or synthetic metadata list. -fn push_aarch64_visible_arg_as_target( - emitter: &mut Emitter, - frame_slot_offset: usize, - incoming_ty: &PhpType, - target_ty: &PhpType, -) { - if incoming_ty.codegen_repr() == target_ty.codegen_repr() { - push_aarch64_prepared_arg(emitter, frame_slot_offset, target_ty); - return; - } - if incoming_ty.codegen_repr() != PhpType::Mixed { - push_aarch64_prepared_arg(emitter, frame_slot_offset, incoming_ty); - return; - } - - abi::load_at_offset(emitter, "x0", frame_slot_offset); - match target_ty.codegen_repr() { - PhpType::Bool => { - abi::emit_call_label(emitter, "__rt_mixed_cast_bool"); // cast boxed callback argument to bool for the target closure - abi::emit_push_reg(emitter, "x0"); // push the converted bool callback argument - } - PhpType::Int | PhpType::Resource(_) => { - abi::emit_call_label(emitter, "__rt_mixed_cast_int"); // cast boxed callback argument to int for the target closure - abi::emit_push_reg(emitter, "x0"); // push the converted int callback argument - } - PhpType::Float => { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // cast boxed callback argument to float for the target closure - abi::emit_push_float_reg(emitter, "d0"); // push the converted float callback argument - } - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_mixed_cast_string"); // cast boxed callback argument to string for the target closure - abi::emit_push_reg_pair(emitter, "x1", "x2"); // push the converted string callback argument - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // unwrap boxed callback argument for pointer-like target parameters - abi::emit_push_reg(emitter, "x1"); // push the unboxed callback payload pointer - } - } -} - -/// Pushes AArch64 prepared arg onto the temporary call stack or synthetic metadata list. -fn push_aarch64_prepared_arg(emitter: &mut Emitter, frame_slot_offset: usize, ty: &PhpType) { - match ty.codegen_repr() { - PhpType::Float => { - abi::load_at_offset(emitter, "d0", frame_slot_offset); - abi::emit_push_float_reg(emitter, "d0"); // push the prepared float argument onto the standard temporary call stack - } - PhpType::Str => { - abi::load_at_offset(emitter, "x9", frame_slot_offset); - abi::load_at_offset(emitter, "x10", frame_slot_offset - 8); - abi::emit_push_reg_pair(emitter, "x9", "x10"); // push the prepared string argument pair onto the standard temporary call stack - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::load_at_offset(emitter, "x9", frame_slot_offset); - abi::emit_push_reg(emitter, "x9"); // push the prepared scalar/pointer argument onto the standard temporary call stack - } - } -} - -/// Pushes x86 64 visible arg as target onto the temporary call stack or synthetic metadata list. -fn push_x86_64_visible_arg_as_target( - emitter: &mut Emitter, - slot_offset: usize, - incoming_ty: &PhpType, - target_ty: &PhpType, -) { - if incoming_ty.codegen_repr() == target_ty.codegen_repr() { - push_x86_64_prepared_arg(emitter, slot_offset, target_ty); - return; - } - if incoming_ty.codegen_repr() != PhpType::Mixed { - push_x86_64_prepared_arg(emitter, slot_offset, incoming_ty); - return; - } - - abi::load_at_offset(emitter, "rax", slot_offset); - match target_ty.codegen_repr() { - PhpType::Bool => { - abi::emit_call_label(emitter, "__rt_mixed_cast_bool"); // cast boxed callback argument to bool for the target closure - abi::emit_push_reg(emitter, "rax"); // push the converted bool callback argument - } - PhpType::Int | PhpType::Resource(_) => { - abi::emit_call_label(emitter, "__rt_mixed_cast_int"); // cast boxed callback argument to int for the target closure - abi::emit_push_reg(emitter, "rax"); // push the converted int callback argument - } - PhpType::Float => { - abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // cast boxed callback argument to float for the target closure - abi::emit_push_float_reg(emitter, "xmm0"); // push the converted float callback argument - } - PhpType::Str => { - abi::emit_call_label(emitter, "__rt_mixed_cast_string"); // cast boxed callback argument to string for the target closure - abi::emit_push_reg_pair(emitter, "rax", "rdx"); // push the converted string callback argument - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::emit_call_label(emitter, "__rt_mixed_unbox"); // unwrap boxed callback argument for pointer-like target parameters - abi::emit_push_reg(emitter, "rdi"); // push the unboxed callback payload pointer - } - } -} - -/// Pushes x86 64 prepared arg onto the temporary call stack or synthetic metadata list. -fn push_x86_64_prepared_arg(emitter: &mut Emitter, slot_offset: usize, ty: &PhpType) { - match ty.codegen_repr() { - PhpType::Float => { - abi::load_at_offset(emitter, "xmm0", slot_offset); - abi::emit_push_float_reg(emitter, "xmm0"); // push the prepared float argument onto the standard temporary call stack - } - PhpType::Str => { - abi::load_at_offset(emitter, "r10", slot_offset); - abi::load_at_offset(emitter, "r11", slot_offset - 8); - abi::emit_push_reg_pair(emitter, "r10", "r11"); // push the prepared string argument pair onto the standard temporary call stack - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::load_at_offset(emitter, "r10", slot_offset); - abi::emit_push_reg(emitter, "r10"); // push the prepared scalar/pointer argument onto the standard temporary call stack - } - } -} - -/// Returns the fixed stack slot offset for the idx-th incoming frame argument on x86_64. -/// Each slot occupies 16 bytes, and slot 0 is reserved for the return address. -fn frame_arg_slot_offset(idx: usize) -> usize { - (idx + 1) * 16 -} - -/// Rounds `n` up to the nearest 16-byte boundary for stack alignment purposes. -fn align16(n: usize) -> usize { - (n + 15) & !15 -} diff --git a/src/codegen/functions/cleanup.rs b/src/codegen/functions/cleanup.rs deleted file mode 100644 index 21ec6df764..0000000000 --- a/src/codegen/functions/cleanup.rs +++ /dev/null @@ -1,221 +0,0 @@ -//! Purpose: -//! Emits function-scope cleanup for owned locals and structured exit paths. -//! Balances refcounted values before normal returns and exceptional control transfers leave a frame. -//! -//! Called from: -//! - `crate::codegen::functions` and return/throw statement lowering -//! -//! Key details: -//! - Cleanup must follow ownership metadata and avoid releasing borrowed aliases or persistent values. - -use crate::codegen::context::{Context, HeapOwnership}; -use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; -use crate::types::PhpType; - -use super::super::abi; -use super::super::callable_descriptor; - -/// Preserves return registers before epilogue cleanup so the return value survives. -pub(super) fn preserve_return_registers(emitter: &mut Emitter, ctx: &Context, return_ty: &PhpType) { - let return_offset = ctx - .pending_return_value_offset - .expect("codegen bug: missing pending return spill slot"); - super::super::abi::emit_preserve_return_value(emitter, return_ty, return_offset); -} - -/// Restores return registers after epilogue cleanup. -pub(super) fn restore_return_registers(emitter: &mut Emitter, ctx: &Context, return_ty: &PhpType) { - let return_offset = ctx - .pending_return_value_offset - .expect("codegen bug: missing pending return spill slot"); - super::super::abi::emit_restore_return_value(emitter, return_ty, return_offset); -} - -/// Returns true if the function epilogue must emit cleanup for owned locals. -pub(super) fn epilogue_has_side_effects(ctx: &Context) -> bool { - !ctx.static_vars.is_empty() - || !ctx.local_ref_cell_flags.is_empty() - || ctx.variables.iter().any(|(name, var)| { - !ctx.global_vars.contains(name) - && !ctx.static_vars.contains(name) - && !ctx.ref_params.contains(name) - && var.epilogue_cleanup_safe - && var.ownership == HeapOwnership::Owned - && (matches!(var.ty, PhpType::Str | PhpType::Callable) - || var.ty.is_refcounted()) - }) -} - -/// Emits zero-initialization for local reference-cell flags at function entry. -pub(crate) fn emit_local_ref_cell_flag_zero_init(emitter: &mut Emitter, ctx: &Context) { - let mut offsets: Vec<_> = ctx - .local_ref_cell_flags - .values() - .map(|flag| flag.offset) - .collect(); - offsets.sort_unstable(); - for offset in offsets { - abi::emit_store_zero_to_local_slot(emitter, offset); // clear the owned local reference-cell flag at function entry - } -} - -/// Emits cleanup for owned locals at function epilogue (before return). -pub(crate) fn emit_owned_local_epilogue_cleanup( - emitter: &mut Emitter, - ctx: &Context, - label_scope: &str, -) { - let mut cleanup_vars: Vec<_> = ctx - .variables - .iter() - .filter(|(name, var)| { - !ctx.global_vars.contains(*name) - && !ctx.static_vars.contains(*name) - && !ctx.ref_params.contains(*name) - && var.epilogue_cleanup_safe - && var.ownership == HeapOwnership::Owned - }) - .collect(); - cleanup_vars.sort_by_key(|(_, var)| var.stack_offset); - - for (name, var) in cleanup_vars { - match &var.ty { - PhpType::Str => { - emitter.comment(&format!("epilogue cleanup ${}", name)); - super::super::abi::load_at_offset( - emitter, - super::super::abi::int_result_reg(emitter), - var.stack_offset, - ); // load owned string pointer from the local slot into the target integer result register - super::super::abi::emit_call_label(emitter, "__rt_heap_free_safe"); // release owned string storage before returning - } - PhpType::Callable => { - emitter.comment(&format!("epilogue cleanup ${}", name)); - super::super::abi::load_at_offset( - emitter, - super::super::abi::int_result_reg(emitter), - var.stack_offset, - ); // load owned callable descriptor from the local slot into the target integer result register - callable_descriptor::emit_release_current_descriptor(emitter); - } - ty if ty.is_refcounted() => { - emitter.comment(&format!("epilogue cleanup ${}", name)); - super::super::abi::load_at_offset( - emitter, - super::super::abi::int_result_reg(emitter), - var.stack_offset, - ); // load owned heap pointer from the local slot into the target integer result register - super::super::abi::emit_decref_if_refcounted(emitter, ty); - } - _ => {} - } - } - emit_local_ref_cell_epilogue_cleanup(emitter, ctx, label_scope); -} - -/// Emits conditional cleanup for local reference cells at function epilogue. -/// -/// For each reference-cell flag, emits a branch that skips cleanup when the flag -/// indicates borrowed storage. Otherwise loads the cell address and releases it, -/// then zeros the flag. Handles both AArch64 and x86_64 register conventions. -fn emit_local_ref_cell_epilogue_cleanup( - emitter: &mut Emitter, - ctx: &Context, - label_scope: &str, -) { - let mut cleanup_cells: Vec<_> = ctx - .local_ref_cell_flags - .values() - .filter_map(|flag| { - ctx.variables - .get(&flag.variable) - .map(|var| { - ( - flag.variable.as_str(), - flag.offset, - var.stack_offset, - flag.value_ty.clone().unwrap_or_else(|| var.ty.clone()), - ) - }) - }) - .collect(); - cleanup_cells.sort_by_key(|(_, flag_offset, _, _)| *flag_offset); - - for (idx, (name, flag_offset, slot_offset, value_ty)) in cleanup_cells.into_iter().enumerate() - { - let done = format!("{}_local_ref_cell_cleanup_done_{}", label_scope, idx); - emitter.comment(&format!("epilogue cleanup local ref cell ${}", name)); - match emitter.target.arch { - Arch::AArch64 => { - abi::load_at_offset_scratch(emitter, "x10", flag_offset, "x11"); - emitter.instruction(&format!("cbz x10, {}", done)); // skip cleanup when the reference variable is bound to borrowed storage - abi::load_at_offset_scratch(emitter, "x9", slot_offset, "x11"); - abi::emit_release_local_ref_cell(emitter, "x9", &value_ty); - abi::emit_store_zero_to_local_slot(emitter, flag_offset); // mark the owned reference cell as released - } - Arch::X86_64 => { - abi::load_at_offset_scratch(emitter, "r10", flag_offset, "r11"); - emitter.instruction(&format!("test r10, r10")); // check whether this reference variable owns a local cell - emitter.instruction(&format!("je {}", done)); // skip cleanup when the reference variable is bound to borrowed storage - abi::load_at_offset_scratch(emitter, "r11", slot_offset, "r10"); - abi::emit_release_local_ref_cell(emitter, "r11", &value_ty); - abi::emit_store_zero_to_local_slot(emitter, flag_offset); // mark the owned reference cell as released - } - } - emitter.label(&done); - } -} - -/// Pushes the exception-activation record for the current function frame. -pub(super) fn emit_activation_record_push(emitter: &mut Emitter, ctx: &Context, cleanup_label: &str) { - let prev_offset = ctx - .activation_prev_offset - .expect("codegen bug: missing activation prev slot"); - let cleanup_offset = ctx - .activation_cleanup_offset - .expect("codegen bug: missing activation cleanup slot"); - let frame_base_offset = ctx - .activation_frame_base_offset - .expect("codegen bug: missing activation frame-base slot"); - - emitter.comment("register exception cleanup frame"); - let scratch = super::super::abi::temp_int_reg(emitter.target); - super::super::abi::emit_load_symbol_to_reg(emitter, scratch, "_exc_call_frame_top", 0); - super::super::abi::store_at_offset(emitter, scratch, prev_offset); // save the previous call-frame pointer in this frame record - super::super::abi::emit_symbol_address(emitter, scratch, cleanup_label); - super::super::abi::store_at_offset(emitter, scratch, cleanup_offset); // save the cleanup callback address in this frame record - super::super::abi::emit_copy_frame_pointer(emitter, scratch); - super::super::abi::store_at_offset(emitter, scratch, frame_base_offset); // save the current frame pointer in this frame record - super::super::abi::emit_store_zero_to_local_slot( - emitter, - ctx.pending_action_offset - .expect("codegen bug: missing pending-action slot"), - ); // clear pending finally action for this activation - super::super::abi::emit_frame_slot_address(emitter, scratch, prev_offset); // compute the address of this activation record's first slot - super::super::abi::emit_store_reg_to_symbol(emitter, scratch, "_exc_call_frame_top", 0); -} - -/// Pops the exception-activation record for the current function frame. -pub(super) fn emit_activation_record_pop(emitter: &mut Emitter, ctx: &Context) { - let prev_offset = ctx - .activation_prev_offset - .expect("codegen bug: missing activation prev slot"); - - emitter.comment("unregister exception cleanup frame"); - let scratch = super::super::abi::temp_int_reg(emitter.target); - super::super::abi::load_at_offset(emitter, scratch, prev_offset); // reload the previous call-frame pointer from this activation - super::super::abi::emit_store_reg_to_symbol(emitter, scratch, "_exc_call_frame_top", 0); -} - -/// Emits the cleanup callback label that runs owned-local cleanup and then returns. -pub(super) fn emit_frame_cleanup_callback(emitter: &mut Emitter, ctx: &Context, cleanup_label: &str) { - emitter.label(cleanup_label); - super::super::abi::emit_cleanup_callback_prologue( - emitter, - super::super::abi::int_arg_reg_name(emitter.target, 0), - ); - emit_owned_local_epilogue_cleanup(emitter, ctx, cleanup_label); - super::super::abi::emit_cleanup_callback_epilogue(emitter); - emitter.blank(); -} diff --git a/src/codegen/functions/control_flow.rs b/src/codegen/functions/control_flow.rs deleted file mode 100644 index db504739d4..0000000000 --- a/src/codegen/functions/control_flow.rs +++ /dev/null @@ -1,313 +0,0 @@ -//! Purpose: -//! Builds return and exit control-flow labels used while emitting function bodies. -//! Centralizes branch destinations for normal completion, explicit returns, and cleanup callbacks. -//! -//! Called from: -//! - `crate::codegen::functions` during function body emission -//! -//! Key details: -//! - All early exits must route through the same cleanup-aware labels to keep ownership balanced. - -use std::collections::HashMap; - -use crate::codegen::context::{Context, HeapOwnership, TRY_HANDLER_SLOT_SIZE}; -use crate::parser::ast::StmtKind; -use crate::types::{FunctionSig, PhpType}; - -use super::types::infer_local_type; - -/// Marks variables whose assignment within control flow disables epilogue cleanup. -pub(super) fn mark_control_flow_epilogue_unsafe( - stmts: &[crate::parser::ast::Stmt], - ctx: &mut Context, - sig: &FunctionSig, - in_control_flow: bool, -) { - for stmt in stmts { - match &stmt.kind { - StmtKind::Assign { name, .. } => { - if in_control_flow && assignment_needs_epilogue_guard(name, ctx) { - ctx.disable_epilogue_cleanup(name); - } - } - StmtKind::RefAssign { target, .. } => { - if in_control_flow { - ctx.disable_epilogue_cleanup(target); - } - } - StmtKind::ListUnpack { vars, .. } => { - if in_control_flow { - for var in vars { - if assignment_needs_epilogue_guard(var, ctx) { - ctx.disable_epilogue_cleanup(var); - } - } - } - } - StmtKind::Global { vars } => { - for var in vars { - ctx.disable_epilogue_cleanup(var); - } - } - StmtKind::StaticVar { name, .. } => { - ctx.disable_epilogue_cleanup(name); - } - StmtKind::If { - then_body, - elseif_clauses, - else_body, - .. - } => { - let direct_assigns = exhaustive_if_direct_heap_assignments( - then_body, - elseif_clauses, - else_body, - ctx, - sig, - ); - mark_control_flow_epilogue_unsafe(then_body, ctx, sig, true); - for (_, body) in elseif_clauses { - mark_control_flow_epilogue_unsafe(body, ctx, sig, true); - } - if let Some(body) = else_body { - mark_control_flow_epilogue_unsafe(body, ctx, sig, true); - } - for (name, ty) in direct_assigns { - if ctx.global_vars.contains(&name) - || ctx.static_vars.contains(&name) - || ctx.ref_params.contains(&name) - { - continue; - } - let Some(var) = ctx.variables.get(&name) else { - continue; - }; - if var.ty != ty { - continue; - } - ctx.update_var_type_and_ownership( - &name, - ty.clone(), - HeapOwnership::local_owner_for_type(&ty), - ); - ctx.enable_epilogue_cleanup(&name); - } - } - StmtKind::Foreach { - body, - key_var, - value_var, - .. - } => { - ctx.disable_epilogue_cleanup(value_var); - if let Some(key_var) = key_var { - ctx.disable_epilogue_cleanup(key_var); - } - mark_control_flow_epilogue_unsafe(body, ctx, sig, true); - } - StmtKind::DoWhile { body, .. } | StmtKind::While { body, .. } => { - mark_control_flow_epilogue_unsafe(body, ctx, sig, true); - } - StmtKind::For { - init, update, body, .. - } => { - if let Some(stmt) = init { - mark_control_flow_epilogue_unsafe( - std::slice::from_ref(stmt.as_ref()), - ctx, - sig, - true, - ); - } - if let Some(stmt) = update { - mark_control_flow_epilogue_unsafe( - std::slice::from_ref(stmt.as_ref()), - ctx, - sig, - true, - ); - } - mark_control_flow_epilogue_unsafe(body, ctx, sig, true); - } - StmtKind::Switch { cases, default, .. } => { - for (_, body) in cases { - mark_control_flow_epilogue_unsafe(body, ctx, sig, true); - } - if let Some(body) = default { - mark_control_flow_epilogue_unsafe(body, ctx, sig, true); - } - } - StmtKind::Try { - try_body, - catches, - finally_body, - } => { - mark_control_flow_epilogue_unsafe(try_body, ctx, sig, true); - for catch_clause in catches { - mark_control_flow_epilogue_unsafe(&catch_clause.body, ctx, sig, true); - } - if let Some(body) = finally_body { - mark_control_flow_epilogue_unsafe(body, ctx, sig, true); - } - } - _ => {} - } - } -} - -/// Returns true if an assignment to `name` requires an epilogue cleanup guard. -/// -/// A guard is needed when the variable is borrowed, a global, a static, or a -/// by-ref parameter — cases where the normal epilogue cleanup would be incorrect -/// or insufficient. -fn assignment_needs_epilogue_guard(name: &str, ctx: &Context) -> bool { - ctx.variables.get(name).is_some_and(|var| { - var.ownership == HeapOwnership::Borrowed - || ctx.global_vars.contains(name) - || ctx.static_vars.contains(name) - || ctx.ref_params.contains(name) - }) -} - -/// Collects try-handler slot offsets for all try/catch/finally blocks in the statement list. -pub(super) fn collect_try_slots(stmts: &[crate::parser::ast::Stmt], ctx: &mut Context) { - for stmt in stmts { - match &stmt.kind { - StmtKind::Try { - try_body, - catches, - finally_body, - } => { - let slot_offset = ctx.alloc_hidden_slot(TRY_HANDLER_SLOT_SIZE); - ctx.try_slot_offsets.push(slot_offset); - collect_try_slots(try_body, ctx); - for catch_clause in catches { - collect_try_slots(&catch_clause.body, ctx); - } - if let Some(body) = finally_body { - collect_try_slots(body, ctx); - } - } - StmtKind::If { - then_body, - elseif_clauses, - else_body, - .. - } => { - collect_try_slots(then_body, ctx); - for (_, body) in elseif_clauses { - collect_try_slots(body, ctx); - } - if let Some(body) = else_body { - collect_try_slots(body, ctx); - } - } - StmtKind::Foreach { body, .. } - | StmtKind::While { body, .. } - | StmtKind::DoWhile { body, .. } => collect_try_slots(body, ctx), - StmtKind::For { - init, update, body, .. - } => { - if let Some(s) = init { - collect_try_slots(&[*s.clone()], ctx); - } - if let Some(s) = update { - collect_try_slots(&[*s.clone()], ctx); - } - collect_try_slots(body, ctx); - } - StmtKind::Switch { cases, default, .. } => { - for (_, body) in cases { - collect_try_slots(body, ctx); - } - if let Some(body) = default { - collect_try_slots(body, ctx); - } - } - _ => {} - } - } -} - -/// Collects variable assignments from `stmts` that are guaranteed to execute -/// along any straight-line control-flow path (no branches, no loops). -/// -/// Returns a map from variable name to inferred type, and a boolean indicating -/// whether the statement list may fall through (ends with return/break/continue). -/// When `may_fall_through` is false, later statements in the same block are -/// unreachable and are not included. -fn collect_straight_line_direct_assignments( - stmts: &[crate::parser::ast::Stmt], - ctx: &Context, - sig: &FunctionSig, -) -> (HashMap, bool) { - let mut assignments = HashMap::new(); - let mut may_fall_through = true; - - for stmt in stmts { - if !may_fall_through { - break; - } - match &stmt.kind { - StmtKind::Assign { name, value } => { - assignments.insert(name.clone(), infer_local_type(value, sig, Some(ctx))); - } - StmtKind::Return(_) | StmtKind::Break(_) | StmtKind::Continue(_) => { - may_fall_through = false; - } - _ => {} - } - } - - (assignments, may_fall_through) -} - -/// Finds variables that are definitely assigned in every branch of an if/elseif/else -/// construct where all branches fall through. -/// -/// For each branch that may fall through, collects direct (non-conditional) assignments. -/// A variable is "definitely assigned" when it receives the same refcounted type in every -/// such branch. These variables can have their epilogue cleanup re-enabled after the if -/// statement, because the control flow merge guarantees the assignment executes regardless -/// of which branch is taken. -fn exhaustive_if_direct_heap_assignments( - then_body: &[crate::parser::ast::Stmt], - elseif_clauses: &[(crate::parser::ast::Expr, Vec)], - else_body: &Option>, - ctx: &Context, - sig: &FunctionSig, -) -> HashMap { - let Some(else_body) = else_body.as_ref() else { - return HashMap::new(); - }; - - let mut branch_assignments = Vec::new(); - let (then_assigns, then_falls_through) = - collect_straight_line_direct_assignments(then_body, ctx, sig); - if then_falls_through { - branch_assignments.push(then_assigns); - } - for (_, body) in elseif_clauses { - let (assigns, falls_through) = collect_straight_line_direct_assignments(body, ctx, sig); - if falls_through { - branch_assignments.push(assigns); - } - } - let (else_assigns, else_falls_through) = - collect_straight_line_direct_assignments(else_body, ctx, sig); - if else_falls_through { - branch_assignments.push(else_assigns); - } - - let Some((first_branch, remaining_branches)) = branch_assignments.split_first() else { - return HashMap::new(); - }; - let mut definitely_assigned = first_branch.clone(); - definitely_assigned.retain(|name, ty| { - (matches!(ty, PhpType::Str | PhpType::Callable) || ty.is_refcounted()) - && remaining_branches - .iter() - .all(|assigns| assigns.get(name) == Some(ty)) - }); - definitely_assigned -} diff --git a/src/codegen/functions/fiber_wrapper.rs b/src/codegen/functions/fiber_wrapper.rs deleted file mode 100644 index 7ef6d8773a..0000000000 --- a/src/codegen/functions/fiber_wrapper.rs +++ /dev/null @@ -1,806 +0,0 @@ -//! Purpose: -//! Emits deferred fiber wrapper functions for callable bodies that execute inside runtime fibers. -//! Stitches closure captures, parameters, and resume results into normal function emission. -//! -//! Called from: -//! - `crate::codegen::functions` after deferred fiber wrappers are registered -//! -//! Key details: -//! - Wrapper frames must preserve captured values and follow the same cleanup rules as user functions. - -use crate::codegen::context::DeferredFiberWrapper; -use crate::codegen::emit::Emitter; -use crate::codegen::expr::arrays::emit_array_value_type_stamp; -use crate::codegen::platform::Arch; -use crate::codegen::{abi, callable_descriptor, runtime}; -use crate::types::PhpType; - -/// Emits a fiber wrapper that adapts a closure to run inside a runtime Fiber. -pub(crate) fn emit_fiber_wrapper(emitter: &mut Emitter, wrapper: &DeferredFiberWrapper) { - if wrapper.use_descriptor_invoker { - emit_descriptor_invoker_wrapper(emitter, &wrapper.label); - return; - } - - if emitter.target.arch == Arch::X86_64 { - emit_x86_64_wrapper(emitter, wrapper); - return; - } - - let arg_types = wrapper_arg_types(wrapper); - let slot_count = arg_types.len().max(1); - let frame_size = align16(slot_count * 16 + 64); - let saved_callee_offset = frame_size - 64; - - emitter.blank(); - emitter.comment(&format!("fiber wrapper: {}", wrapper.label)); - emitter.raw(".align 2"); - emitter.label_global(&wrapper.label); - abi::emit_frame_prologue(emitter, frame_size); - emitter.instruction(&format!("stp x19, x20, [sp, #{}]", saved_callee_offset)); // preserve the fiber pointer and callable entry across helper calls - emitter.instruction(&format!("str x21, [sp, #{}]", saved_callee_offset + 16)); // preserve the callable descriptor across helper calls - emitter.instruction(&format!("stp x22, x23, [sp, #{}]", saved_callee_offset + 32)); // preserve variadic tail scratch registers across helper calls - emitter.instruction("mov x19, x0"); // x19 = Fiber object passed by __rt_fiber_entry - emitter.instruction(&format!("ldr x20, [x19, #{}]", runtime::FIBER_CALLABLE_OFFSET)); // x20 = callable descriptor stored on the Fiber - emitter.instruction("mov x21, x20"); // x21 = descriptor pointer kept for hidden capture reloads - callable_descriptor::emit_load_entry_from_descriptor(emitter, "x20", "x20"); - - spill_wrapper_args(emitter, wrapper, &arg_types, "x21"); - let overflow_bytes = materialize_spilled_args_for_closure_call(emitter, &arg_types, frame_size); - let call_stack_padding = if overflow_bytes > 0 { 16 } else { 0 }; - abi::emit_reserve_temporary_stack(emitter, call_stack_padding); // leave the first spilled callback argument where the callee expects it - - emitter.instruction("blr x20"); // call the original closure with ABI-correct arguments - abi::emit_release_temporary_stack(emitter, call_stack_padding); // drop the wrapper-only caller-stack alignment pad - abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop stack-passed closure arguments after the Fiber callback returns - box_wrapper_return(emitter, wrapper.sig.return_type.codegen_repr()); - - emitter.instruction(&format!("ldp x22, x23, [sp, #{}]", saved_callee_offset + 32)); // restore variadic tail scratch registers - emitter.instruction(&format!("ldr x21, [sp, #{}]", saved_callee_offset + 16)); // restore the caller's descriptor scratch register - emitter.instruction(&format!("ldp x19, x20, [sp, #{}]", saved_callee_offset)); // restore callee-saved wrapper registers - abi::emit_frame_restore(emitter, frame_size); - abi::emit_return(emitter); -} - -/// Emits a generic Fiber wrapper that invokes the callable descriptor's uniform invoker. -fn emit_descriptor_invoker_wrapper(emitter: &mut Emitter, label: &str) { - if emitter.target.arch == Arch::X86_64 { - emit_x86_64_descriptor_invoker_wrapper(emitter, label); - } else { - emit_aarch64_descriptor_invoker_wrapper(emitter, label); - } -} - -/// Emits the ARM64 descriptor-invoker Fiber wrapper. -fn emit_aarch64_descriptor_invoker_wrapper(emitter: &mut Emitter, label: &str) { - let frame_size = 96; - let missing_label = format!("{}_missing_invoker", label); - let loop_label = format!("{}_copy_args", label); - let copy_done_label = format!("{}_args_done", label); - let return_label = format!("{}_return", label); - - emitter.blank(); - emitter.comment(&format!("fiber descriptor invoker wrapper: {}", label)); - emitter.raw(".align 2"); - emitter.label_global(label); - abi::emit_frame_prologue(emitter, frame_size); - emitter.instruction("stp x19, x20, [sp, #0]"); // preserve Fiber and descriptor registers across nested helper calls - emitter.instruction("stp x21, x22, [sp, #16]"); // preserve start-argument count and array registers - emitter.instruction("stp x23, x24, [sp, #32]"); // preserve loop index and invoker/result scratch registers - emitter.instruction("str x25, [sp, #48]"); // preserve the boxed argument-container register - - emitter.instruction("mov x19, x0"); // x19 = Fiber object passed by __rt_fiber_entry - emitter.instruction(&format!("ldr x20, [x19, #{}]", runtime::FIBER_CALLABLE_OFFSET)); // x20 = callable descriptor stored on the Fiber - emitter.instruction(&format!("ldr x21, [x19, #{}]", runtime::FIBER_START_ARG_COUNT_OFFSET)); // x21 = number of boxed start() values to forward - emit_allocate_descriptor_start_arg_array_aarch64(emitter); - emit_copy_fiber_start_args_to_array_aarch64(emitter, &loop_label, ©_done_label); - emit_box_descriptor_start_arg_array(emitter, "x22", "x1"); - - emitter.instruction("mov x25, x1"); // keep the boxed argument array alive across the descriptor invocation - emitter.instruction("mov x0, x20"); // pass callable descriptor as invoker argument 1 - callable_descriptor::emit_load_invoker_from_descriptor(emitter, "x24", "x20"); - emitter.instruction(&format!("cbz x24, {}", missing_label)); // reject descriptors that do not expose the uniform invoker slot - emitter.instruction("mov x1, x25"); // pass boxed start-argument array as invoker argument 2 - emitter.instruction("blr x24"); // invoke descriptor adapter; x0 = boxed Mixed return value - emitter.instruction("mov x24, x0"); // preserve the Fiber callback return while releasing the argument container - emitter.instruction("mov x0, x25"); // move the boxed argument container into the decref helper input - emitter.instruction("bl __rt_decref_mixed"); // release the temporary boxed argument container - emitter.instruction("mov x0, x24"); // restore the callback return value for __rt_fiber_entry - emitter.instruction(&format!("b {}", return_label)); // skip the missing-invoker diagnostic path - - emitter.label(&missing_label); - emitter.instruction("mov x0, x25"); // move the boxed argument container into the decref helper before throwing - emitter.instruction("bl __rt_decref_mixed"); // release the temporary boxed argument container on the error path - abi::emit_symbol_address(emitter, "x0", "_fiber_msg_unsupported_callable"); // x0 = pointer to the unsupported-callable diagnostic - emitter.instruction("mov x1, #48"); // x1 = diagnostic byte length - emitter.instruction("bl __rt_fiber_throw_state_error"); // raise FiberError through the fiber boundary handler - emitter.instruction("brk #0xfffe"); // defensive trap: the throw helper must not return - - emitter.label(&return_label); - emitter.instruction("ldr x25, [sp, #48]"); // restore the caller's x25 register - emitter.instruction("ldp x23, x24, [sp, #32]"); // restore loop/index scratch callee-saved registers - emitter.instruction("ldp x21, x22, [sp, #16]"); // restore count/array callee-saved registers - emitter.instruction("ldp x19, x20, [sp, #0]"); // restore Fiber/descriptor callee-saved registers - abi::emit_frame_restore(emitter, frame_size); - abi::emit_return(emitter); -} - -/// Allocates the Mixed-pointer argument array used by an ARM64 descriptor invoker. -fn emit_allocate_descriptor_start_arg_array_aarch64(emitter: &mut Emitter) { - emitter.instruction("mov x0, #4"); // default descriptor argument-array capacity - emitter.instruction("cmp x21, #4"); // does the actual start() arity exceed the small-array default? - emitter.instruction("csel x0, x21, x0, hi"); // use the actual arity when it is larger than four - emitter.instruction("mov x1, #8"); // descriptor argument arrays store boxed Mixed pointers - emitter.instruction("bl __rt_array_new"); // allocate the descriptor invoker argument array - emitter.instruction("mov x22, x0"); // keep the argument array pointer across element retains - emit_array_value_type_stamp(emitter, "x22", &PhpType::Mixed); -} - -/// Copies Fiber start arguments into an ARM64 Mixed-pointer array, retaining each cell. -fn emit_copy_fiber_start_args_to_array_aarch64( - emitter: &mut Emitter, - loop_label: &str, - done_label: &str, -) { - emitter.instruction("mov x23, #0"); // start copying at start_args[0] - emitter.label(loop_label); - emitter.instruction("cmp x23, x21"); // have all supplied start() arguments been copied? - emitter.instruction(&format!("b.hs {}", done_label)); // leave the copy loop once index >= count - emitter.instruction("lsl x9, x23, #3"); // convert the argument index into an 8-byte slot offset - emitter.instruction(&format!("add x10, x19, #{}", runtime::FIBER_START_ARGS_OFFSET)); // x10 = base of Fiber start_args storage - emitter.instruction("ldr x0, [x10, x9]"); // load the boxed Mixed start argument - emitter.instruction("bl __rt_incref"); // retain the boxed Mixed cell for the temporary invoker array - emitter.instruction("lsl x9, x23, #3"); // recompute the element offset after the retain helper clobbers scratch regs - emitter.instruction("add x11, x22, #24"); // x11 = first payload slot of the descriptor argument array - emitter.instruction("add x11, x11, x9"); // x11 = destination slot for the current boxed argument - emitter.instruction("str x0, [x11]"); // store the retained boxed Mixed pointer into the argument array - emitter.instruction("add x23, x23, #1"); // advance to the next supplied start() argument - emitter.instruction(&format!("b {}", loop_label)); // continue copying boxed start() arguments - emitter.label(done_label); - emitter.instruction("str x21, [x22]"); // publish the argument array length after all payload slots are initialized -} - -/// Boxes the descriptor start-argument array into the invoker's target argument register. -fn emit_box_descriptor_start_arg_array(emitter: &mut Emitter, source_reg: &str, dest_reg: &str) { - let array_ty = PhpType::Array(Box::new(PhpType::Mixed)); - emitter.instruction(&format!("mov {}, {}", dest_reg, source_reg)); // move the argument array pointer into the invoker argument register - crate::codegen::builtins::arrays::call_user_func_array::emit_box_invoker_arg_clone_as_mixed( - dest_reg, - &array_ty, - emitter, - ); -} - -/// Emits the x86_64 descriptor-invoker Fiber wrapper. -fn emit_x86_64_descriptor_invoker_wrapper(emitter: &mut Emitter, label: &str) { - let frame_size = 96; - let saved_fiber_offset = 16; - let saved_descriptor_offset = 24; - let saved_count_offset = 32; - let saved_array_offset = 40; - let saved_argbox_offset = 48; - let missing_label = format!("{}_missing_invoker", label); - let loop_label = format!("{}_copy_args", label); - let copy_done_label = format!("{}_args_done", label); - let return_label = format!("{}_return", label); - - emitter.blank(); - emitter.comment(&format!("fiber descriptor invoker wrapper: {}", label)); - emitter.raw(".align 16"); - emitter.label_global(label); - abi::emit_frame_prologue(emitter, frame_size); - abi::store_at_offset(emitter, "r12", saved_fiber_offset); - abi::store_at_offset(emitter, "r13", saved_descriptor_offset); - abi::store_at_offset(emitter, "r14", saved_count_offset); - abi::store_at_offset(emitter, "r15", saved_array_offset); - abi::store_at_offset(emitter, "rbx", saved_argbox_offset); - - emitter.instruction("mov r12, rdi"); // r12 = Fiber object passed by __rt_fiber_entry - emitter.instruction(&format!("mov r13, QWORD PTR [r12 + {}]", runtime::FIBER_CALLABLE_OFFSET)); // r13 = callable descriptor stored on the Fiber - emitter.instruction(&format!("mov r14, QWORD PTR [r12 + {}]", runtime::FIBER_START_ARG_COUNT_OFFSET)); // r14 = number of boxed start() values to forward - emit_allocate_descriptor_start_arg_array_x86_64(emitter); - emit_copy_fiber_start_args_to_array_x86_64(emitter, &loop_label, ©_done_label); - emit_box_descriptor_start_arg_array(emitter, "r15", "rsi"); - - emitter.instruction("mov rbx, rsi"); // keep the boxed argument array alive across the descriptor invocation - emitter.instruction("mov rdi, r13"); // pass callable descriptor as invoker argument 1 - callable_descriptor::emit_load_invoker_from_descriptor(emitter, "r10", "r13"); - emitter.instruction(&format!("test r10, r10")); // check whether the descriptor exposes a uniform invoker slot - emitter.instruction(&format!("je {}", missing_label)); // reject descriptors that cannot be called through the generic path - emitter.instruction("mov rsi, rbx"); // pass boxed start-argument array as invoker argument 2 - emitter.instruction("call r10"); // invoke descriptor adapter; rax = boxed Mixed return value - emitter.instruction("mov r15, rax"); // preserve the Fiber callback return while releasing the argument container - emitter.instruction("mov rax, rbx"); // move the boxed argument container into the decref helper input - emitter.instruction("call __rt_decref_mixed"); // release the temporary boxed argument container - emitter.instruction("mov rax, r15"); // restore the callback return value for __rt_fiber_entry - emitter.instruction(&format!("jmp {}", return_label)); // skip the missing-invoker diagnostic path - - emitter.label(&missing_label); - emitter.instruction("mov rax, rbx"); // move the boxed argument container into the decref helper before throwing - emitter.instruction("call __rt_decref_mixed"); // release the temporary boxed argument container on the error path - abi::emit_symbol_address(emitter, "rdi", "_fiber_msg_unsupported_callable"); // rdi = pointer to the unsupported-callable diagnostic - emitter.instruction("mov esi, 48"); // rsi = diagnostic byte length - emitter.instruction("call __rt_fiber_throw_state_error"); // raise FiberError through the fiber boundary handler - emitter.instruction("ud2"); // defensive trap: the throw helper must not return - - emitter.label(&return_label); - abi::load_at_offset(emitter, "rbx", saved_argbox_offset); - abi::load_at_offset(emitter, "r15", saved_array_offset); - abi::load_at_offset(emitter, "r14", saved_count_offset); - abi::load_at_offset(emitter, "r13", saved_descriptor_offset); - abi::load_at_offset(emitter, "r12", saved_fiber_offset); - abi::emit_frame_restore(emitter, frame_size); - abi::emit_return(emitter); -} - -/// Allocates the Mixed-pointer argument array used by an x86_64 descriptor invoker. -fn emit_allocate_descriptor_start_arg_array_x86_64(emitter: &mut Emitter) { - emitter.instruction("mov rdi, 4"); // default descriptor argument-array capacity - emitter.instruction("cmp r14, 4"); // does the actual start() arity exceed the small-array default? - emitter.instruction("cmova rdi, r14"); // use the actual arity when it is larger than four - emitter.instruction("mov rsi, 8"); // descriptor argument arrays store boxed Mixed pointers - emitter.instruction("call __rt_array_new"); // allocate the descriptor invoker argument array - emitter.instruction("mov r15, rax"); // keep the argument array pointer across element retains - emit_array_value_type_stamp(emitter, "r15", &PhpType::Mixed); -} - -/// Copies Fiber start arguments into an x86_64 Mixed-pointer array, retaining each cell. -fn emit_copy_fiber_start_args_to_array_x86_64( - emitter: &mut Emitter, - loop_label: &str, - done_label: &str, -) { - emitter.instruction("xor ebx, ebx"); // start copying at start_args[0] - emitter.label(loop_label); - emitter.instruction("cmp rbx, r14"); // have all supplied start() arguments been copied? - emitter.instruction(&format!("jae {}", done_label)); // leave the copy loop once index >= count - emitter.instruction(&format!("mov rax, QWORD PTR [r12 + rbx * 8 + {}]", runtime::FIBER_START_ARGS_OFFSET)); // load the boxed Mixed start argument - emitter.instruction("call __rt_incref"); // retain the boxed Mixed cell for the temporary invoker array - emitter.instruction("mov QWORD PTR [r15 + 24 + rbx * 8], rax"); // store the retained boxed Mixed pointer into the argument array - emitter.instruction("add rbx, 1"); // advance to the next supplied start() argument - emitter.instruction(&format!("jmp {}", loop_label)); // continue copying boxed start() arguments - emitter.label(done_label); - emitter.instruction("mov QWORD PTR [r15], r14"); // publish the argument array length after all payload slots are initialized -} - -/// Spills visible parameters and hidden arguments from the Fiber's argument storage -/// into the wrapper's stack frame, calculating how many integer/float registers each -/// argument consumes so the caller's ABI expectations are met at the final call site. -/// Visible params are read directly from the Fiber's start arguments; hidden args come -/// after and use the same offset scheme. -fn spill_wrapper_args( - emitter: &mut Emitter, - wrapper: &DeferredFiberWrapper, - arg_types: &[PhpType], - descriptor_reg: &str, -) { - let visible = fixed_visible_param_count(wrapper).min(arg_types.len()); - - for (idx, ty) in arg_types.iter().take(visible).enumerate() { - spill_user_arg(emitter, idx, ty, idx * 16); - } - - let hidden_start = if let Some(variadic_idx) = variadic_param_index(wrapper) { - spill_variadic_start_arg_array_aarch64(emitter, wrapper, visible, variadic_idx * 16); - variadic_idx + 1 - } else { - visible - }; - - for (idx, ty) in arg_types.iter().enumerate().skip(hidden_start) { - let slot_offset = idx * 16; - spill_descriptor_hidden_arg( - emitter, - wrapper, - descriptor_reg, - idx - hidden_start, - ty, - slot_offset, - ); - } -} - -/// Returns the count of fixed visible Fiber callback parameters before any variadic tail. -fn fixed_visible_param_count(wrapper: &DeferredFiberWrapper) -> usize { - variadic_param_index(wrapper).unwrap_or(wrapper.visible_param_count) -} - -/// Returns the wrapper signature index of the variadic parameter, if present. -fn variadic_param_index(wrapper: &DeferredFiberWrapper) -> Option { - let variadic_name = wrapper.sig.variadic.as_ref()?; - wrapper - .sig - .params - .iter() - .position(|(name, _)| name == variadic_name) -} - -/// Builds and spills the `array` variadic tail for an ARM64 Fiber callback wrapper. -fn spill_variadic_start_arg_array_aarch64( - emitter: &mut Emitter, - wrapper: &DeferredFiberWrapper, - fixed_param_count: usize, - slot_offset: usize, -) { - let has_tail_label = format!("{}_variadic_tail", wrapper.label); - let tail_ready_label = format!("{}_variadic_tail_ready", wrapper.label); - let loop_label = format!("{}_variadic_copy", wrapper.label); - let done_label = format!("{}_variadic_done", wrapper.label); - - emitter.instruction(&format!("ldr x22, [x19, #{}]", runtime::FIBER_START_ARG_COUNT_OFFSET)); // x22 = number of boxed start() values supplied by the caller - if fixed_param_count > 0 { - emitter.instruction(&format!("cmp x22, #{}", fixed_param_count)); // did start() supply values beyond the fixed Fiber callback params? - emitter.instruction(&format!("b.hi {}", has_tail_label)); // compute a non-empty tail when supplied args exceed fixed params - emitter.instruction("mov x22, #0"); // no variadic tail values were supplied - emitter.instruction(&format!("b {}", tail_ready_label)); // skip the positive-tail subtraction path - emitter.label(&has_tail_label); - emitter.instruction(&format!("sub x22, x22, #{}", fixed_param_count)); // x22 = number of values collected into ...$args - emitter.label(&tail_ready_label); - } - - emitter.instruction("mov x0, #4"); // default variadic tail array capacity - emitter.instruction("cmp x22, #4"); // does the actual variadic tail exceed the small-array default? - emitter.instruction("csel x0, x22, x0, hi"); // use the actual tail count when it is larger than four - emitter.instruction("mov x1, #8"); // variadic tail arrays store boxed Mixed pointers - emitter.instruction("bl __rt_array_new"); // allocate the Fiber variadic tail array - emitter.instruction(&format!("str x0, [sp, #{}]", slot_offset)); // spill the variadic array pointer for the final closure call - emit_array_value_type_stamp(emitter, "x0", &PhpType::Mixed); - - emitter.instruction("mov x23, #0"); // start copying tail values at ...$args[0] - emitter.label(&loop_label); - emitter.instruction("cmp x23, x22"); // have all supplied variadic tail values been copied? - emitter.instruction(&format!("b.hs {}", done_label)); // leave the copy loop once tail index >= tail count - if fixed_param_count > 0 { - emitter.instruction(&format!("add x9, x23, #{}", fixed_param_count)); // map tail index to the matching Fiber start_args index - } else { - emitter.instruction("mov x9, x23"); // tail index already matches the Fiber start_args index - } - emitter.instruction("lsl x10, x9, #3"); // convert the start_args index into an 8-byte slot offset - emitter.instruction(&format!("add x11, x19, #{}", runtime::FIBER_START_ARGS_OFFSET)); // x11 = base of Fiber start_args storage - emitter.instruction("ldr x0, [x11, x10]"); // load the boxed Mixed tail value - emitter.instruction("bl __rt_incref"); // retain the boxed Mixed cell for the variadic tail array - emitter.instruction(&format!("ldr x12, [sp, #{}]", slot_offset)); // reload the variadic array pointer after the retain helper - emitter.instruction("lsl x10, x23, #3"); // convert the variadic tail index into an 8-byte payload offset - emitter.instruction("add x12, x12, #24"); // x12 = first payload slot of the variadic tail array - emitter.instruction("add x12, x12, x10"); // x12 = destination slot for the current tail value - emitter.instruction("str x0, [x12]"); // store the retained boxed Mixed pointer into the variadic array - emitter.instruction("add x23, x23, #1"); // advance to the next variadic tail value - emitter.instruction(&format!("b {}", loop_label)); // continue copying Fiber start_args into ...$args - emitter.label(&done_label); - emitter.instruction(&format!("ldr x12, [sp, #{}]", slot_offset)); // reload the variadic array pointer before publishing its length - emitter.instruction("str x22, [x12]"); // publish the final variadic tail array length -} - -/// Spills one hidden Fiber callback argument from the callable descriptor's runtime capture slots. -fn spill_descriptor_hidden_arg( - emitter: &mut Emitter, - wrapper: &DeferredFiberWrapper, - descriptor_reg: &str, - capture_index: usize, - ty: &PhpType, - slot_offset: usize, -) { - callable_descriptor::emit_load_runtime_capture_to_result( - emitter, - descriptor_reg, - capture_index, - ty, - ); - match ty { - PhpType::Float => { - emitter.instruction(&format!("str d0, [sp, #{}]", slot_offset)); // spill the descriptor-captured float for the final call - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - emitter.instruction(&format!("stp {}, {}, [sp, #{}]", ptr_reg, len_reg, slot_offset)); // spill the descriptor-captured string pair for the final call - } - PhpType::Void | PhpType::Never => {} - _ => { - emitter.instruction(&format!("str {}, [sp, #{}]", abi::int_result_reg(emitter), slot_offset)); // spill the descriptor-captured payload for the final call - retain_refcounted_capture_for_closure_frame( - emitter, - wrapper, - ty, - abi::int_result_reg(emitter), - ); - } - } -} - -/// Retains a refcounted hidden capture for the closure parameter frame. -/// -/// The callable descriptor remains the persistent owner of its capture slots. The -/// legacy closure frame cleans up hidden parameters like ordinary arguments, so that -/// wrapper mode must hand it a separate retained owner for refcounted values. EIR -/// closure params are borrowed and therefore skip this retain. -fn retain_refcounted_capture_for_closure_frame( - emitter: &mut Emitter, - wrapper: &DeferredFiberWrapper, - ty: &PhpType, - value_reg: &str, -) { - if !wrapper.retain_hidden_args_for_closure_call { - return; - } - if !ty.is_refcounted() && !matches!(ty.codegen_repr(), PhpType::Callable) { - return; - } - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction(&format!("mov x0, {}", value_reg)); // pass the descriptor capture to the retain helper - emitter.instruction("bl __rt_incref"); // retain it for the closure frame's normal parameter cleanup - } - Arch::X86_64 => { - emitter.instruction(&format!("mov rax, {}", value_reg)); // pass the descriptor capture to the retain helper - emitter.instruction("call __rt_incref"); // retain it for the closure frame's normal parameter cleanup - } - } -} - -/// Builds the argument type list for a fiber wrapper by mapping the wrapper's visible -/// parameters and hidden arguments to their codegen representations, in order. -fn wrapper_arg_types(wrapper: &DeferredFiberWrapper) -> Vec { - wrapper - .sig - .params - .iter() - .map(|(_, ty)| ty.codegen_repr()) - .chain(wrapper.hidden_arg_types.iter().map(PhpType::codegen_repr)) - .collect() -} - -/// Loads a visible user parameter from the Fiber object's argument area and spills it -/// into the wrapper's stack frame at the slot corresponding to its parameter index. -/// Unboxes the boxed Mixed argument from the Fiber's start area; for Float and Str -/// types also handles the ABI register layout transformation for the final call. -fn spill_user_arg(emitter: &mut Emitter, param_idx: usize, ty: &PhpType, slot_offset: usize) { - let src_offset = runtime::FIBER_START_ARGS_OFFSET + (param_idx as i32) * 8; - emitter.instruction(&format!("ldr x0, [x19, #{}]", src_offset)); // load the boxed Mixed start() argument from the Fiber object - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - emitter.instruction(&format!("str x0, [sp, #{}]", slot_offset)); // pass mixed parameters as their boxed cell pointer - return; - } - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); - match ty { - PhpType::Float => { - emitter.instruction("fmov d0, x1"); // reinterpret the unboxed float payload bits as d0 - emitter.instruction(&format!("str d0, [sp, #{}]", slot_offset)); // spill the normalized float argument for the final call - } - PhpType::Str => { - emitter.instruction(&format!("stp x1, x2, [sp, #{}]", slot_offset)); // spill the unboxed string pointer and length for the final call - } - PhpType::Void | PhpType::Never => {} - _ => { - emitter.instruction(&format!("str x1, [sp, #{}]", slot_offset)); // spill the unboxed scalar/pointer payload for the final call - } - } -} - -/// Materializes the spilled wrapper arguments back into ABI registers/stack slots for -/// the closure call. First pushes all spilled args as call temporaries onto the -/// temporary stack, then builds outgoing argument assignments for the target and -/// materializes them. Returns the number of overflow bytes that must be cleaned up -/// after the call returns. -fn materialize_spilled_args_for_closure_call( - emitter: &mut Emitter, - arg_types: &[PhpType], - frame_size: usize, -) -> usize { - push_spilled_args_as_call_temporaries(emitter, arg_types, frame_size); - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, arg_types, 0); - abi::materialize_outgoing_args(emitter, &assignments) -} - -/// Pushes each spilled wrapper argument from its frame slot onto the temporary call -/// stack in preparation for the closure call. Arguments are pushed in reverse order -/// so they land at the correct stack offsets for the callee; Float, Str, and scalar -/// types each use their appropriate register-pair or single-register push sequence. -fn push_spilled_args_as_call_temporaries( - emitter: &mut Emitter, - arg_types: &[PhpType], - frame_size: usize, -) { - for (idx, ty) in arg_types.iter().enumerate() { - let slot_offset = idx * 16; - let frame_slot_offset = frame_size - 16 - slot_offset; - match ty.codegen_repr() { - PhpType::Float => { - let reg = match emitter.target.arch { - Arch::AArch64 => "d0", - Arch::X86_64 => "xmm0", - }; - abi::load_at_offset(emitter, reg, frame_slot_offset); - abi::emit_push_float_reg(emitter, reg); // push the prepared float argument onto the standard temporary call stack - } - PhpType::Str => { - let (ptr_reg, len_reg) = match emitter.target.arch { - Arch::AArch64 => ("x9", "x10"), - Arch::X86_64 => ("r10", "r11"), - }; - abi::load_at_offset(emitter, ptr_reg, frame_slot_offset); - abi::load_at_offset(emitter, len_reg, frame_slot_offset - 8); - abi::emit_push_reg_pair(emitter, ptr_reg, len_reg); // push the prepared string argument pair onto the standard temporary call stack - } - PhpType::Void | PhpType::Never => {} - _ => { - let reg = match emitter.target.arch { - Arch::AArch64 => "x9", - Arch::X86_64 => "r10", - }; - abi::load_at_offset(emitter, reg, frame_slot_offset); - abi::emit_push_reg(emitter, reg); // push the prepared scalar/pointer argument onto the standard temporary call stack - } - } - } -} - -/// Boxes the closure's raw return value into a Mixed cell for the Fiber's result slot. -/// For Void/Never return types, normalizes the implicit null to 0/NULL so the boxed -/// representation is consistent before the wrapper returns to the fiber entry point. -fn box_wrapper_return(emitter: &mut Emitter, return_ty: PhpType) { - if matches!(return_ty, PhpType::Void | PhpType::Never) { - match emitter.target.arch { - Arch::AArch64 => { - emitter.instruction("mov x0, #0"); // normalize implicit/null closure returns before boxing as Mixed - } - Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // normalize implicit/null closure returns before boxing as Mixed - } - } - } - crate::codegen::emit_box_current_value_as_mixed(emitter, &return_ty); -} - -/// x86_64-specific fiber wrapper emission. Uses the System V AMD64 ABI for both the -/// wrapper frame and the closure call; preserves r12/r13 as callee-saved Fiber/callable -/// pointers across the call. Differs from ARM64 in register layout and frame slot indexing. -fn emit_x86_64_wrapper(emitter: &mut Emitter, wrapper: &DeferredFiberWrapper) { - let arg_types = wrapper_arg_types(wrapper); - let slot_count = arg_types.len().max(1); - let frame_size = align16(slot_count * 16 + 80); - let saved_fiber_offset = slot_count * 16 + 16; - let saved_callable_offset = slot_count * 16 + 24; - let saved_descriptor_offset = slot_count * 16 + 32; - let saved_tail_count_offset = slot_count * 16 + 40; - let saved_tail_index_offset = slot_count * 16 + 48; - - emitter.blank(); - emitter.comment(&format!("fiber wrapper: {}", wrapper.label)); - emitter.raw(".align 16"); - emitter.label_global(&wrapper.label); - abi::emit_frame_prologue(emitter, frame_size); - abi::store_at_offset(emitter, "r12", saved_fiber_offset); // preserve the caller's r12 before caching the Fiber pointer - abi::store_at_offset(emitter, "r13", saved_callable_offset); // preserve the caller's r13 before caching the callable entry - abi::store_at_offset(emitter, "r14", saved_descriptor_offset); // preserve the caller's r14 before caching the descriptor - abi::store_at_offset(emitter, "r15", saved_tail_count_offset); // preserve the caller's r15 before caching variadic tail count - abi::store_at_offset(emitter, "rbx", saved_tail_index_offset); // preserve the caller's rbx before using it as a tail copy index - emitter.instruction("mov r12, rdi"); // r12 = Fiber object passed by __rt_fiber_entry - emitter.instruction(&format!("mov r13, QWORD PTR [r12 + {}]", runtime::FIBER_CALLABLE_OFFSET)); // r13 = callable descriptor stored on the Fiber - emitter.instruction("mov r14, r13"); // r14 = descriptor pointer kept for hidden capture reloads - callable_descriptor::emit_load_entry_from_descriptor(emitter, "r13", "r13"); - - spill_wrapper_args_x86_64(emitter, wrapper, &arg_types, "r14"); - let overflow_bytes = materialize_spilled_args_for_closure_call_x86_64(emitter, &arg_types); - abi::emit_call_reg(emitter, "r13"); - abi::emit_release_temporary_stack(emitter, overflow_bytes); // drop stack-passed closure arguments after the Fiber callback returns - box_wrapper_return(emitter, wrapper.sig.return_type.codegen_repr()); - - abi::load_at_offset(emitter, "rbx", saved_tail_index_offset); - abi::load_at_offset(emitter, "r15", saved_tail_count_offset); - abi::load_at_offset(emitter, "r14", saved_descriptor_offset); - abi::load_at_offset(emitter, "r13", saved_callable_offset); - abi::load_at_offset(emitter, "r12", saved_fiber_offset); - abi::emit_frame_restore(emitter, frame_size); - abi::emit_return(emitter); -} - -/// x86_64-specific spilling of visible parameters and hidden arguments from the Fiber's -/// argument storage into the wrapper frame. Uses r12 to address the Fiber and accesses -/// float/string/scalar slots via the same offset scheme as ARM64 but with x86_64 load -/// instructions and frame slot offsets computed by frame_arg_slot_offset(). -fn spill_wrapper_args_x86_64( - emitter: &mut Emitter, - wrapper: &DeferredFiberWrapper, - arg_types: &[PhpType], - descriptor_reg: &str, -) { - let visible = fixed_visible_param_count(wrapper).min(arg_types.len()); - - for (idx, ty) in arg_types.iter().take(visible).enumerate() { - spill_user_arg_x86_64(emitter, idx, ty, frame_arg_slot_offset(idx)); - } - - let hidden_start = if let Some(variadic_idx) = variadic_param_index(wrapper) { - spill_variadic_start_arg_array_x86_64( - emitter, - wrapper, - visible, - frame_arg_slot_offset(variadic_idx), - ); - variadic_idx + 1 - } else { - visible - }; - - for (idx, ty) in arg_types.iter().enumerate().skip(hidden_start) { - let slot_offset = frame_arg_slot_offset(idx); - spill_descriptor_hidden_arg_x86_64( - emitter, - wrapper, - descriptor_reg, - idx - hidden_start, - ty, - slot_offset, - ); - } -} - -/// Builds and spills the `array` variadic tail for an x86_64 Fiber callback wrapper. -fn spill_variadic_start_arg_array_x86_64( - emitter: &mut Emitter, - wrapper: &DeferredFiberWrapper, - fixed_param_count: usize, - slot_offset: usize, -) { - let has_tail_label = format!("{}_variadic_tail", wrapper.label); - let tail_ready_label = format!("{}_variadic_tail_ready", wrapper.label); - let loop_label = format!("{}_variadic_copy", wrapper.label); - let done_label = format!("{}_variadic_done", wrapper.label); - - emitter.instruction(&format!("mov r15, QWORD PTR [r12 + {}]", runtime::FIBER_START_ARG_COUNT_OFFSET)); // r15 = number of boxed start() values supplied by the caller - if fixed_param_count > 0 { - emitter.instruction(&format!("cmp r15, {}", fixed_param_count)); // did start() supply values beyond the fixed Fiber callback params? - emitter.instruction(&format!("ja {}", has_tail_label)); // compute a non-empty tail when supplied args exceed fixed params - emitter.instruction("xor r15d, r15d"); // no variadic tail values were supplied - emitter.instruction(&format!("jmp {}", tail_ready_label)); // skip the positive-tail subtraction path - emitter.label(&has_tail_label); - emitter.instruction(&format!("sub r15, {}", fixed_param_count)); // r15 = number of values collected into ...$args - emitter.label(&tail_ready_label); - } - - emitter.instruction("mov rdi, 4"); // default variadic tail array capacity - emitter.instruction("cmp r15, 4"); // does the actual variadic tail exceed the small-array default? - emitter.instruction("cmova rdi, r15"); // use the actual tail count when it is larger than four - emitter.instruction("mov rsi, 8"); // variadic tail arrays store boxed Mixed pointers - emitter.instruction("call __rt_array_new"); // allocate the Fiber variadic tail array - abi::store_at_offset(emitter, "rax", slot_offset); - emit_array_value_type_stamp(emitter, "rax", &PhpType::Mixed); - - emitter.instruction("xor ebx, ebx"); // start copying tail values at ...$args[0] - emitter.label(&loop_label); - emitter.instruction("cmp rbx, r15"); // have all supplied variadic tail values been copied? - emitter.instruction(&format!("jae {}", done_label)); // leave the copy loop once tail index >= tail count - if fixed_param_count > 0 { - emitter.instruction("mov r10, rbx"); // r10 = current variadic tail index - emitter.instruction(&format!("add r10, {}", fixed_param_count)); // map tail index to the matching Fiber start_args index - } else { - emitter.instruction("mov r10, rbx"); // tail index already matches the Fiber start_args index - } - emitter.instruction(&format!("mov rax, QWORD PTR [r12 + r10 * 8 + {}]", runtime::FIBER_START_ARGS_OFFSET)); // load the boxed Mixed tail value - emitter.instruction("call __rt_incref"); // retain the boxed Mixed cell for the variadic tail array - abi::load_at_offset(emitter, "r10", slot_offset); - emitter.instruction("mov QWORD PTR [r10 + 24 + rbx * 8], rax"); // store the retained boxed Mixed pointer into the variadic array - emitter.instruction("add rbx, 1"); // advance to the next variadic tail value - emitter.instruction(&format!("jmp {}", loop_label)); // continue copying Fiber start_args into ...$args - emitter.label(&done_label); - abi::load_at_offset(emitter, "r10", slot_offset); - emitter.instruction("mov QWORD PTR [r10], r15"); // publish the final variadic tail array length -} - -/// x86_64-specific spill of one hidden argument from descriptor runtime capture storage. -fn spill_descriptor_hidden_arg_x86_64( - emitter: &mut Emitter, - wrapper: &DeferredFiberWrapper, - descriptor_reg: &str, - capture_index: usize, - ty: &PhpType, - slot_offset: usize, -) { - callable_descriptor::emit_load_runtime_capture_to_result( - emitter, - descriptor_reg, - capture_index, - ty, - ); - match ty { - PhpType::Float => { - abi::store_at_offset(emitter, "xmm0", slot_offset); - } - PhpType::Str => { - let (ptr_reg, len_reg) = abi::string_result_regs(emitter); - abi::store_at_offset(emitter, ptr_reg, slot_offset); - abi::store_at_offset(emitter, len_reg, slot_offset - 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::store_at_offset(emitter, abi::int_result_reg(emitter), slot_offset); - retain_refcounted_capture_for_closure_frame( - emitter, - wrapper, - ty, - abi::int_result_reg(emitter), - ); - } - } -} - -/// x86_64-specific loading and unboxing of a visible user parameter from the Fiber -/// object's start argument area into the wrapper's frame slot. Unboxes via the same -/// __rt_mixed_unbox helper; Float requires movq from rdi to xmm0 for bit reinterpretation. -fn spill_user_arg_x86_64(emitter: &mut Emitter, param_idx: usize, ty: &PhpType, slot_offset: usize) { - let src_offset = runtime::FIBER_START_ARGS_OFFSET + (param_idx as i32) * 8; - emitter.instruction(&format!("mov rax, QWORD PTR [r12 + {}]", src_offset)); // load the boxed Mixed start() argument from the Fiber object - - if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - abi::store_at_offset(emitter, "rax", slot_offset); - return; - } - - abi::emit_call_label(emitter, "__rt_mixed_unbox"); - match ty { - PhpType::Float => { - emitter.instruction("movq xmm0, rdi"); // reinterpret the unboxed float payload bits as xmm0 - abi::store_at_offset(emitter, "xmm0", slot_offset); - } - PhpType::Str => { - abi::store_at_offset(emitter, "rdi", slot_offset); - abi::store_at_offset(emitter, "rdx", slot_offset - 8); - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::store_at_offset(emitter, "rdi", slot_offset); - } - } -} - -/// x86_64-specific materialization of spilled wrapper arguments into ABI registers/stack -/// for the closure call. Differs from ARM64 in that it does not pass frame_size since -/// x86_64 frame slot offsets are computed directly from the slot index. -fn materialize_spilled_args_for_closure_call_x86_64( - emitter: &mut Emitter, - arg_types: &[PhpType], -) -> usize { - push_spilled_args_as_call_temporaries_x86_64(emitter, arg_types); - let assignments = abi::build_outgoing_arg_assignments_for_target(emitter.target, arg_types, 0); - abi::materialize_outgoing_args(emitter, &assignments) -} - -/// x86_64-specific pushing of spilled wrapper arguments from frame slots onto the temporary -/// call stack. Arguments are pushed in reverse order; uses xmm0 for float push and -/// r10/r11 register pair for string push, matching the System V AMD64 ABI conventions. -fn push_spilled_args_as_call_temporaries_x86_64(emitter: &mut Emitter, arg_types: &[PhpType]) { - for (idx, ty) in arg_types.iter().enumerate() { - let slot_offset = frame_arg_slot_offset(idx); - match ty.codegen_repr() { - PhpType::Float => { - abi::load_at_offset(emitter, "xmm0", slot_offset); - abi::emit_push_float_reg(emitter, "xmm0"); // push the prepared float argument onto the standard temporary call stack - } - PhpType::Str => { - abi::load_at_offset(emitter, "r10", slot_offset); - abi::load_at_offset(emitter, "r11", slot_offset - 8); - abi::emit_push_reg_pair(emitter, "r10", "r11"); // push the prepared string argument pair onto the standard temporary call stack - } - PhpType::Void | PhpType::Never => {} - _ => { - abi::load_at_offset(emitter, "r10", slot_offset); - abi::emit_push_reg(emitter, "r10"); // push the prepared scalar/pointer argument onto the standard temporary call stack - } - } - } -} - -/// Computes the x86_64 frame slot offset for argument slot `idx`. Each slot occupies -/// 16 bytes; slot 0 is reserved (holds the return address), so argument i uses slot i+1. -fn frame_arg_slot_offset(idx: usize) -> usize { - (idx + 1) * 16 -} - -/// Rounds `n` up to the nearest 16-byte aligned value. Used to compute frame sizes -/// that satisfy the ABI requirement for callee-saved register spill space and stack -/// alignment at calls. -fn align16(n: usize) -> usize { - (n + 15) & !15 -} diff --git a/src/codegen/functions/generator/build.rs b/src/codegen/functions/generator/build.rs deleted file mode 100644 index 376393c78e..0000000000 --- a/src/codegen/functions/generator/build.rs +++ /dev/null @@ -1,987 +0,0 @@ -//! Purpose: -//! Translates a generator body's parser AST into the narrow `ResumeNode` IR -//! consumed by the emit pass. Runs three internal stages: expression -//! classifiers (`classify_int_expr`, `classify_mixed_expr`, `classify_bool_expr`), -//! locals inference (`collect_locals`/`visit_assignments`), and the node -//! builder (`build_nodes`/`build_node`/`build_else_chain`). -//! -//! Called from: -//! - `crate::codegen::functions::generator::emit_generator_function()` after -//! parameter slots are allocated and before assembly emission begins. -//! -//! Key details: -//! - Classifiers return `None` for shapes outside the v1 grammar — the -//! builder turns those into `ResumeNode::Bail` rather than failing, so -//! the wrapper still compiles and yields nothing past the unsupported -//! construct. -//! - State numbering is depth-first in source order; the emit pass relies -//! on this for resume-label correspondence. - -use super::model::*; -use crate::codegen::data_section::DataSection; -use crate::parser::ast::{BinOp, Expr, ExprKind, Stmt, StmtKind}; -use std::collections::HashSet; - -/// Records how generator locals are later read so yield-assignment slots -/// can choose between the integer fast path and the boxed Mixed path. -#[derive(Default)] -struct SlotUseHints { - int: HashSet, - mixed: HashSet, -} - -/// Look up a slot index for `name`, but only if the slot's type matches -/// `expected`. Returns `None` for missing names or type mismatches — -/// classify_int_expr therefore correctly refuses to read a Mixed slot -/// as an int. -fn slot_idx_of_type(name: &str, slots: &[String], types: &[SlotType], expected: SlotType) -> Option { - let idx = slots.iter().position(|p| p == name)?; - if types.get(idx).copied() == Some(expected) { - Some(idx) - } else { - None - } -} - -/// Classify an expression that can be translated to a v1 `IntSource`. -/// Returns `Some(IntSource)` for int literals, int-typed slot reads, -/// binary ops on int operands, and function calls where all args are -/// int-classifiable and arg count ≤ 8 (ARM64 register limit). Returns -/// `None` for unsupported shapes — the builder turns those into -/// `ResumeNode::Bail`. -pub(super) fn classify_int_expr( - expr: &ExprKind, - slots: &[String], - types: &[SlotType], -) -> Option { - match expr { - ExprKind::IntLiteral(n) => Some(IntSource::Literal(*n)), - ExprKind::Variable(name) => { - slot_idx_of_type(name, slots, types, SlotType::Int).map(IntSource::Slot) - } - ExprKind::BinaryOp { left, op, right } => { - let op = match op { - BinOp::Add => IntBinOp::Add, - BinOp::Sub => IntBinOp::Sub, - BinOp::Mul => IntBinOp::Mul, - BinOp::Div => IntBinOp::Div, - _ => return None, - }; - let l = classify_int_expr(&left.kind, slots, types)?; - let r = classify_int_expr(&right.kind, slots, types)?; - Some(IntSource::BinaryOp(Box::new(l), op, Box::new(r))) - } - ExprKind::FunctionCall { name, args } => { - // ARM64 only has 8 int argument registers; v1 doesn't spill - // arguments onto the stack for the call itself. - if args.len() > 8 { - return None; - } - let fn_name = name.as_str().to_string(); - let mut arg_sources = Vec::with_capacity(args.len()); - for arg in args { - arg_sources.push(classify_int_expr(&arg.kind, slots, types)?); - } - Some(IntSource::Call { fn_name, args: arg_sources }) - } - _ => None, - } -} - -/// Classify an expression that can be translated to a `MixedSource`. -/// Handles: null, string literals (emitted to data section), homogeneous -/// int-array literals, Mixed-typed slot reads, and any int-classifiable -/// expression (mapped to `MixedSource::Int`). Returns `None` for -/// unsupported shapes, which become `ResumeNode::Bail`. -pub(super) fn classify_mixed_expr( - expr: &ExprKind, - slots: &[String], - types: &[SlotType], - data: &mut DataSection, -) -> Option { - if matches!(expr, ExprKind::Null) { - return Some(MixedSource::Null); - } - if let ExprKind::StringLiteral(s) = expr { - let bytes = crate::string_bytes::literal_bytes(s); - let (label, len) = data.add_string(&bytes); - return Some(MixedSource::Str { label, len }); - } - if let ExprKind::ArrayLiteral(items) = expr { - // Homogeneous int-array literal: `yield [1, 2, 3]`. - let mut values = Vec::with_capacity(items.len()); - for item in items { - if let ExprKind::IntLiteral(n) = &item.kind { - values.push(*n); - } else { - return None; - } - } - return Some(MixedSource::IntArrayLit(values)); - } - // Reads of Mixed-typed slots (e.g. a local that was assigned a string - // literal or an array literal earlier in the body). - if let ExprKind::Variable(name) = expr { - if let Some(idx) = slot_idx_of_type(name, slots, types, SlotType::Mixed) { - return Some(MixedSource::MixedSlot(idx)); - } - } - classify_int_expr(expr, slots, types).map(MixedSource::Int) -} - -/// Classify a boolean expression for v1 generator conditionals. -/// Supports integer comparisons and strict/loose null checks against -/// Mixed-typed slots. Returns `None` for unsupported operands — the -/// builder turns those into `ResumeNode::Bail`. -pub(super) fn classify_bool_expr( - expr: &ExprKind, - slots: &[String], - types: &[SlotType], -) -> Option { - if let ExprKind::BinaryOp { left, op, right } = expr { - if matches!(op, BinOp::Eq | BinOp::StrictEq | BinOp::NotEq | BinOp::StrictNotEq) { - if let Some(slot_idx) = mixed_slot_null_cmp(left, right, slots, types) { - return Some(BoolExpr::MixedSlotNull { - slot_idx, - is_equal: matches!(op, BinOp::Eq | BinOp::StrictEq), - }); - } - if let Some(slot_idx) = mixed_slot_null_cmp(right, left, slots, types) { - return Some(BoolExpr::MixedSlotNull { - slot_idx, - is_equal: matches!(op, BinOp::Eq | BinOp::StrictEq), - }); - } - } - let cmp = match op { - BinOp::Lt => CmpOp::Lt, - BinOp::LtEq => CmpOp::Le, - BinOp::Gt => CmpOp::Gt, - BinOp::GtEq => CmpOp::Ge, - BinOp::Eq | BinOp::StrictEq => CmpOp::Eq, - BinOp::NotEq | BinOp::StrictNotEq => CmpOp::Ne, - _ => return None, - }; - let l = classify_int_expr(&left.kind, slots, types)?; - let r = classify_int_expr(&right.kind, slots, types)?; - return Some(BoolExpr::IntCompare { - left: l, - op: cmp, - right: r, - }); - } - None -} - -/// Returns the Mixed slot index when `value` is a Mixed variable and -/// `null_candidate` is the PHP null literal. -fn mixed_slot_null_cmp( - value: &Expr, - null_candidate: &Expr, - slots: &[String], - types: &[SlotType], -) -> Option { - if !matches!(null_candidate.kind, ExprKind::Null) { - return None; - } - let ExprKind::Variable(name) = &value.kind else { - return None; - }; - slot_idx_of_type(name, slots, types, SlotType::Mixed) -} - -/// Collects variable-use hints from the generator body before slot -/// inference. The scan mirrors the narrow generator IR: arithmetic, -/// comparisons, counters, and helper-call arguments are int contexts, -/// while echo, var_dump, return, and yielded values are Mixed contexts. -fn collect_slot_use_hints(body: &[Stmt]) -> SlotUseHints { - let mut hints = SlotUseHints::default(); - record_stmt_use_hints(body, &mut hints); - hints -} - -/// Walks statements in source order, adding variable names to the -/// relevant usage set for slot inference. Unsupported statements are -/// ignored here because the builder will still turn them into `Bail`. -fn record_stmt_use_hints(body: &[Stmt], hints: &mut SlotUseHints) { - for stmt in body { - match &stmt.kind { - StmtKind::Echo(expr) => record_mixed_expr_use_hints(&expr.kind, hints), - StmtKind::Assign { value, .. } | StmtKind::TypedAssign { value, .. } => { - record_assignment_rhs_use_hints(&value.kind, hints); - } - StmtKind::If { - condition, - then_body, - elseif_clauses, - else_body, - } => { - record_bool_expr_use_hints(&condition.kind, hints); - record_stmt_use_hints(then_body, hints); - for (elseif_cond, elseif_body) in elseif_clauses { - record_bool_expr_use_hints(&elseif_cond.kind, hints); - record_stmt_use_hints(elseif_body, hints); - } - if let Some(body) = else_body { - record_stmt_use_hints(body, hints); - } - } - StmtKind::While { condition, body } => { - record_bool_expr_use_hints(&condition.kind, hints); - record_stmt_use_hints(body, hints); - } - StmtKind::DoWhile { body, condition } => { - record_stmt_use_hints(body, hints); - record_bool_expr_use_hints(&condition.kind, hints); - } - StmtKind::For { init, condition, update, body } => { - if let Some(init_stmt) = init.as_deref() { - record_stmt_use_hints(std::slice::from_ref(init_stmt), hints); - } - if let Some(cond) = condition { - record_bool_expr_use_hints(&cond.kind, hints); - } - if let Some(update_stmt) = update.as_deref() { - record_stmt_use_hints(std::slice::from_ref(update_stmt), hints); - } - record_stmt_use_hints(body, hints); - } - StmtKind::Switch { subject, cases, default } => { - record_int_expr_use_hints(&subject.kind, hints); - for (_, case_body) in cases { - record_stmt_use_hints(case_body, hints); - } - if let Some(body) = default { - record_stmt_use_hints(body, hints); - } - } - StmtKind::Synthetic(stmts) => record_stmt_use_hints(stmts, hints), - StmtKind::Try { - try_body, - catches, - finally_body, - } => { - record_stmt_use_hints(try_body, hints); - for catch in catches { - record_stmt_use_hints(&catch.body, hints); - } - if let Some(body) = finally_body { - record_stmt_use_hints(body, hints); - } - } - StmtKind::ExprStmt(expr) => record_expr_stmt_use_hints(&expr.kind, hints), - StmtKind::Return(Some(expr)) => record_mixed_expr_use_hints(&expr.kind, hints), - _ => {} - } - } -} - -/// Records uses inside an assignment RHS. Yield values are observed by -/// the caller as Mixed values; arithmetic expressions keep their operand -/// variables on the int path. -fn record_assignment_rhs_use_hints(expr: &ExprKind, hints: &mut SlotUseHints) { - match expr { - ExprKind::Yield { key, value } => { - if let Some(k) = key { - record_mixed_expr_use_hints(&k.kind, hints); - } - if let Some(v) = value { - record_mixed_expr_use_hints(&v.kind, hints); - } - } - ExprKind::YieldFrom(inner) => record_mixed_expr_use_hints(&inner.kind, hints), - ExprKind::BinaryOp { .. } | ExprKind::FunctionCall { .. } => { - record_int_expr_use_hints(expr, hints); - } - _ => record_mixed_expr_use_hints(expr, hints), - } -} - -/// Records uses inside expression statements that the generator IR knows -/// how to lower, including var_dump diagnostics and post-increment style -/// counters. -fn record_expr_stmt_use_hints(expr: &ExprKind, hints: &mut SlotUseHints) { - match expr { - ExprKind::Yield { key, value } => { - if let Some(k) = key { - record_mixed_expr_use_hints(&k.kind, hints); - } - if let Some(v) = value { - record_mixed_expr_use_hints(&v.kind, hints); - } - } - ExprKind::YieldFrom(inner) => record_mixed_expr_use_hints(&inner.kind, hints), - ExprKind::PostIncrement(name) - | ExprKind::PostDecrement(name) - | ExprKind::PreIncrement(name) - | ExprKind::PreDecrement(name) => { - hints.int.insert(name.clone()); - } - ExprKind::FunctionCall { name, args } if is_var_dump_call(name.as_str()) => { - for arg in args { - record_mixed_expr_use_hints(&arg.kind, hints); - } - } - _ => {} - } -} - -/// Records variables that are read through a Mixed-capable generator -/// path. Supported int expressions nested inside a Mixed context keep -/// their operands int-typed because `classify_mixed_expr` boxes int -/// expressions after evaluating them. -fn record_mixed_expr_use_hints(expr: &ExprKind, hints: &mut SlotUseHints) { - match expr { - ExprKind::Variable(name) => { - hints.mixed.insert(name.clone()); - } - ExprKind::BinaryOp { left, op: BinOp::Concat, right } => { - record_mixed_expr_use_hints(&left.kind, hints); - record_mixed_expr_use_hints(&right.kind, hints); - } - ExprKind::BinaryOp { left, op, right } if is_generator_int_binop(op) => { - record_int_expr_use_hints(&left.kind, hints); - record_int_expr_use_hints(&right.kind, hints); - } - ExprKind::FunctionCall { args, .. } => { - for arg in args { - record_int_expr_use_hints(&arg.kind, hints); - } - } - ExprKind::ArrayLiteral(items) => { - for item in items { - record_mixed_expr_use_hints(&item.kind, hints); - } - } - ExprKind::Ternary { - condition, - then_expr, - else_expr, - } => { - record_bool_expr_use_hints(&condition.kind, hints); - record_mixed_expr_use_hints(&then_expr.kind, hints); - record_mixed_expr_use_hints(&else_expr.kind, hints); - } - ExprKind::Yield { key, value } => { - if let Some(k) = key { - record_mixed_expr_use_hints(&k.kind, hints); - } - if let Some(v) = value { - record_mixed_expr_use_hints(&v.kind, hints); - } - } - ExprKind::YieldFrom(inner) - | ExprKind::Negate(inner) - | ExprKind::Not(inner) - | ExprKind::BitNot(inner) - | ExprKind::Print(inner) - | ExprKind::ErrorSuppress(inner) - | ExprKind::Throw(inner) - | ExprKind::Cast { expr: inner, .. } => { - record_mixed_expr_use_hints(&inner.kind, hints); - } - _ => {} - } -} - -/// Records variable uses inside boolean expressions. Null checks against -/// variables require Mixed slots; other supported comparisons stay int-typed. -fn record_bool_expr_use_hints(expr: &ExprKind, hints: &mut SlotUseHints) { - match expr { - ExprKind::BinaryOp { left, op, right } - if matches!(op, BinOp::Eq | BinOp::StrictEq | BinOp::NotEq | BinOp::StrictNotEq) - && matches!(right.kind, ExprKind::Null) => - { - record_mixed_expr_use_hints(&left.kind, hints); - } - ExprKind::BinaryOp { left, op, right } - if matches!(op, BinOp::Eq | BinOp::StrictEq | BinOp::NotEq | BinOp::StrictNotEq) - && matches!(left.kind, ExprKind::Null) => - { - record_mixed_expr_use_hints(&right.kind, hints); - } - _ => record_int_expr_use_hints(expr, hints), - } -} - -/// Records variables that are read through int-only generator paths such -/// as arithmetic, comparisons, loop counters, and integer helper calls. -fn record_int_expr_use_hints(expr: &ExprKind, hints: &mut SlotUseHints) { - match expr { - ExprKind::Variable(name) => { - hints.int.insert(name.clone()); - } - ExprKind::BinaryOp { left, right, .. } => { - record_int_expr_use_hints(&left.kind, hints); - record_int_expr_use_hints(&right.kind, hints); - } - ExprKind::FunctionCall { args, .. } => { - for arg in args { - record_int_expr_use_hints(&arg.kind, hints); - } - } - ExprKind::Negate(inner) - | ExprKind::Cast { expr: inner, .. } - | ExprKind::ErrorSuppress(inner) => { - record_int_expr_use_hints(&inner.kind, hints); - } - _ => {} - } -} - -/// Returns true when the operator is one of the arithmetic operators -/// supported by the current generator integer-expression classifier. -fn is_generator_int_binop(op: &BinOp) -> bool { - matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div) -} - -/// Collected locals with their inferred slot types. Each local is -/// assigned a single SlotType for the lifetime of the generator: the -/// type is decided by the *first* assignment seen in source order. -pub(super) fn collect_locals(body: &[Stmt], param_names: &[String]) -> Vec<(String, SlotType)> { - let mut locals: Vec<(String, SlotType)> = Vec::new(); - let hints = collect_slot_use_hints(body); - // `probe`/`probe_types` mirror the eventual params+locals slot table - // so that classify_int_expr can resolve previously-introduced int - // locals while we walk subsequent assignments. - let mut probe: Vec = param_names.to_vec(); - let mut probe_types: Vec = vec![SlotType::Int; param_names.len()]; - visit_assignments( - body, - &mut probe, - &mut probe_types, - &mut locals, - param_names, - &hints, - ); - locals -} - -/// Recursively collects variable assignments within `body`, inferring -/// each local's `SlotType` from the first assignment seen (source order). -/// Skips parameters in `param_names`. Populates `probe`/`probe_types` -/// incrementally so later assignments can reference previously-introduced -/// locals during type inference. -fn visit_assignments( - body: &[Stmt], - probe: &mut Vec, - probe_types: &mut Vec, - locals: &mut Vec<(String, SlotType)>, - param_names: &[String], - hints: &SlotUseHints, -) { - for stmt in body { - match &stmt.kind { - StmtKind::Assign { name, value } | StmtKind::TypedAssign { name, value, .. } => { - if param_names.iter().any(|p| p == name) { - continue; - } - if locals.iter().any(|(l, _)| l == name) { - continue; - } - let inferred = infer_slot_type(name, &value.kind, probe, probe_types, hints); - if let Some(ty) = inferred { - locals.push((name.clone(), ty)); - probe.push(name.clone()); - probe_types.push(ty); - } - } - StmtKind::If { - then_body, - elseif_clauses, - else_body, - .. - } => { - visit_assignments(then_body, probe, probe_types, locals, param_names, hints); - for (_, b) in elseif_clauses { - visit_assignments(b, probe, probe_types, locals, param_names, hints); - } - if let Some(eb) = else_body { - visit_assignments(eb, probe, probe_types, locals, param_names, hints); - } - } - StmtKind::While { body, .. } | StmtKind::DoWhile { body, .. } => { - visit_assignments(body, probe, probe_types, locals, param_names, hints); - } - StmtKind::For { init, body, .. } => { - if let Some(init_stmt) = init.as_deref() { - visit_assignments( - std::slice::from_ref(init_stmt), - probe, - probe_types, - locals, - param_names, - hints, - ); - } - visit_assignments(body, probe, probe_types, locals, param_names, hints); - } - StmtKind::Synthetic(stmts) => { - visit_assignments(stmts, probe, probe_types, locals, param_names, hints); - } - StmtKind::Try { - try_body, - catches, - finally_body, - } => { - visit_assignments(try_body, probe, probe_types, locals, param_names, hints); - for catch in catches { - visit_assignments(&catch.body, probe, probe_types, locals, param_names, hints); - } - if let Some(body) = finally_body { - visit_assignments(body, probe, probe_types, locals, param_names, hints); - } - } - _ => {} - } - } -} - -/// Decide whether an assignment RHS makes the LHS local an Int slot or a -/// Mixed slot. Returns `None` if neither classifier accepts the RHS, in -/// which case the local stays unallocated and any later use bails the -/// generator at that point. -fn infer_slot_type( - name: &str, - rhs: &ExprKind, - probe: &[String], - probe_types: &[SlotType], - hints: &SlotUseHints, -) -> Option { - if classify_int_expr(rhs, probe, probe_types).is_some() { - return Some(SlotType::Int); - } - // String literal / int-array literal / Mixed slot read on the RHS - // means this local is Mixed-typed. - match rhs { - ExprKind::StringLiteral(_) | ExprKind::ArrayLiteral(_) => Some(SlotType::Mixed), - ExprKind::Variable(name) => { - let idx = probe.iter().position(|p| p == name)?; - probe_types.get(idx).copied() - } - // `$local = yield ;` receives the value supplied by - // `Generator::send()`, not the yielded expression. Use the boxed - // path unless later integer-only operations require the historical - // int fast path. - ExprKind::Yield { .. } => { - if hints.int.contains(name) { - Some(SlotType::Int) - } else { - Some(SlotType::Mixed) - } - } - // `yield from` evaluates to the delegated generator's return - // value. Store that boxed result in a Mixed slot so it remains - // available after the delegation completes. - ExprKind::YieldFrom(_) => Some(SlotType::Mixed), - _ => None, - } -} - -/// Walks the statement list, building a vector of `ResumeNode`s. -/// Stops on the first `Bail` node and returns what was accumulated -/// up to that point. -pub(super) fn build_nodes( - body: &[Stmt], - slots: &[String], - types: &[SlotType], - num: &mut StateNumberer, - data: &mut DataSection, -) -> Vec { - let mut out = Vec::new(); - for stmt in body { - match build_node(stmt, slots, types, num, data) { - Some(node) => { - let bail = matches!(node, ResumeNode::Bail); - out.push(node); - if bail { - return out; - } - } - None => { - out.push(ResumeNode::Bail); - return out; - } - } - } - out -} - -/// Translates a single statement into a `ResumeNode`. Returns `None` -/// for unsupported constructs — the caller converts this to -/// `ResumeNode::Bail`. Handles assign, expr-stmt, if/while/do-while/for -/// loops, break/continue, return, switch, try/finally, echo, and yield/yield-from. -fn build_node( - stmt: &Stmt, - slots: &[String], - types: &[SlotType], - num: &mut StateNumberer, - data: &mut DataSection, -) -> Option { - match &stmt.kind { - StmtKind::Assign { name, value } | StmtKind::TypedAssign { name, value, .. } => { - let idx = slots.iter().position(|p| p == name)?; - // `$local = yield ;` — translate as YieldAssign. The - // slot type decides whether the sent value is unboxed to int - // or moved as an owned Mixed cell. - if let ExprKind::Yield { key, value } = &value.kind { - let local_ty = types.get(idx).copied()?; - let yield_value = match value.as_deref() { - Some(v) => classify_mixed_expr(&v.kind, slots, types, data)?, - None => MixedSource::Null, - }; - let yield_key = match key.as_deref() { - None => None, - Some(Expr { kind: k, .. }) => Some(classify_mixed_expr(k, slots, types, data)?), - }; - let state_idx = num.next(); - return Some(ResumeNode::YieldAssign { - local_idx: idx, - local_ty, - yield_entry: YieldEntry { key: yield_key, value: yield_value }, - state_idx, - }); - } - if let ExprKind::YieldFrom(inner) = &value.kind { - if types.get(idx).copied() != Some(SlotType::Mixed) { - return None; - } - return build_yield_from_node( - inner, - YieldFromResult::Local(idx), - slots, - types, - num, - data, - ); - } - // Otherwise: dispatch on the slot's type. - match types.get(idx).copied() { - Some(SlotType::Int) => { - let src = classify_int_expr(&value.kind, slots, types)?; - Some(ResumeNode::Stmt(BodyStmt::AssignInt(idx, src))) - } - Some(SlotType::Mixed) => { - let src = classify_mixed_expr(&value.kind, slots, types, data)?; - Some(ResumeNode::Stmt(BodyStmt::AssignMixed(idx, src))) - } - None => None, - } - } - StmtKind::ExprStmt(expr) => match &expr.kind { - ExprKind::YieldFrom(inner) => { - build_yield_from_node( - inner, - YieldFromResult::Discard, - slots, - types, - num, - data, - ) - } - ExprKind::Yield { key, value } => { - let value = match value.as_deref() { - Some(v) => classify_mixed_expr(&v.kind, slots, types, data)?, - None => MixedSource::Null, - }; - let key = match key.as_deref() { - None => None, - Some(Expr { kind: k, .. }) => Some(classify_mixed_expr(k, slots, types, data)?), - }; - let state = num.next(); - Some(ResumeNode::Yield(YieldEntry { key, value }, state)) - } - ExprKind::PostIncrement(name) => { - let idx = slots.iter().position(|p| p == name)?; - if types.get(idx).copied() != Some(SlotType::Int) { - return None; - } - Some(ResumeNode::Stmt(BodyStmt::PostIncrement(idx))) - } - ExprKind::PostDecrement(name) => { - let idx = slots.iter().position(|p| p == name)?; - if types.get(idx).copied() != Some(SlotType::Int) { - return None; - } - Some(ResumeNode::Stmt(BodyStmt::PostDecrement(idx))) - } - ExprKind::FunctionCall { name, args } if is_var_dump_call(name.as_str()) => { - let mut stmts = Vec::with_capacity(args.len()); - for arg in args { - let src = classify_mixed_expr(&arg.kind, slots, types, data)?; - stmts.push(ResumeNode::Stmt(BodyStmt::VarDumpMixed(src))); - } - Some(ResumeNode::Block { stmts }) - } - _ => None, - }, - StmtKind::Echo(expr) => build_echo_node(expr, slots, types, data), - StmtKind::If { - condition, - then_body, - elseif_clauses, - else_body, - } => { - let cond = classify_bool_expr(&condition.kind, slots, types)?; - let then_nodes = build_nodes(then_body, slots, types, num, data); - let else_nodes = build_else_chain(elseif_clauses, else_body, slots, types, num, data)?; - Some(ResumeNode::If { - cond, - then_body: then_nodes, - else_body: else_nodes, - }) - } - StmtKind::While { condition, body } => { - let cond = classify_bool_expr(&condition.kind, slots, types)?; - let body_nodes = build_nodes(body, slots, types, num, data); - Some(ResumeNode::While { cond, body: body_nodes }) - } - StmtKind::DoWhile { body, condition } => { - let cond = classify_bool_expr(&condition.kind, slots, types)?; - let body_nodes = build_nodes(body, slots, types, num, data); - Some(ResumeNode::DoWhile { cond, body: body_nodes }) - } - StmtKind::For { init, condition, update, body } => { - let init_nodes = match init.as_deref() { - Some(s) => build_nodes(std::slice::from_ref(s), slots, types, num, data), - None => Vec::new(), - }; - let cond = match condition { - Some(c) => classify_bool_expr(&c.kind, slots, types)?, - None => return None, - }; - let body_nodes = build_nodes(body, slots, types, num, data); - let update_nodes = match update.as_deref() { - Some(s) => build_nodes(std::slice::from_ref(s), slots, types, num, data), - None => Vec::new(), - }; - Some(ResumeNode::For { - init: init_nodes, - cond, - update: update_nodes, - body: body_nodes, - }) - } - StmtKind::Break(_) => Some(ResumeNode::Break), - StmtKind::Continue(_) => Some(ResumeNode::Continue), - StmtKind::Return(opt) => { - if let Some(expr) = opt { - if let ExprKind::YieldFrom(inner) = &expr.kind { - return build_yield_from_node( - inner, - YieldFromResult::Return, - slots, - types, - num, - data, - ); - } - } - let value = match opt { - Some(expr) => Some(classify_mixed_expr(&expr.kind, slots, types, data)?), - None => None, - }; - Some(ResumeNode::Return(value)) - } - StmtKind::Switch { subject, cases, default } => { - let subject_src = classify_int_expr(&subject.kind, slots, types)?; - let mut translated_cases: Vec<(Vec, Vec)> = Vec::new(); - for (values, body) in cases { - let mut int_values = Vec::with_capacity(values.len()); - for v in values { - if let ExprKind::IntLiteral(n) = &v.kind { - int_values.push(*n); - } else { - return None; - } - } - let body_nodes = build_nodes(body, slots, types, num, data); - translated_cases.push((int_values, body_nodes)); - } - let default_nodes = match default { - Some(d) => build_nodes(d, slots, types, num, data), - None => Vec::new(), - }; - Some(ResumeNode::Switch { - subject: subject_src, - cases: translated_cases, - default: default_nodes, - }) - } - StmtKind::Try { - try_body, - finally_body, - .. - } => { - let try_nodes = build_nodes(try_body, slots, types, num, data); - let finally_nodes = finally_body - .as_ref() - .map(|body| build_nodes(body, slots, types, num, data)) - .unwrap_or_default(); - Some(ResumeNode::Try { - try_body: try_nodes, - finally_body: finally_nodes, - }) - } - StmtKind::Synthetic(stmts) => Some(ResumeNode::Block { - stmts: build_nodes(stmts, slots, types, num, data), - }), - _ => None, - } -} - -/// Translates `echo ` into generator IR. -/// -/// The narrow generator IR lowers concat echo expressions by emitting each -/// operand in source order, and lowers ternary echo expressions as an `If` -/// whose branches each emit one echo. Other expressions use the normal Mixed -/// boxing path. -fn build_echo_node( - expr: &Expr, - slots: &[String], - types: &[SlotType], - data: &mut DataSection, -) -> Option { - if let ExprKind::BinaryOp { - left, - op: BinOp::Concat, - right, - } = &expr.kind - { - return Some(ResumeNode::Block { - stmts: vec![ - build_echo_node(left, slots, types, data)?, - build_echo_node(right, slots, types, data)?, - ], - }); - } - - if let ExprKind::Ternary { - condition, - then_expr, - else_expr, - } = &expr.kind - { - let cond = classify_bool_expr(&condition.kind, slots, types)?; - let then_node = build_echo_node(then_expr, slots, types, data)?; - let else_node = build_echo_node(else_expr, slots, types, data)?; - return Some(ResumeNode::If { - cond, - then_body: vec![then_node], - else_body: vec![else_node], - }); - } - - let src = classify_mixed_expr(&expr.kind, slots, types, data)?; - Some(ResumeNode::Stmt(BodyStmt::EchoMixed(src))) -} - -/// Returns true for PHP's global `var_dump` builtin name as it may appear -/// after name resolution. Generator lowering uses this to keep simple -/// diagnostic expression statements from bailing the narrow generator IR. -fn is_var_dump_call(name: &str) -> bool { - name.trim_start_matches('\\').eq_ignore_ascii_case("var_dump") -} - -/// Translates a `yield from` expression into a `ResumeNode`. Handles -/// three shapes: array literal (unpacked into individual yields), -/// function call (yield-from-generator with Call source, arg count ≤ 8), -/// and variable (yield-from-generator with IntSlot or MixedSlot source). -/// `result` indicates how the final value is consumed (Discard, Local, -/// or Return). Returns `None` for unsupported shapes. -fn build_yield_from_node( - inner: &Expr, - result: YieldFromResult, - slots: &[String], - types: &[SlotType], - num: &mut StateNumberer, - data: &mut DataSection, -) -> Option { - if let ExprKind::ArrayLiteral(items) = &inner.kind { - let mut stmts = Vec::new(); - for item in items { - let value = classify_mixed_expr(&item.kind, slots, types, data)?; - let state = num.next(); - stmts.push(ResumeNode::Yield(YieldEntry { key: None, value }, state)); - } - match result { - YieldFromResult::Discard => {} - YieldFromResult::Local(idx) => { - stmts.push(ResumeNode::Stmt(BodyStmt::AssignMixed(idx, MixedSource::Null))); - } - YieldFromResult::Return => { - stmts.push(ResumeNode::Return(Some(MixedSource::Null))); - } - } - return Some(ResumeNode::Block { stmts }); - } - if let ExprKind::FunctionCall { name, args } = &inner.kind { - if args.len() > 8 { - return None; - } - let mut arg_sources = Vec::with_capacity(args.len()); - for arg in args { - arg_sources.push(classify_int_expr(&arg.kind, slots, types)?); - } - let state_idx = num.next(); - return Some(ResumeNode::YieldFromGenerator { - source: YieldFromSource::Call { - fn_name: name.as_str().to_string(), - args: arg_sources, - }, - state_idx, - result, - }); - } - if let ExprKind::Variable(name) = &inner.kind { - // `yield from $local` — the slot holds either a raw Generator - // pointer (Int-typed slot) or a boxed Mixed cell wrapping an - // Object payload (Mixed slot). - if let Some(idx) = slot_idx_of_type(name, slots, types, SlotType::Int) { - let state_idx = num.next(); - return Some(ResumeNode::YieldFromGenerator { - source: YieldFromSource::IntSlot(idx), - state_idx, - result, - }); - } - if let Some(idx) = slot_idx_of_type(name, slots, types, SlotType::Mixed) { - let state_idx = num.next(); - return Some(ResumeNode::YieldFromGenerator { - source: YieldFromSource::MixedSlot(idx), - state_idx, - result, - }); - } - } - None -} - -/// Recursively translates if/else-if/else chains into a flat vector of -/// nested `ResumeNode::If` nodes. The else branch is resolved by -/// re-invoking `build_nodes`. Returns `Some(nodes)` or `None` if any -/// condition fails to classify. -fn build_else_chain( - elseif_clauses: &[(Expr, Vec)], - else_body: &Option>, - slots: &[String], - types: &[SlotType], - num: &mut StateNumberer, - data: &mut DataSection, -) -> Option> { - if let Some(((cond_expr, then_body), rest)) = elseif_clauses.split_first() { - let cond = classify_bool_expr(&cond_expr.kind, slots, types)?; - let then_nodes = build_nodes(then_body, slots, types, num, data); - let rest_vec: Vec<(Expr, Vec)> = rest.to_vec(); - let else_nodes = build_else_chain(&rest_vec, else_body, slots, types, num, data)?; - Some(vec![ResumeNode::If { - cond, - then_body: then_nodes, - else_body: else_nodes, - }]) - } else if let Some(eb) = else_body { - Some(build_nodes(eb, slots, types, num, data)) - } else { - Some(Vec::new()) - } -} diff --git a/src/codegen/functions/generator/emit/dispatcher.rs b/src/codegen/functions/generator/emit/dispatcher.rs deleted file mode 100644 index bfa143879a..0000000000 --- a/src/codegen/functions/generator/emit/dispatcher.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Purpose: -//! Emits the `_fn___resume` state-machine entry point: prologue, state-idx -//! dispatch table, body invocation through `stmts::emit_nodes`, and the -//! shared terminator/epilogue that releases retained Mixed slots. -//! -//! Called from: -//! - `crate::codegen::functions::generator::emit_generator_function()` via -//! the parent module's `emit_resume` re-export. -//! -//! Key details: -//! - State 0 is the body entry; states 1..N each have a corresponding -//! `